##// END OF EJS Templates
pull-request: extended default reviewers functionality....
marcink -
r1769:fb9baff8 default
parent child Browse files
Show More

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

@@ -0,0 +1,83 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 from rhodecode.model.db import Repository
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
36
37
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
41
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed Pull Requests'),
44 ({'source': 0, 'my': 1}, 'opened by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'),
47 ({'source': 1}, 'Pull Requests from'),
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
51
52 response = self.app.get(
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
56
57 assert_response = response.assert_response()
58 assert_response.element_equals_to('.panel-title', expected_title)
59 element = assert_response.get_element('.panel-title')
60 element_text = assert_response._element_to_string(element)
61
62 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 pull_request = pr_util.create_pull_request()
64 response = self.app.get(
65 route_path('pullrequest_show_all_data',
66 repo_name=pull_request.target_repo.repo_name),
67 extra_environ=xhr_header)
68
69 assert response.json['recordsTotal'] == 1
70 assert response.json['data'][0]['description'] == 'Description'
71
72 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 xss_description = "<script>alert('Hi!')</script>"
74 pull_request = pr_util.create_pull_request(description=xss_description)
75
76 response = self.app.get(
77 route_path('pullrequest_show_all_data',
78 repo_name=pull_request.target_repo.repo_name),
79 extra_environ=xhr_header)
80
81 assert response.json['recordsTotal'] == 1
82 assert response.json['data'][0]['description'] == \
83 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -0,0 +1,76 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
23
24
25 def reviewer_as_json(user, reasons, mandatory):
26 """
27 Returns json struct of a reviewer for frontend
28
29 :param user: the reviewer
30 :param reasons: list of strings of why they are reviewers
31 :param mandatory: bool, to set user as mandatory
32 """
33
34 return {
35 'user_id': user.user_id,
36 'reasons': reasons,
37 'mandatory': mandatory,
38 'username': user.username,
39 'firstname': user.firstname,
40 'lastname': user.lastname,
41 'gravatar_link': h.gravatar_url(user.email, 14),
42 }
43
44
45 def get_default_reviewers_data(
46 current_user, source_repo, source_commit, target_repo, target_commit):
47
48 """ Return json for default reviewers of a repository """
49
50 reasons = ['Default reviewer', 'Repository owner']
51 default = reviewer_as_json(
52 user=current_user, reasons=reasons, mandatory=False)
53
54 return {
55 'api_ver': 'v1', # define version for later possible schema upgrade
56 'reviewers': [default],
57 'rules': {},
58 'rules_data': {},
59 }
60
61
62 def validate_default_reviewers(review_members, reviewer_rules):
63 """
64 Function to validate submitted reviewers against the saved rules
65
66 """
67 reviewers = []
68 reviewer_by_id = {}
69 for r in review_members:
70 reviewer_user_id = safe_int(r['user_id'])
71 entry = (reviewer_user_id, r['reasons'], r['mandatory'])
72
73 reviewer_by_id[reviewer_user_id] = entry
74 reviewers.append(entry)
75
76 return reviewers
@@ -0,0 +1,37 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_table = db.RepoReviewRule.__table__
19
20 forbid_author_to_review = Column(
21 "forbid_author_to_review", Boolean(), nullable=True, default=False)
22 forbid_author_to_review.create(table=repo_review_rule_table)
23
24 forbid_adding_reviewers = Column(
25 "forbid_adding_reviewers", Boolean(), nullable=True, default=False)
26 forbid_adding_reviewers.create(table=repo_review_rule_table)
27
28 fixups(db, meta.Session)
29
30
31 def downgrade(migrate_engine):
32 meta = MetaData()
33 meta.bind = migrate_engine
34
35
36 def fixups(models, _SESSION):
37 pass
@@ -0,0 +1,38 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_user_table = db.RepoReviewRuleUser.__table__
19 repo_review_rule_user_group_table = db.RepoReviewRuleUserGroup.__table__
20
21 mandatory_user = Column(
22 "mandatory", Boolean(), nullable=True, default=False)
23 mandatory_user.create(table=repo_review_rule_user_table)
24
25 mandatory_user_group = Column(
26 "mandatory", Boolean(), nullable=True, default=False)
27 mandatory_user_group.create(table=repo_review_rule_user_group_table)
28
29 fixups(db, meta.Session)
30
31
32 def downgrade(migrate_engine):
33 meta = MetaData()
34 meta.bind = migrate_engine
35
36
37 def fixups(models, _SESSION):
38 pass
@@ -0,0 +1,33 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request_reviewers = db.PullRequestReviewers.__table__
19
20 mandatory = Column(
21 "mandatory", Boolean(), nullable=True, default=False)
22 mandatory.create(table=pull_request_reviewers)
23
24 fixups(db, meta.Session)
25
26
27 def downgrade(migrate_engine):
28 meta = MetaData()
29 meta.bind = migrate_engine
30
31
32 def fixups(models, _SESSION):
33 pass
@@ -0,0 +1,40 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request = db.PullRequest.__table__
19 pull_request_version = db.PullRequestVersion.__table__
20
21 reviewer_data_1 = Column(
22 'reviewer_data_json',
23 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
24 reviewer_data_1.create(table=pull_request)
25
26 reviewer_data_2 = Column(
27 'reviewer_data_json',
28 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
29 reviewer_data_2.create(table=pull_request_version)
30
31 fixups(db, meta.Session)
32
33
34 def downgrade(migrate_engine):
35 meta = MetaData()
36 meta.bind = migrate_engine
37
38
39 def fixups(models, _SESSION):
40 pass
@@ -0,0 +1,34 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 from rhodecode.model.validation_schema import validators, preparers, types
23
24
25 class ReviewerSchema(colander.MappingSchema):
26 username = colander.SchemaNode(types.StrOrIntType())
27 reasons = colander.SchemaNode(colander.List(), missing=[])
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29
30
31 class ReviewerListSchema(colander.SequenceSchema):
32 reviewers = ReviewerSchema()
33
34
@@ -1,63 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22
22
23 RhodeCode, a web based repository management software
23 RhodeCode, a web based repository management software
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 """
25 """
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import platform
29 import platform
30
30
31 VERSION = tuple(open(os.path.join(
31 VERSION = tuple(open(os.path.join(
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33
33
34 BACKENDS = {
34 BACKENDS = {
35 'hg': 'Mercurial repository',
35 'hg': 'Mercurial repository',
36 'git': 'Git repository',
36 'git': 'Git repository',
37 'svn': 'Subversion repository',
37 'svn': 'Subversion repository',
38 }
38 }
39
39
40 CELERY_ENABLED = False
40 CELERY_ENABLED = False
41 CELERY_EAGER = False
41 CELERY_EAGER = False
42
42
43 # link to config for pylons
43 # link to config for pylons
44 CONFIG = {}
44 CONFIG = {}
45
45
46 # Populated with the settings dictionary from application init in
46 # Populated with the settings dictionary from application init in
47 # rhodecode.conf.environment.load_pyramid_environment
47 # rhodecode.conf.environment.load_pyramid_environment
48 PYRAMID_SETTINGS = {}
48 PYRAMID_SETTINGS = {}
49
49
50 # Linked module for extensions
50 # Linked module for extensions
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 73 # defines current db version for migrations
54 __dbversion__ = 77 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
58 __url__ = 'https://code.rhodecode.com'
58 __url__ = 'https://code.rhodecode.com'
59
59
60 is_windows = __platform__ in ['Windows']
60 is_windows = __platform__ in ['Windows']
61 is_unix = not is_windows
61 is_unix = not is_windows
62 is_test = False
62 is_test = False
63 disable_error_handler = False
63 disable_error_handler = False
@@ -1,279 +1,297 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import User
23 from rhodecode.model.db import User
24 from rhodecode.model.pull_request import PullRequestModel
24 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.repo import RepoModel
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.user import UserModel
26 from rhodecode.model.user import UserModel
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCreatePullRequestApi(object):
32 class TestCreatePullRequestApi(object):
33 finalizers = []
33 finalizers = []
34
34
35 def teardown_method(self, method):
35 def teardown_method(self, method):
36 if self.finalizers:
36 if self.finalizers:
37 for finalizer in self.finalizers:
37 for finalizer in self.finalizers:
38 finalizer()
38 finalizer()
39 self.finalizers = []
39 self.finalizers = []
40
40
41 def test_create_with_wrong_data(self):
41 def test_create_with_wrong_data(self):
42 required_data = {
42 required_data = {
43 'source_repo': 'tests/source_repo',
43 'source_repo': 'tests/source_repo',
44 'target_repo': 'tests/target_repo',
44 'target_repo': 'tests/target_repo',
45 'source_ref': 'branch:default:initial',
45 'source_ref': 'branch:default:initial',
46 'target_ref': 'branch:default:new-feature',
46 'target_ref': 'branch:default:new-feature',
47 'title': 'Test PR 1'
47 'title': 'Test PR 1'
48 }
48 }
49 for key in required_data:
49 for key in required_data:
50 data = required_data.copy()
50 data = required_data.copy()
51 data.pop(key)
51 data.pop(key)
52 id_, params = build_data(
52 id_, params = build_data(
53 self.apikey, 'create_pull_request', **data)
53 self.apikey, 'create_pull_request', **data)
54 response = api_call(self.app, params)
54 response = api_call(self.app, params)
55
55
56 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
56 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
57 assert_error(id_, expected, given=response.body)
57 assert_error(id_, expected, given=response.body)
58
58
59 @pytest.mark.backends("git", "hg")
59 @pytest.mark.backends("git", "hg")
60 def test_create_with_correct_data(self, backend):
60 def test_create_with_correct_data(self, backend):
61 data = self._prepare_data(backend)
61 data = self._prepare_data(backend)
62 RepoModel().revoke_user_permission(
62 RepoModel().revoke_user_permission(
63 self.source.repo_name, User.DEFAULT_USER)
63 self.source.repo_name, User.DEFAULT_USER)
64 id_, params = build_data(
64 id_, params = build_data(
65 self.apikey_regular, 'create_pull_request', **data)
65 self.apikey_regular, 'create_pull_request', **data)
66 response = api_call(self.app, params)
66 response = api_call(self.app, params)
67 expected_message = "Created new pull request `{title}`".format(
67 expected_message = "Created new pull request `{title}`".format(
68 title=data['title'])
68 title=data['title'])
69 result = response.json
69 result = response.json
70 assert result['result']['msg'] == expected_message
70 assert result['result']['msg'] == expected_message
71 pull_request_id = result['result']['pull_request_id']
71 pull_request_id = result['result']['pull_request_id']
72 pull_request = PullRequestModel().get(pull_request_id)
72 pull_request = PullRequestModel().get(pull_request_id)
73 assert pull_request.title == data['title']
73 assert pull_request.title == data['title']
74 assert pull_request.description == data['description']
74 assert pull_request.description == data['description']
75 assert pull_request.source_ref == data['source_ref']
75 assert pull_request.source_ref == data['source_ref']
76 assert pull_request.target_ref == data['target_ref']
76 assert pull_request.target_ref == data['target_ref']
77 assert pull_request.source_repo.repo_name == data['source_repo']
77 assert pull_request.source_repo.repo_name == data['source_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
79 assert pull_request.revisions == [self.commit_ids['change']]
79 assert pull_request.revisions == [self.commit_ids['change']]
80 assert pull_request.reviewers == []
80 assert pull_request.reviewers == []
81
81
82 @pytest.mark.backends("git", "hg")
82 @pytest.mark.backends("git", "hg")
83 def test_create_with_empty_description(self, backend):
83 def test_create_with_empty_description(self, backend):
84 data = self._prepare_data(backend)
84 data = self._prepare_data(backend)
85 data.pop('description')
85 data.pop('description')
86 id_, params = build_data(
86 id_, params = build_data(
87 self.apikey_regular, 'create_pull_request', **data)
87 self.apikey_regular, 'create_pull_request', **data)
88 response = api_call(self.app, params)
88 response = api_call(self.app, params)
89 expected_message = "Created new pull request `{title}`".format(
89 expected_message = "Created new pull request `{title}`".format(
90 title=data['title'])
90 title=data['title'])
91 result = response.json
91 result = response.json
92 assert result['result']['msg'] == expected_message
92 assert result['result']['msg'] == expected_message
93 pull_request_id = result['result']['pull_request_id']
93 pull_request_id = result['result']['pull_request_id']
94 pull_request = PullRequestModel().get(pull_request_id)
94 pull_request = PullRequestModel().get(pull_request_id)
95 assert pull_request.description == ''
95 assert pull_request.description == ''
96
96
97 @pytest.mark.backends("git", "hg")
97 @pytest.mark.backends("git", "hg")
98 def test_create_with_reviewers_specified_by_names(
98 def test_create_with_reviewers_specified_by_names(
99 self, backend, no_notifications):
99 self, backend, no_notifications):
100 data = self._prepare_data(backend)
100 data = self._prepare_data(backend)
101 reviewers = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
101 reviewers = [
102 {'username': TEST_USER_REGULAR_LOGIN,
103 'reasons': ['added manually']},
104 {'username': TEST_USER_ADMIN_LOGIN,
105 'reasons': ['added manually']},
106 ]
102 data['reviewers'] = reviewers
107 data['reviewers'] = reviewers
103 id_, params = build_data(
108 id_, params = build_data(
104 self.apikey_regular, 'create_pull_request', **data)
109 self.apikey_regular, 'create_pull_request', **data)
105 response = api_call(self.app, params)
110 response = api_call(self.app, params)
106
111
107 expected_message = "Created new pull request `{title}`".format(
112 expected_message = "Created new pull request `{title}`".format(
108 title=data['title'])
113 title=data['title'])
109 result = response.json
114 result = response.json
110 assert result['result']['msg'] == expected_message
115 assert result['result']['msg'] == expected_message
111 pull_request_id = result['result']['pull_request_id']
116 pull_request_id = result['result']['pull_request_id']
112 pull_request = PullRequestModel().get(pull_request_id)
117 pull_request = PullRequestModel().get(pull_request_id)
113 actual_reviewers = [r.user.username for r in pull_request.reviewers]
118 actual_reviewers = [
119 {'username': r.user.username,
120 'reasons': ['added manually'],
121 } for r in pull_request.reviewers
122 ]
114 assert sorted(actual_reviewers) == sorted(reviewers)
123 assert sorted(actual_reviewers) == sorted(reviewers)
115
124
116 @pytest.mark.backends("git", "hg")
125 @pytest.mark.backends("git", "hg")
117 def test_create_with_reviewers_specified_by_ids(
126 def test_create_with_reviewers_specified_by_ids(
118 self, backend, no_notifications):
127 self, backend, no_notifications):
119 data = self._prepare_data(backend)
128 data = self._prepare_data(backend)
120 reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
121 reviewers = [
129 reviewers = [
122 UserModel().get_by_username(n).user_id for n in reviewer_names]
130 {'username': UserModel().get_by_username(
131 TEST_USER_REGULAR_LOGIN).user_id,
132 'reasons': ['added manually']},
133 {'username': UserModel().get_by_username(
134 TEST_USER_ADMIN_LOGIN).user_id,
135 'reasons': ['added manually']},
136 ]
137
123 data['reviewers'] = reviewers
138 data['reviewers'] = reviewers
124 id_, params = build_data(
139 id_, params = build_data(
125 self.apikey_regular, 'create_pull_request', **data)
140 self.apikey_regular, 'create_pull_request', **data)
126 response = api_call(self.app, params)
141 response = api_call(self.app, params)
127
142
128 expected_message = "Created new pull request `{title}`".format(
143 expected_message = "Created new pull request `{title}`".format(
129 title=data['title'])
144 title=data['title'])
130 result = response.json
145 result = response.json
131 assert result['result']['msg'] == expected_message
146 assert result['result']['msg'] == expected_message
132 pull_request_id = result['result']['pull_request_id']
147 pull_request_id = result['result']['pull_request_id']
133 pull_request = PullRequestModel().get(pull_request_id)
148 pull_request = PullRequestModel().get(pull_request_id)
134 actual_reviewers = [r.user.username for r in pull_request.reviewers]
149 actual_reviewers = [
135 assert sorted(actual_reviewers) == sorted(reviewer_names)
150 {'username': r.user.user_id,
151 'reasons': ['added manually'],
152 } for r in pull_request.reviewers
153 ]
154 assert sorted(actual_reviewers) == sorted(reviewers)
136
155
137 @pytest.mark.backends("git", "hg")
156 @pytest.mark.backends("git", "hg")
138 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
157 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
139 data = self._prepare_data(backend)
158 data = self._prepare_data(backend)
140 reviewers = ['somebody']
159 data['reviewers'] = [{'username': 'somebody'}]
141 data['reviewers'] = reviewers
142 id_, params = build_data(
160 id_, params = build_data(
143 self.apikey_regular, 'create_pull_request', **data)
161 self.apikey_regular, 'create_pull_request', **data)
144 response = api_call(self.app, params)
162 response = api_call(self.app, params)
145 expected_message = 'user `somebody` does not exist'
163 expected_message = 'user `somebody` does not exist'
146 assert_error(id_, expected_message, given=response.body)
164 assert_error(id_, expected_message, given=response.body)
147
165
148 @pytest.mark.backends("git", "hg")
166 @pytest.mark.backends("git", "hg")
149 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
167 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
150 data = self._prepare_data(backend)
168 data = self._prepare_data(backend)
151 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
169 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
152 data['reviewers'] = reviewers
170 data['reviewers'] = reviewers
153 id_, params = build_data(
171 id_, params = build_data(
154 self.apikey_regular, 'create_pull_request', **data)
172 self.apikey_regular, 'create_pull_request', **data)
155 response = api_call(self.app, params)
173 response = api_call(self.app, params)
156 expected_message = 'reviewers should be specified as a list'
174 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
157 assert_error(id_, expected_message, given=response.body)
175 assert_error(id_, expected_message, given=response.body)
158
176
159 @pytest.mark.backends("git", "hg")
177 @pytest.mark.backends("git", "hg")
160 def test_create_with_no_commit_hashes(self, backend):
178 def test_create_with_no_commit_hashes(self, backend):
161 data = self._prepare_data(backend)
179 data = self._prepare_data(backend)
162 expected_source_ref = data['source_ref']
180 expected_source_ref = data['source_ref']
163 expected_target_ref = data['target_ref']
181 expected_target_ref = data['target_ref']
164 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
182 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
165 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
183 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
166 id_, params = build_data(
184 id_, params = build_data(
167 self.apikey_regular, 'create_pull_request', **data)
185 self.apikey_regular, 'create_pull_request', **data)
168 response = api_call(self.app, params)
186 response = api_call(self.app, params)
169 expected_message = "Created new pull request `{title}`".format(
187 expected_message = "Created new pull request `{title}`".format(
170 title=data['title'])
188 title=data['title'])
171 result = response.json
189 result = response.json
172 assert result['result']['msg'] == expected_message
190 assert result['result']['msg'] == expected_message
173 pull_request_id = result['result']['pull_request_id']
191 pull_request_id = result['result']['pull_request_id']
174 pull_request = PullRequestModel().get(pull_request_id)
192 pull_request = PullRequestModel().get(pull_request_id)
175 assert pull_request.source_ref == expected_source_ref
193 assert pull_request.source_ref == expected_source_ref
176 assert pull_request.target_ref == expected_target_ref
194 assert pull_request.target_ref == expected_target_ref
177
195
178 @pytest.mark.backends("git", "hg")
196 @pytest.mark.backends("git", "hg")
179 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
197 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
180 def test_create_fails_with_wrong_repo(self, backend, data_key):
198 def test_create_fails_with_wrong_repo(self, backend, data_key):
181 repo_name = 'fake-repo'
199 repo_name = 'fake-repo'
182 data = self._prepare_data(backend)
200 data = self._prepare_data(backend)
183 data[data_key] = repo_name
201 data[data_key] = repo_name
184 id_, params = build_data(
202 id_, params = build_data(
185 self.apikey_regular, 'create_pull_request', **data)
203 self.apikey_regular, 'create_pull_request', **data)
186 response = api_call(self.app, params)
204 response = api_call(self.app, params)
187 expected_message = 'repository `{}` does not exist'.format(repo_name)
205 expected_message = 'repository `{}` does not exist'.format(repo_name)
188 assert_error(id_, expected_message, given=response.body)
206 assert_error(id_, expected_message, given=response.body)
189
207
190 @pytest.mark.backends("git", "hg")
208 @pytest.mark.backends("git", "hg")
191 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
209 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
192 def test_create_fails_with_non_existing_branch(self, backend, data_key):
210 def test_create_fails_with_non_existing_branch(self, backend, data_key):
193 branch_name = 'test-branch'
211 branch_name = 'test-branch'
194 data = self._prepare_data(backend)
212 data = self._prepare_data(backend)
195 data[data_key] = "branch:{}".format(branch_name)
213 data[data_key] = "branch:{}".format(branch_name)
196 id_, params = build_data(
214 id_, params = build_data(
197 self.apikey_regular, 'create_pull_request', **data)
215 self.apikey_regular, 'create_pull_request', **data)
198 response = api_call(self.app, params)
216 response = api_call(self.app, params)
199 expected_message = 'The specified branch `{}` does not exist'.format(
217 expected_message = 'The specified branch `{}` does not exist'.format(
200 branch_name)
218 branch_name)
201 assert_error(id_, expected_message, given=response.body)
219 assert_error(id_, expected_message, given=response.body)
202
220
203 @pytest.mark.backends("git", "hg")
221 @pytest.mark.backends("git", "hg")
204 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
222 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
205 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
223 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
206 data = self._prepare_data(backend)
224 data = self._prepare_data(backend)
207 ref = 'stange-ref'
225 ref = 'stange-ref'
208 data[data_key] = ref
226 data[data_key] = ref
209 id_, params = build_data(
227 id_, params = build_data(
210 self.apikey_regular, 'create_pull_request', **data)
228 self.apikey_regular, 'create_pull_request', **data)
211 response = api_call(self.app, params)
229 response = api_call(self.app, params)
212 expected_message = (
230 expected_message = (
213 'Ref `{ref}` given in a wrong format. Please check the API'
231 'Ref `{ref}` given in a wrong format. Please check the API'
214 ' documentation for more details'.format(ref=ref))
232 ' documentation for more details'.format(ref=ref))
215 assert_error(id_, expected_message, given=response.body)
233 assert_error(id_, expected_message, given=response.body)
216
234
217 @pytest.mark.backends("git", "hg")
235 @pytest.mark.backends("git", "hg")
218 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
236 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
219 def test_create_fails_with_non_existing_ref(self, backend, data_key):
237 def test_create_fails_with_non_existing_ref(self, backend, data_key):
220 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
238 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
221 ref = self._get_full_ref(backend, commit_id)
239 ref = self._get_full_ref(backend, commit_id)
222 data = self._prepare_data(backend)
240 data = self._prepare_data(backend)
223 data[data_key] = ref
241 data[data_key] = ref
224 id_, params = build_data(
242 id_, params = build_data(
225 self.apikey_regular, 'create_pull_request', **data)
243 self.apikey_regular, 'create_pull_request', **data)
226 response = api_call(self.app, params)
244 response = api_call(self.app, params)
227 expected_message = 'Ref `{}` does not exist'.format(ref)
245 expected_message = 'Ref `{}` does not exist'.format(ref)
228 assert_error(id_, expected_message, given=response.body)
246 assert_error(id_, expected_message, given=response.body)
229
247
230 @pytest.mark.backends("git", "hg")
248 @pytest.mark.backends("git", "hg")
231 def test_create_fails_when_no_revisions(self, backend):
249 def test_create_fails_when_no_revisions(self, backend):
232 data = self._prepare_data(backend, source_head='initial')
250 data = self._prepare_data(backend, source_head='initial')
233 id_, params = build_data(
251 id_, params = build_data(
234 self.apikey_regular, 'create_pull_request', **data)
252 self.apikey_regular, 'create_pull_request', **data)
235 response = api_call(self.app, params)
253 response = api_call(self.app, params)
236 expected_message = 'no commits found'
254 expected_message = 'no commits found'
237 assert_error(id_, expected_message, given=response.body)
255 assert_error(id_, expected_message, given=response.body)
238
256
239 @pytest.mark.backends("git", "hg")
257 @pytest.mark.backends("git", "hg")
240 def test_create_fails_when_no_permissions(self, backend):
258 def test_create_fails_when_no_permissions(self, backend):
241 data = self._prepare_data(backend)
259 data = self._prepare_data(backend)
242 RepoModel().revoke_user_permission(
260 RepoModel().revoke_user_permission(
243 self.source.repo_name, User.DEFAULT_USER)
261 self.source.repo_name, User.DEFAULT_USER)
244 RepoModel().revoke_user_permission(
262 RepoModel().revoke_user_permission(
245 self.source.repo_name, self.test_user)
263 self.source.repo_name, self.test_user)
246 id_, params = build_data(
264 id_, params = build_data(
247 self.apikey_regular, 'create_pull_request', **data)
265 self.apikey_regular, 'create_pull_request', **data)
248 response = api_call(self.app, params)
266 response = api_call(self.app, params)
249 expected_message = 'repository `{}` does not exist'.format(
267 expected_message = 'repository `{}` does not exist'.format(
250 self.source.repo_name)
268 self.source.repo_name)
251 assert_error(id_, expected_message, given=response.body)
269 assert_error(id_, expected_message, given=response.body)
252
270
253 def _prepare_data(
271 def _prepare_data(
254 self, backend, source_head='change', target_head='initial'):
272 self, backend, source_head='change', target_head='initial'):
255 commits = [
273 commits = [
256 {'message': 'initial'},
274 {'message': 'initial'},
257 {'message': 'change'},
275 {'message': 'change'},
258 {'message': 'new-feature', 'parents': ['initial']},
276 {'message': 'new-feature', 'parents': ['initial']},
259 ]
277 ]
260 self.commit_ids = backend.create_master_repo(commits)
278 self.commit_ids = backend.create_master_repo(commits)
261 self.source = backend.create_repo(heads=[source_head])
279 self.source = backend.create_repo(heads=[source_head])
262 self.target = backend.create_repo(heads=[target_head])
280 self.target = backend.create_repo(heads=[target_head])
263 data = {
281 data = {
264 'source_repo': self.source.repo_name,
282 'source_repo': self.source.repo_name,
265 'target_repo': self.target.repo_name,
283 'target_repo': self.target.repo_name,
266 'source_ref': self._get_full_ref(
284 'source_ref': self._get_full_ref(
267 backend, self.commit_ids[source_head]),
285 backend, self.commit_ids[source_head]),
268 'target_ref': self._get_full_ref(
286 'target_ref': self._get_full_ref(
269 backend, self.commit_ids[target_head]),
287 backend, self.commit_ids[target_head]),
270 'title': 'Test PR 1',
288 'title': 'Test PR 1',
271 'description': 'Test'
289 'description': 'Test'
272 }
290 }
273 RepoModel().grant_user_permission(
291 RepoModel().grant_user_permission(
274 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
292 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
275 return data
293 return data
276
294
277 def _get_full_ref(self, backend, commit_id):
295 def _get_full_ref(self, backend, commit_id):
278 return 'branch:{branch}:{commit_id}'.format(
296 return 'branch:{branch}:{commit_id}'.format(
279 branch=backend.default_branch_name, commit_id=commit_id)
297 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,133 +1,134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24 import urlobject
24 import urlobject
25 from pylons import url
25 from pylons import url
26
26
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_ok)
28 build_data, api_call, assert_error, assert_ok)
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
32
32
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
34 class TestGetPullRequest(object):
35
35
36 def test_api_get_pull_request(self, pr_util):
36 def test_api_get_pull_request(self, pr_util):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 pull_request = pr_util.create_pull_request(mergeable=True)
38 pull_request = pr_util.create_pull_request(mergeable=True)
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'get_pull_request',
40 self.apikey, 'get_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 assert response.status == '200 OK'
46 assert response.status == '200 OK'
47
47
48 url_obj = urlobject.URLObject(
48 url_obj = urlobject.URLObject(
49 url(
49 url(
50 'pullrequest_show',
50 'pullrequest_show',
51 repo_name=pull_request.target_repo.repo_name,
51 repo_name=pull_request.target_repo.repo_name,
52 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pull_request_id=pull_request.pull_request_id, qualified=True))
53 pr_url = unicode(
53 pr_url = unicode(
54 url_obj.with_netloc('test.example.com:80'))
54 url_obj.with_netloc('test.example.com:80'))
55 source_url = unicode(
55 source_url = unicode(
56 pull_request.source_repo.clone_url()
56 pull_request.source_repo.clone_url()
57 .with_netloc('test.example.com:80'))
57 .with_netloc('test.example.com:80'))
58 target_url = unicode(
58 target_url = unicode(
59 pull_request.target_repo.clone_url()
59 pull_request.target_repo.clone_url()
60 .with_netloc('test.example.com:80'))
60 .with_netloc('test.example.com:80'))
61 shadow_url = unicode(
61 shadow_url = unicode(
62 PullRequestModel().get_shadow_clone_url(pull_request))
62 PullRequestModel().get_shadow_clone_url(pull_request))
63 expected = {
63 expected = {
64 'pull_request_id': pull_request.pull_request_id,
64 'pull_request_id': pull_request.pull_request_id,
65 'url': pr_url,
65 'url': pr_url,
66 'title': pull_request.title,
66 'title': pull_request.title,
67 'description': pull_request.description,
67 'description': pull_request.description,
68 'status': pull_request.status,
68 'status': pull_request.status,
69 'created_on': pull_request.created_on,
69 'created_on': pull_request.created_on,
70 'updated_on': pull_request.updated_on,
70 'updated_on': pull_request.updated_on,
71 'commit_ids': pull_request.revisions,
71 'commit_ids': pull_request.revisions,
72 'review_status': pull_request.calculated_review_status(),
72 'review_status': pull_request.calculated_review_status(),
73 'mergeable': {
73 'mergeable': {
74 'status': True,
74 'status': True,
75 'message': 'This pull request can be automatically merged.',
75 'message': 'This pull request can be automatically merged.',
76 },
76 },
77 'source': {
77 'source': {
78 'clone_url': source_url,
78 'clone_url': source_url,
79 'repository': pull_request.source_repo.repo_name,
79 'repository': pull_request.source_repo.repo_name,
80 'reference': {
80 'reference': {
81 'name': pull_request.source_ref_parts.name,
81 'name': pull_request.source_ref_parts.name,
82 'type': pull_request.source_ref_parts.type,
82 'type': pull_request.source_ref_parts.type,
83 'commit_id': pull_request.source_ref_parts.commit_id,
83 'commit_id': pull_request.source_ref_parts.commit_id,
84 },
84 },
85 },
85 },
86 'target': {
86 'target': {
87 'clone_url': target_url,
87 'clone_url': target_url,
88 'repository': pull_request.target_repo.repo_name,
88 'repository': pull_request.target_repo.repo_name,
89 'reference': {
89 'reference': {
90 'name': pull_request.target_ref_parts.name,
90 'name': pull_request.target_ref_parts.name,
91 'type': pull_request.target_ref_parts.type,
91 'type': pull_request.target_ref_parts.type,
92 'commit_id': pull_request.target_ref_parts.commit_id,
92 'commit_id': pull_request.target_ref_parts.commit_id,
93 },
93 },
94 },
94 },
95 'merge': {
95 'merge': {
96 'clone_url': shadow_url,
96 'clone_url': shadow_url,
97 'reference': {
97 'reference': {
98 'name': pull_request.shadow_merge_ref.name,
98 'name': pull_request.shadow_merge_ref.name,
99 'type': pull_request.shadow_merge_ref.type,
99 'type': pull_request.shadow_merge_ref.type,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 },
101 },
102 },
102 },
103 'author': pull_request.author.get_api_data(include_secrets=False,
103 'author': pull_request.author.get_api_data(include_secrets=False,
104 details='basic'),
104 details='basic'),
105 'reviewers': [
105 'reviewers': [
106 {
106 {
107 'user': reviewer.get_api_data(include_secrets=False,
107 'user': reviewer.get_api_data(include_secrets=False,
108 details='basic'),
108 details='basic'),
109 'reasons': reasons,
109 'reasons': reasons,
110 'review_status': st[0][1].status if st else 'not_reviewed',
110 'review_status': st[0][1].status if st else 'not_reviewed',
111 }
111 }
112 for reviewer, reasons, st in pull_request.reviewers_statuses()
112 for reviewer, reasons, mandatory, st in
113 pull_request.reviewers_statuses()
113 ]
114 ]
114 }
115 }
115 assert_ok(id_, expected, response.body)
116 assert_ok(id_, expected, response.body)
116
117
117 def test_api_get_pull_request_repo_error(self):
118 def test_api_get_pull_request_repo_error(self):
118 id_, params = build_data(
119 id_, params = build_data(
119 self.apikey, 'get_pull_request',
120 self.apikey, 'get_pull_request',
120 repoid=666, pullrequestid=1)
121 repoid=666, pullrequestid=1)
121 response = api_call(self.app, params)
122 response = api_call(self.app, params)
122
123
123 expected = 'repository `666` does not exist'
124 expected = 'repository `666` does not exist'
124 assert_error(id_, expected, given=response.body)
125 assert_error(id_, expected, given=response.body)
125
126
126 def test_api_get_pull_request_pull_request_error(self):
127 def test_api_get_pull_request_pull_request_error(self):
127 id_, params = build_data(
128 id_, params = build_data(
128 self.apikey, 'get_pull_request',
129 self.apikey, 'get_pull_request',
129 repoid=1, pullrequestid=666)
130 repoid=1, pullrequestid=666)
130 response = api_call(self.app, params)
131 response = api_call(self.app, params)
131
132
132 expected = 'pull request `666` does not exist'
133 expected = 'pull request `666` does not exist'
133 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
@@ -1,205 +1,213 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.vcs.nodes import FileNode
23 from rhodecode.lib.vcs.nodes import FileNode
24 from rhodecode.model.db import User
24 from rhodecode.model.db import User
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_ok, assert_error)
28 build_data, api_call, assert_ok, assert_error)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestUpdatePullRequest(object):
32 class TestUpdatePullRequest(object):
33
33
34 @pytest.mark.backends("git", "hg")
34 @pytest.mark.backends("git", "hg")
35 def test_api_update_pull_request_title_or_description(
35 def test_api_update_pull_request_title_or_description(
36 self, pr_util, silence_action_logger, no_notifications):
36 self, pr_util, silence_action_logger, no_notifications):
37 pull_request = pr_util.create_pull_request()
37 pull_request = pr_util.create_pull_request()
38
38
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'update_pull_request',
40 self.apikey, 'update_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id,
42 pullrequestid=pull_request.pull_request_id,
43 title='New TITLE OF A PR',
43 title='New TITLE OF A PR',
44 description='New DESC OF A PR',
44 description='New DESC OF A PR',
45 )
45 )
46 response = api_call(self.app, params)
46 response = api_call(self.app, params)
47
47
48 expected = {
48 expected = {
49 "msg": "Updated pull request `{}`".format(
49 "msg": "Updated pull request `{}`".format(
50 pull_request.pull_request_id),
50 pull_request.pull_request_id),
51 "pull_request": response.json['result']['pull_request'],
51 "pull_request": response.json['result']['pull_request'],
52 "updated_commits": {"added": [], "common": [], "removed": []},
52 "updated_commits": {"added": [], "common": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
54 }
54 }
55
55
56 response_json = response.json['result']
56 response_json = response.json['result']
57 assert response_json == expected
57 assert response_json == expected
58 pr = response_json['pull_request']
58 pr = response_json['pull_request']
59 assert pr['title'] == 'New TITLE OF A PR'
59 assert pr['title'] == 'New TITLE OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
61
61
62 @pytest.mark.backends("git", "hg")
62 @pytest.mark.backends("git", "hg")
63 def test_api_try_update_closed_pull_request(
63 def test_api_try_update_closed_pull_request(
64 self, pr_util, silence_action_logger, no_notifications):
64 self, pr_util, silence_action_logger, no_notifications):
65 pull_request = pr_util.create_pull_request()
65 pull_request = pr_util.create_pull_request()
66 PullRequestModel().close_pull_request(
66 PullRequestModel().close_pull_request(
67 pull_request, TEST_USER_ADMIN_LOGIN)
67 pull_request, TEST_USER_ADMIN_LOGIN)
68
68
69 id_, params = build_data(
69 id_, params = build_data(
70 self.apikey, 'update_pull_request',
70 self.apikey, 'update_pull_request',
71 repoid=pull_request.target_repo.repo_name,
71 repoid=pull_request.target_repo.repo_name,
72 pullrequestid=pull_request.pull_request_id)
72 pullrequestid=pull_request.pull_request_id)
73 response = api_call(self.app, params)
73 response = api_call(self.app, params)
74
74
75 expected = 'pull request `{}` update failed, pull request ' \
75 expected = 'pull request `{}` update failed, pull request ' \
76 'is closed'.format(pull_request.pull_request_id)
76 'is closed'.format(pull_request.pull_request_id)
77
77
78 assert_error(id_, expected, response.body)
78 assert_error(id_, expected, response.body)
79
79
80 @pytest.mark.backends("git", "hg")
80 @pytest.mark.backends("git", "hg")
81 def test_api_update_update_commits(
81 def test_api_update_update_commits(
82 self, pr_util, silence_action_logger, no_notifications):
82 self, pr_util, silence_action_logger, no_notifications):
83 commits = [
83 commits = [
84 {'message': 'a'},
84 {'message': 'a'},
85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
86 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
86 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
87 ]
87 ]
88 pull_request = pr_util.create_pull_request(
88 pull_request = pr_util.create_pull_request(
89 commits=commits, target_head='a', source_head='b', revisions=['b'])
89 commits=commits, target_head='a', source_head='b', revisions=['b'])
90 pr_util.update_source_repository(head='c')
90 pr_util.update_source_repository(head='c')
91 repo = pull_request.source_repo.scm_instance()
91 repo = pull_request.source_repo.scm_instance()
92 commits = [x for x in repo.get_commits()]
92 commits = [x for x in repo.get_commits()]
93 print commits
93 print commits
94
94
95 added_commit_id = commits[-1].raw_id # c commit
95 added_commit_id = commits[-1].raw_id # c commit
96 common_commit_id = commits[1].raw_id # b commit is common ancestor
96 common_commit_id = commits[1].raw_id # b commit is common ancestor
97 total_commits = [added_commit_id, common_commit_id]
97 total_commits = [added_commit_id, common_commit_id]
98
98
99 id_, params = build_data(
99 id_, params = build_data(
100 self.apikey, 'update_pull_request',
100 self.apikey, 'update_pull_request',
101 repoid=pull_request.target_repo.repo_name,
101 repoid=pull_request.target_repo.repo_name,
102 pullrequestid=pull_request.pull_request_id,
102 pullrequestid=pull_request.pull_request_id,
103 update_commits=True
103 update_commits=True
104 )
104 )
105 response = api_call(self.app, params)
105 response = api_call(self.app, params)
106
106
107 expected = {
107 expected = {
108 "msg": "Updated pull request `{}`".format(
108 "msg": "Updated pull request `{}`".format(
109 pull_request.pull_request_id),
109 pull_request.pull_request_id),
110 "pull_request": response.json['result']['pull_request'],
110 "pull_request": response.json['result']['pull_request'],
111 "updated_commits": {"added": [added_commit_id],
111 "updated_commits": {"added": [added_commit_id],
112 "common": [common_commit_id],
112 "common": [common_commit_id],
113 "total": total_commits,
113 "total": total_commits,
114 "removed": []},
114 "removed": []},
115 "updated_reviewers": {"added": [], "removed": []},
115 "updated_reviewers": {"added": [], "removed": []},
116 }
116 }
117
117
118 assert_ok(id_, expected, response.body)
118 assert_ok(id_, expected, response.body)
119
119
120 @pytest.mark.backends("git", "hg")
120 @pytest.mark.backends("git", "hg")
121 def test_api_update_change_reviewers(
121 def test_api_update_change_reviewers(
122 self, pr_util, silence_action_logger, no_notifications):
122 self, user_util, pr_util, silence_action_logger, no_notifications):
123 a = user_util.create_user()
124 b = user_util.create_user()
125 c = user_util.create_user()
126 new_reviewers = [
127 {'username': b.username,'reasons': ['updated via API'],
128 'mandatory':False},
129 {'username': c.username, 'reasons': ['updated via API'],
130 'mandatory':False},
131 ]
123
132
124 users = [x.username for x in User.get_all()]
133 added = [b.username, c.username]
125 new = [users.pop(0)]
134 removed = [a.username]
126 removed = sorted(new)
127 added = sorted(users)
128
135
129 pull_request = pr_util.create_pull_request(reviewers=new)
136 pull_request = pr_util.create_pull_request(
137 reviewers=[(a.username, ['added via API'], False)])
130
138
131 id_, params = build_data(
139 id_, params = build_data(
132 self.apikey, 'update_pull_request',
140 self.apikey, 'update_pull_request',
133 repoid=pull_request.target_repo.repo_name,
141 repoid=pull_request.target_repo.repo_name,
134 pullrequestid=pull_request.pull_request_id,
142 pullrequestid=pull_request.pull_request_id,
135 reviewers=added)
143 reviewers=new_reviewers)
136 response = api_call(self.app, params)
144 response = api_call(self.app, params)
137 expected = {
145 expected = {
138 "msg": "Updated pull request `{}`".format(
146 "msg": "Updated pull request `{}`".format(
139 pull_request.pull_request_id),
147 pull_request.pull_request_id),
140 "pull_request": response.json['result']['pull_request'],
148 "pull_request": response.json['result']['pull_request'],
141 "updated_commits": {"added": [], "common": [], "removed": []},
149 "updated_commits": {"added": [], "common": [], "removed": []},
142 "updated_reviewers": {"added": added, "removed": removed},
150 "updated_reviewers": {"added": added, "removed": removed},
143 }
151 }
144
152
145 assert_ok(id_, expected, response.body)
153 assert_ok(id_, expected, response.body)
146
154
147 @pytest.mark.backends("git", "hg")
155 @pytest.mark.backends("git", "hg")
148 def test_api_update_bad_user_in_reviewers(self, pr_util):
156 def test_api_update_bad_user_in_reviewers(self, pr_util):
149 pull_request = pr_util.create_pull_request()
157 pull_request = pr_util.create_pull_request()
150
158
151 id_, params = build_data(
159 id_, params = build_data(
152 self.apikey, 'update_pull_request',
160 self.apikey, 'update_pull_request',
153 repoid=pull_request.target_repo.repo_name,
161 repoid=pull_request.target_repo.repo_name,
154 pullrequestid=pull_request.pull_request_id,
162 pullrequestid=pull_request.pull_request_id,
155 reviewers=['bad_name'])
163 reviewers=[{'username': 'bad_name'}])
156 response = api_call(self.app, params)
164 response = api_call(self.app, params)
157
165
158 expected = 'user `bad_name` does not exist'
166 expected = 'user `bad_name` does not exist'
159
167
160 assert_error(id_, expected, response.body)
168 assert_error(id_, expected, response.body)
161
169
162 @pytest.mark.backends("git", "hg")
170 @pytest.mark.backends("git", "hg")
163 def test_api_update_repo_error(self, pr_util):
171 def test_api_update_repo_error(self, pr_util):
164 id_, params = build_data(
172 id_, params = build_data(
165 self.apikey, 'update_pull_request',
173 self.apikey, 'update_pull_request',
166 repoid='fake',
174 repoid='fake',
167 pullrequestid='fake',
175 pullrequestid='fake',
168 reviewers=['bad_name'])
176 reviewers=[{'username': 'bad_name'}])
169 response = api_call(self.app, params)
177 response = api_call(self.app, params)
170
178
171 expected = 'repository `fake` does not exist'
179 expected = 'repository `fake` does not exist'
172
180
173 response_json = response.json['error']
181 response_json = response.json['error']
174 assert response_json == expected
182 assert response_json == expected
175
183
176 @pytest.mark.backends("git", "hg")
184 @pytest.mark.backends("git", "hg")
177 def test_api_update_pull_request_error(self, pr_util):
185 def test_api_update_pull_request_error(self, pr_util):
178 pull_request = pr_util.create_pull_request()
186 pull_request = pr_util.create_pull_request()
179
187
180 id_, params = build_data(
188 id_, params = build_data(
181 self.apikey, 'update_pull_request',
189 self.apikey, 'update_pull_request',
182 repoid=pull_request.target_repo.repo_name,
190 repoid=pull_request.target_repo.repo_name,
183 pullrequestid=999999,
191 pullrequestid=999999,
184 reviewers=['bad_name'])
192 reviewers=[{'username': 'bad_name'}])
185 response = api_call(self.app, params)
193 response = api_call(self.app, params)
186
194
187 expected = 'pull request `999999` does not exist'
195 expected = 'pull request `999999` does not exist'
188 assert_error(id_, expected, response.body)
196 assert_error(id_, expected, response.body)
189
197
190 @pytest.mark.backends("git", "hg")
198 @pytest.mark.backends("git", "hg")
191 def test_api_update_pull_request_no_perms_to_update(
199 def test_api_update_pull_request_no_perms_to_update(
192 self, user_util, pr_util):
200 self, user_util, pr_util):
193 user = user_util.create_user()
201 user = user_util.create_user()
194 pull_request = pr_util.create_pull_request()
202 pull_request = pr_util.create_pull_request()
195
203
196 id_, params = build_data(
204 id_, params = build_data(
197 user.api_key, 'update_pull_request',
205 user.api_key, 'update_pull_request',
198 repoid=pull_request.target_repo.repo_name,
206 repoid=pull_request.target_repo.repo_name,
199 pullrequestid=pull_request.pull_request_id,)
207 pullrequestid=pull_request.pull_request_id,)
200 response = api_call(self.app, params)
208 response = api_call(self.app, params)
201
209
202 expected = ('pull request `%s` update failed, '
210 expected = ('pull request `%s` update failed, '
203 'no permission to update.') % pull_request.pull_request_id
211 'no permission to update.') % pull_request.pull_request_id
204
212
205 assert_error(id_, expected, response.body)
213 assert_error(id_, expected, response.body)
@@ -1,730 +1,734 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error)
28 validate_repo_permissions, resolve_ref_or_error)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import \
39 ReviewerListSchema
37
40
38 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
39
42
40
43
41 @jsonrpc_method()
44 @jsonrpc_method()
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
45 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 """
46 """
44 Get a pull request based on the given ID.
47 Get a pull request based on the given ID.
45
48
46 :param apiuser: This is filled automatically from the |authtoken|.
49 :param apiuser: This is filled automatically from the |authtoken|.
47 :type apiuser: AuthUser
50 :type apiuser: AuthUser
48 :param repoid: Repository name or repository ID from where the pull
51 :param repoid: Repository name or repository ID from where the pull
49 request was opened.
52 request was opened.
50 :type repoid: str or int
53 :type repoid: str or int
51 :param pullrequestid: ID of the requested pull request.
54 :param pullrequestid: ID of the requested pull request.
52 :type pullrequestid: int
55 :type pullrequestid: int
53
56
54 Example output:
57 Example output:
55
58
56 .. code-block:: bash
59 .. code-block:: bash
57
60
58 "id": <id_given_in_input>,
61 "id": <id_given_in_input>,
59 "result":
62 "result":
60 {
63 {
61 "pull_request_id": "<pull_request_id>",
64 "pull_request_id": "<pull_request_id>",
62 "url": "<url>",
65 "url": "<url>",
63 "title": "<title>",
66 "title": "<title>",
64 "description": "<description>",
67 "description": "<description>",
65 "status" : "<status>",
68 "status" : "<status>",
66 "created_on": "<date_time_created>",
69 "created_on": "<date_time_created>",
67 "updated_on": "<date_time_updated>",
70 "updated_on": "<date_time_updated>",
68 "commit_ids": [
71 "commit_ids": [
69 ...
72 ...
70 "<commit_id>",
73 "<commit_id>",
71 "<commit_id>",
74 "<commit_id>",
72 ...
75 ...
73 ],
76 ],
74 "review_status": "<review_status>",
77 "review_status": "<review_status>",
75 "mergeable": {
78 "mergeable": {
76 "status": "<bool>",
79 "status": "<bool>",
77 "message": "<message>",
80 "message": "<message>",
78 },
81 },
79 "source": {
82 "source": {
80 "clone_url": "<clone_url>",
83 "clone_url": "<clone_url>",
81 "repository": "<repository_name>",
84 "repository": "<repository_name>",
82 "reference":
85 "reference":
83 {
86 {
84 "name": "<name>",
87 "name": "<name>",
85 "type": "<type>",
88 "type": "<type>",
86 "commit_id": "<commit_id>",
89 "commit_id": "<commit_id>",
87 }
90 }
88 },
91 },
89 "target": {
92 "target": {
90 "clone_url": "<clone_url>",
93 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
94 "repository": "<repository_name>",
92 "reference":
95 "reference":
93 {
96 {
94 "name": "<name>",
97 "name": "<name>",
95 "type": "<type>",
98 "type": "<type>",
96 "commit_id": "<commit_id>",
99 "commit_id": "<commit_id>",
97 }
100 }
98 },
101 },
99 "merge": {
102 "merge": {
100 "clone_url": "<clone_url>",
103 "clone_url": "<clone_url>",
101 "reference":
104 "reference":
102 {
105 {
103 "name": "<name>",
106 "name": "<name>",
104 "type": "<type>",
107 "type": "<type>",
105 "commit_id": "<commit_id>",
108 "commit_id": "<commit_id>",
106 }
109 }
107 },
110 },
108 "author": <user_obj>,
111 "author": <user_obj>,
109 "reviewers": [
112 "reviewers": [
110 ...
113 ...
111 {
114 {
112 "user": "<user_obj>",
115 "user": "<user_obj>",
113 "review_status": "<review_status>",
116 "review_status": "<review_status>",
114 }
117 }
115 ...
118 ...
116 ]
119 ]
117 },
120 },
118 "error": null
121 "error": null
119 """
122 """
120 get_repo_or_error(repoid)
123 get_repo_or_error(repoid)
121 pull_request = get_pull_request_or_error(pullrequestid)
124 pull_request = get_pull_request_or_error(pullrequestid)
122 if not PullRequestModel().check_user_read(
125 if not PullRequestModel().check_user_read(
123 pull_request, apiuser, api=True):
126 pull_request, apiuser, api=True):
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
127 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
125 data = pull_request.get_api_data()
128 data = pull_request.get_api_data()
126 return data
129 return data
127
130
128
131
129 @jsonrpc_method()
132 @jsonrpc_method()
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
133 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
131 """
134 """
132 Get all pull requests from the repository specified in `repoid`.
135 Get all pull requests from the repository specified in `repoid`.
133
136
134 :param apiuser: This is filled automatically from the |authtoken|.
137 :param apiuser: This is filled automatically from the |authtoken|.
135 :type apiuser: AuthUser
138 :type apiuser: AuthUser
136 :param repoid: Repository name or repository ID.
139 :param repoid: Repository name or repository ID.
137 :type repoid: str or int
140 :type repoid: str or int
138 :param status: Only return pull requests with the specified status.
141 :param status: Only return pull requests with the specified status.
139 Valid options are.
142 Valid options are.
140 * ``new`` (default)
143 * ``new`` (default)
141 * ``open``
144 * ``open``
142 * ``closed``
145 * ``closed``
143 :type status: str
146 :type status: str
144
147
145 Example output:
148 Example output:
146
149
147 .. code-block:: bash
150 .. code-block:: bash
148
151
149 "id": <id_given_in_input>,
152 "id": <id_given_in_input>,
150 "result":
153 "result":
151 [
154 [
152 ...
155 ...
153 {
156 {
154 "pull_request_id": "<pull_request_id>",
157 "pull_request_id": "<pull_request_id>",
155 "url": "<url>",
158 "url": "<url>",
156 "title" : "<title>",
159 "title" : "<title>",
157 "description": "<description>",
160 "description": "<description>",
158 "status": "<status>",
161 "status": "<status>",
159 "created_on": "<date_time_created>",
162 "created_on": "<date_time_created>",
160 "updated_on": "<date_time_updated>",
163 "updated_on": "<date_time_updated>",
161 "commit_ids": [
164 "commit_ids": [
162 ...
165 ...
163 "<commit_id>",
166 "<commit_id>",
164 "<commit_id>",
167 "<commit_id>",
165 ...
168 ...
166 ],
169 ],
167 "review_status": "<review_status>",
170 "review_status": "<review_status>",
168 "mergeable": {
171 "mergeable": {
169 "status": "<bool>",
172 "status": "<bool>",
170 "message: "<message>",
173 "message: "<message>",
171 },
174 },
172 "source": {
175 "source": {
173 "clone_url": "<clone_url>",
176 "clone_url": "<clone_url>",
174 "reference":
177 "reference":
175 {
178 {
176 "name": "<name>",
179 "name": "<name>",
177 "type": "<type>",
180 "type": "<type>",
178 "commit_id": "<commit_id>",
181 "commit_id": "<commit_id>",
179 }
182 }
180 },
183 },
181 "target": {
184 "target": {
182 "clone_url": "<clone_url>",
185 "clone_url": "<clone_url>",
183 "reference":
186 "reference":
184 {
187 {
185 "name": "<name>",
188 "name": "<name>",
186 "type": "<type>",
189 "type": "<type>",
187 "commit_id": "<commit_id>",
190 "commit_id": "<commit_id>",
188 }
191 }
189 },
192 },
190 "merge": {
193 "merge": {
191 "clone_url": "<clone_url>",
194 "clone_url": "<clone_url>",
192 "reference":
195 "reference":
193 {
196 {
194 "name": "<name>",
197 "name": "<name>",
195 "type": "<type>",
198 "type": "<type>",
196 "commit_id": "<commit_id>",
199 "commit_id": "<commit_id>",
197 }
200 }
198 },
201 },
199 "author": <user_obj>,
202 "author": <user_obj>,
200 "reviewers": [
203 "reviewers": [
201 ...
204 ...
202 {
205 {
203 "user": "<user_obj>",
206 "user": "<user_obj>",
204 "review_status": "<review_status>",
207 "review_status": "<review_status>",
205 }
208 }
206 ...
209 ...
207 ]
210 ]
208 }
211 }
209 ...
212 ...
210 ],
213 ],
211 "error": null
214 "error": null
212
215
213 """
216 """
214 repo = get_repo_or_error(repoid)
217 repo = get_repo_or_error(repoid)
215 if not has_superadmin_permission(apiuser):
218 if not has_superadmin_permission(apiuser):
216 _perms = (
219 _perms = (
217 'repository.admin', 'repository.write', 'repository.read',)
220 'repository.admin', 'repository.write', 'repository.read',)
218 validate_repo_permissions(apiuser, repoid, repo, _perms)
221 validate_repo_permissions(apiuser, repoid, repo, _perms)
219
222
220 status = Optional.extract(status)
223 status = Optional.extract(status)
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
224 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
222 data = [pr.get_api_data() for pr in pull_requests]
225 data = [pr.get_api_data() for pr in pull_requests]
223 return data
226 return data
224
227
225
228
226 @jsonrpc_method()
229 @jsonrpc_method()
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
230 def merge_pull_request(request, apiuser, repoid, pullrequestid,
228 userid=Optional(OAttr('apiuser'))):
231 userid=Optional(OAttr('apiuser'))):
229 """
232 """
230 Merge the pull request specified by `pullrequestid` into its target
233 Merge the pull request specified by `pullrequestid` into its target
231 repository.
234 repository.
232
235
233 :param apiuser: This is filled automatically from the |authtoken|.
236 :param apiuser: This is filled automatically from the |authtoken|.
234 :type apiuser: AuthUser
237 :type apiuser: AuthUser
235 :param repoid: The Repository name or repository ID of the
238 :param repoid: The Repository name or repository ID of the
236 target repository to which the |pr| is to be merged.
239 target repository to which the |pr| is to be merged.
237 :type repoid: str or int
240 :type repoid: str or int
238 :param pullrequestid: ID of the pull request which shall be merged.
241 :param pullrequestid: ID of the pull request which shall be merged.
239 :type pullrequestid: int
242 :type pullrequestid: int
240 :param userid: Merge the pull request as this user.
243 :param userid: Merge the pull request as this user.
241 :type userid: Optional(str or int)
244 :type userid: Optional(str or int)
242
245
243 Example output:
246 Example output:
244
247
245 .. code-block:: bash
248 .. code-block:: bash
246
249
247 "id": <id_given_in_input>,
250 "id": <id_given_in_input>,
248 "result": {
251 "result": {
249 "executed": "<bool>",
252 "executed": "<bool>",
250 "failure_reason": "<int>",
253 "failure_reason": "<int>",
251 "merge_commit_id": "<merge_commit_id>",
254 "merge_commit_id": "<merge_commit_id>",
252 "possible": "<bool>",
255 "possible": "<bool>",
253 "merge_ref": {
256 "merge_ref": {
254 "commit_id": "<commit_id>",
257 "commit_id": "<commit_id>",
255 "type": "<type>",
258 "type": "<type>",
256 "name": "<name>"
259 "name": "<name>"
257 }
260 }
258 },
261 },
259 "error": null
262 "error": null
260 """
263 """
261 repo = get_repo_or_error(repoid)
264 repo = get_repo_or_error(repoid)
262 if not isinstance(userid, Optional):
265 if not isinstance(userid, Optional):
263 if (has_superadmin_permission(apiuser) or
266 if (has_superadmin_permission(apiuser) or
264 HasRepoPermissionAnyApi('repository.admin')(
267 HasRepoPermissionAnyApi('repository.admin')(
265 user=apiuser, repo_name=repo.repo_name)):
268 user=apiuser, repo_name=repo.repo_name)):
266 apiuser = get_user_or_error(userid)
269 apiuser = get_user_or_error(userid)
267 else:
270 else:
268 raise JSONRPCError('userid is not the same as your user')
271 raise JSONRPCError('userid is not the same as your user')
269
272
270 pull_request = get_pull_request_or_error(pullrequestid)
273 pull_request = get_pull_request_or_error(pullrequestid)
271
274
272 check = MergeCheck.validate(pull_request, user=apiuser)
275 check = MergeCheck.validate(pull_request, user=apiuser)
273 merge_possible = not check.failed
276 merge_possible = not check.failed
274
277
275 if not merge_possible:
278 if not merge_possible:
276 error_messages = []
279 error_messages = []
277 for err_type, error_msg in check.errors:
280 for err_type, error_msg in check.errors:
278 error_msg = request.translate(error_msg)
281 error_msg = request.translate(error_msg)
279 error_messages.append(error_msg)
282 error_messages.append(error_msg)
280
283
281 reasons = ','.join(error_messages)
284 reasons = ','.join(error_messages)
282 raise JSONRPCError(
285 raise JSONRPCError(
283 'merge not possible for following reasons: {}'.format(reasons))
286 'merge not possible for following reasons: {}'.format(reasons))
284
287
285 target_repo = pull_request.target_repo
288 target_repo = pull_request.target_repo
286 extras = vcs_operation_context(
289 extras = vcs_operation_context(
287 request.environ, repo_name=target_repo.repo_name,
290 request.environ, repo_name=target_repo.repo_name,
288 username=apiuser.username, action='push',
291 username=apiuser.username, action='push',
289 scm=target_repo.repo_type)
292 scm=target_repo.repo_type)
290 merge_response = PullRequestModel().merge(
293 merge_response = PullRequestModel().merge(
291 pull_request, apiuser, extras=extras)
294 pull_request, apiuser, extras=extras)
292 if merge_response.executed:
295 if merge_response.executed:
293 PullRequestModel().close_pull_request(
296 PullRequestModel().close_pull_request(
294 pull_request.pull_request_id, apiuser)
297 pull_request.pull_request_id, apiuser)
295
298
296 Session().commit()
299 Session().commit()
297
300
298 # In previous versions the merge response directly contained the merge
301 # In previous versions the merge response directly contained the merge
299 # commit id. It is now contained in the merge reference object. To be
302 # commit id. It is now contained in the merge reference object. To be
300 # backwards compatible we have to extract it again.
303 # backwards compatible we have to extract it again.
301 merge_response = merge_response._asdict()
304 merge_response = merge_response._asdict()
302 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
305 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
303
306
304 return merge_response
307 return merge_response
305
308
306
309
307 @jsonrpc_method()
310 @jsonrpc_method()
308 def close_pull_request(request, apiuser, repoid, pullrequestid,
311 def close_pull_request(request, apiuser, repoid, pullrequestid,
309 userid=Optional(OAttr('apiuser'))):
312 userid=Optional(OAttr('apiuser'))):
310 """
313 """
311 Close the pull request specified by `pullrequestid`.
314 Close the pull request specified by `pullrequestid`.
312
315
313 :param apiuser: This is filled automatically from the |authtoken|.
316 :param apiuser: This is filled automatically from the |authtoken|.
314 :type apiuser: AuthUser
317 :type apiuser: AuthUser
315 :param repoid: Repository name or repository ID to which the pull
318 :param repoid: Repository name or repository ID to which the pull
316 request belongs.
319 request belongs.
317 :type repoid: str or int
320 :type repoid: str or int
318 :param pullrequestid: ID of the pull request to be closed.
321 :param pullrequestid: ID of the pull request to be closed.
319 :type pullrequestid: int
322 :type pullrequestid: int
320 :param userid: Close the pull request as this user.
323 :param userid: Close the pull request as this user.
321 :type userid: Optional(str or int)
324 :type userid: Optional(str or int)
322
325
323 Example output:
326 Example output:
324
327
325 .. code-block:: bash
328 .. code-block:: bash
326
329
327 "id": <id_given_in_input>,
330 "id": <id_given_in_input>,
328 "result": {
331 "result": {
329 "pull_request_id": "<int>",
332 "pull_request_id": "<int>",
330 "closed": "<bool>"
333 "closed": "<bool>"
331 },
334 },
332 "error": null
335 "error": null
333
336
334 """
337 """
335 repo = get_repo_or_error(repoid)
338 repo = get_repo_or_error(repoid)
336 if not isinstance(userid, Optional):
339 if not isinstance(userid, Optional):
337 if (has_superadmin_permission(apiuser) or
340 if (has_superadmin_permission(apiuser) or
338 HasRepoPermissionAnyApi('repository.admin')(
341 HasRepoPermissionAnyApi('repository.admin')(
339 user=apiuser, repo_name=repo.repo_name)):
342 user=apiuser, repo_name=repo.repo_name)):
340 apiuser = get_user_or_error(userid)
343 apiuser = get_user_or_error(userid)
341 else:
344 else:
342 raise JSONRPCError('userid is not the same as your user')
345 raise JSONRPCError('userid is not the same as your user')
343
346
344 pull_request = get_pull_request_or_error(pullrequestid)
347 pull_request = get_pull_request_or_error(pullrequestid)
345 if not PullRequestModel().check_user_update(
348 if not PullRequestModel().check_user_update(
346 pull_request, apiuser, api=True):
349 pull_request, apiuser, api=True):
347 raise JSONRPCError(
350 raise JSONRPCError(
348 'pull request `%s` close failed, no permission to close.' % (
351 'pull request `%s` close failed, no permission to close.' % (
349 pullrequestid,))
352 pullrequestid,))
350 if pull_request.is_closed():
353 if pull_request.is_closed():
351 raise JSONRPCError(
354 raise JSONRPCError(
352 'pull request `%s` is already closed' % (pullrequestid,))
355 'pull request `%s` is already closed' % (pullrequestid,))
353
356
354 PullRequestModel().close_pull_request(
357 PullRequestModel().close_pull_request(
355 pull_request.pull_request_id, apiuser)
358 pull_request.pull_request_id, apiuser)
356 Session().commit()
359 Session().commit()
357 data = {
360 data = {
358 'pull_request_id': pull_request.pull_request_id,
361 'pull_request_id': pull_request.pull_request_id,
359 'closed': True,
362 'closed': True,
360 }
363 }
361 return data
364 return data
362
365
363
366
364 @jsonrpc_method()
367 @jsonrpc_method()
365 def comment_pull_request(
368 def comment_pull_request(
366 request, apiuser, repoid, pullrequestid, message=Optional(None),
369 request, apiuser, repoid, pullrequestid, message=Optional(None),
367 commit_id=Optional(None), status=Optional(None),
370 commit_id=Optional(None), status=Optional(None),
368 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
371 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
369 resolves_comment_id=Optional(None),
372 resolves_comment_id=Optional(None),
370 userid=Optional(OAttr('apiuser'))):
373 userid=Optional(OAttr('apiuser'))):
371 """
374 """
372 Comment on the pull request specified with the `pullrequestid`,
375 Comment on the pull request specified with the `pullrequestid`,
373 in the |repo| specified by the `repoid`, and optionally change the
376 in the |repo| specified by the `repoid`, and optionally change the
374 review status.
377 review status.
375
378
376 :param apiuser: This is filled automatically from the |authtoken|.
379 :param apiuser: This is filled automatically from the |authtoken|.
377 :type apiuser: AuthUser
380 :type apiuser: AuthUser
378 :param repoid: The repository name or repository ID.
381 :param repoid: The repository name or repository ID.
379 :type repoid: str or int
382 :type repoid: str or int
380 :param pullrequestid: The pull request ID.
383 :param pullrequestid: The pull request ID.
381 :type pullrequestid: int
384 :type pullrequestid: int
382 :param commit_id: Specify the commit_id for which to set a comment. If
385 :param commit_id: Specify the commit_id for which to set a comment. If
383 given commit_id is different than latest in the PR status
386 given commit_id is different than latest in the PR status
384 change won't be performed.
387 change won't be performed.
385 :type commit_id: str
388 :type commit_id: str
386 :param message: The text content of the comment.
389 :param message: The text content of the comment.
387 :type message: str
390 :type message: str
388 :param status: (**Optional**) Set the approval status of the pull
391 :param status: (**Optional**) Set the approval status of the pull
389 request. One of: 'not_reviewed', 'approved', 'rejected',
392 request. One of: 'not_reviewed', 'approved', 'rejected',
390 'under_review'
393 'under_review'
391 :type status: str
394 :type status: str
392 :param comment_type: Comment type, one of: 'note', 'todo'
395 :param comment_type: Comment type, one of: 'note', 'todo'
393 :type comment_type: Optional(str), default: 'note'
396 :type comment_type: Optional(str), default: 'note'
394 :param userid: Comment on the pull request as this user
397 :param userid: Comment on the pull request as this user
395 :type userid: Optional(str or int)
398 :type userid: Optional(str or int)
396
399
397 Example output:
400 Example output:
398
401
399 .. code-block:: bash
402 .. code-block:: bash
400
403
401 id : <id_given_in_input>
404 id : <id_given_in_input>
402 result : {
405 result : {
403 "pull_request_id": "<Integer>",
406 "pull_request_id": "<Integer>",
404 "comment_id": "<Integer>",
407 "comment_id": "<Integer>",
405 "status": {"given": <given_status>,
408 "status": {"given": <given_status>,
406 "was_changed": <bool status_was_actually_changed> },
409 "was_changed": <bool status_was_actually_changed> },
407 },
410 },
408 error : null
411 error : null
409 """
412 """
410 repo = get_repo_or_error(repoid)
413 repo = get_repo_or_error(repoid)
411 if not isinstance(userid, Optional):
414 if not isinstance(userid, Optional):
412 if (has_superadmin_permission(apiuser) or
415 if (has_superadmin_permission(apiuser) or
413 HasRepoPermissionAnyApi('repository.admin')(
416 HasRepoPermissionAnyApi('repository.admin')(
414 user=apiuser, repo_name=repo.repo_name)):
417 user=apiuser, repo_name=repo.repo_name)):
415 apiuser = get_user_or_error(userid)
418 apiuser = get_user_or_error(userid)
416 else:
419 else:
417 raise JSONRPCError('userid is not the same as your user')
420 raise JSONRPCError('userid is not the same as your user')
418
421
419 pull_request = get_pull_request_or_error(pullrequestid)
422 pull_request = get_pull_request_or_error(pullrequestid)
420 if not PullRequestModel().check_user_read(
423 if not PullRequestModel().check_user_read(
421 pull_request, apiuser, api=True):
424 pull_request, apiuser, api=True):
422 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
425 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
423 message = Optional.extract(message)
426 message = Optional.extract(message)
424 status = Optional.extract(status)
427 status = Optional.extract(status)
425 commit_id = Optional.extract(commit_id)
428 commit_id = Optional.extract(commit_id)
426 comment_type = Optional.extract(comment_type)
429 comment_type = Optional.extract(comment_type)
427 resolves_comment_id = Optional.extract(resolves_comment_id)
430 resolves_comment_id = Optional.extract(resolves_comment_id)
428
431
429 if not message and not status:
432 if not message and not status:
430 raise JSONRPCError(
433 raise JSONRPCError(
431 'Both message and status parameters are missing. '
434 'Both message and status parameters are missing. '
432 'At least one is required.')
435 'At least one is required.')
433
436
434 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
437 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
435 status is not None):
438 status is not None):
436 raise JSONRPCError('Unknown comment status: `%s`' % status)
439 raise JSONRPCError('Unknown comment status: `%s`' % status)
437
440
438 if commit_id and commit_id not in pull_request.revisions:
441 if commit_id and commit_id not in pull_request.revisions:
439 raise JSONRPCError(
442 raise JSONRPCError(
440 'Invalid commit_id `%s` for this pull request.' % commit_id)
443 'Invalid commit_id `%s` for this pull request.' % commit_id)
441
444
442 allowed_to_change_status = PullRequestModel().check_user_change_status(
445 allowed_to_change_status = PullRequestModel().check_user_change_status(
443 pull_request, apiuser)
446 pull_request, apiuser)
444
447
445 # if commit_id is passed re-validated if user is allowed to change status
448 # if commit_id is passed re-validated if user is allowed to change status
446 # based on latest commit_id from the PR
449 # based on latest commit_id from the PR
447 if commit_id:
450 if commit_id:
448 commit_idx = pull_request.revisions.index(commit_id)
451 commit_idx = pull_request.revisions.index(commit_id)
449 if commit_idx != 0:
452 if commit_idx != 0:
450 allowed_to_change_status = False
453 allowed_to_change_status = False
451
454
452 if resolves_comment_id:
455 if resolves_comment_id:
453 comment = ChangesetComment.get(resolves_comment_id)
456 comment = ChangesetComment.get(resolves_comment_id)
454 if not comment:
457 if not comment:
455 raise JSONRPCError(
458 raise JSONRPCError(
456 'Invalid resolves_comment_id `%s` for this pull request.'
459 'Invalid resolves_comment_id `%s` for this pull request.'
457 % resolves_comment_id)
460 % resolves_comment_id)
458 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
461 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
459 raise JSONRPCError(
462 raise JSONRPCError(
460 'Comment `%s` is wrong type for setting status to resolved.'
463 'Comment `%s` is wrong type for setting status to resolved.'
461 % resolves_comment_id)
464 % resolves_comment_id)
462
465
463 text = message
466 text = message
464 status_label = ChangesetStatus.get_status_lbl(status)
467 status_label = ChangesetStatus.get_status_lbl(status)
465 if status and allowed_to_change_status:
468 if status and allowed_to_change_status:
466 st_message = ('Status change %(transition_icon)s %(status)s'
469 st_message = ('Status change %(transition_icon)s %(status)s'
467 % {'transition_icon': '>', 'status': status_label})
470 % {'transition_icon': '>', 'status': status_label})
468 text = message or st_message
471 text = message or st_message
469
472
470 rc_config = SettingsModel().get_all_settings()
473 rc_config = SettingsModel().get_all_settings()
471 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
474 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
472
475
473 status_change = status and allowed_to_change_status
476 status_change = status and allowed_to_change_status
474 comment = CommentsModel().create(
477 comment = CommentsModel().create(
475 text=text,
478 text=text,
476 repo=pull_request.target_repo.repo_id,
479 repo=pull_request.target_repo.repo_id,
477 user=apiuser.user_id,
480 user=apiuser.user_id,
478 pull_request=pull_request.pull_request_id,
481 pull_request=pull_request.pull_request_id,
479 f_path=None,
482 f_path=None,
480 line_no=None,
483 line_no=None,
481 status_change=(status_label if status_change else None),
484 status_change=(status_label if status_change else None),
482 status_change_type=(status if status_change else None),
485 status_change_type=(status if status_change else None),
483 closing_pr=False,
486 closing_pr=False,
484 renderer=renderer,
487 renderer=renderer,
485 comment_type=comment_type,
488 comment_type=comment_type,
486 resolves_comment_id=resolves_comment_id
489 resolves_comment_id=resolves_comment_id
487 )
490 )
488
491
489 if allowed_to_change_status and status:
492 if allowed_to_change_status and status:
490 ChangesetStatusModel().set_status(
493 ChangesetStatusModel().set_status(
491 pull_request.target_repo.repo_id,
494 pull_request.target_repo.repo_id,
492 status,
495 status,
493 apiuser.user_id,
496 apiuser.user_id,
494 comment,
497 comment,
495 pull_request=pull_request.pull_request_id
498 pull_request=pull_request.pull_request_id
496 )
499 )
497 Session().flush()
500 Session().flush()
498
501
499 Session().commit()
502 Session().commit()
500 data = {
503 data = {
501 'pull_request_id': pull_request.pull_request_id,
504 'pull_request_id': pull_request.pull_request_id,
502 'comment_id': comment.comment_id if comment else None,
505 'comment_id': comment.comment_id if comment else None,
503 'status': {'given': status, 'was_changed': status_change},
506 'status': {'given': status, 'was_changed': status_change},
504 }
507 }
505 return data
508 return data
506
509
507
510
508 @jsonrpc_method()
511 @jsonrpc_method()
509 def create_pull_request(
512 def create_pull_request(
510 request, apiuser, source_repo, target_repo, source_ref, target_ref,
513 request, apiuser, source_repo, target_repo, source_ref, target_ref,
511 title, description=Optional(''), reviewers=Optional(None)):
514 title, description=Optional(''), reviewers=Optional(None)):
512 """
515 """
513 Creates a new pull request.
516 Creates a new pull request.
514
517
515 Accepts refs in the following formats:
518 Accepts refs in the following formats:
516
519
517 * branch:<branch_name>:<sha>
520 * branch:<branch_name>:<sha>
518 * branch:<branch_name>
521 * branch:<branch_name>
519 * bookmark:<bookmark_name>:<sha> (Mercurial only)
522 * bookmark:<bookmark_name>:<sha> (Mercurial only)
520 * bookmark:<bookmark_name> (Mercurial only)
523 * bookmark:<bookmark_name> (Mercurial only)
521
524
522 :param apiuser: This is filled automatically from the |authtoken|.
525 :param apiuser: This is filled automatically from the |authtoken|.
523 :type apiuser: AuthUser
526 :type apiuser: AuthUser
524 :param source_repo: Set the source repository name.
527 :param source_repo: Set the source repository name.
525 :type source_repo: str
528 :type source_repo: str
526 :param target_repo: Set the target repository name.
529 :param target_repo: Set the target repository name.
527 :type target_repo: str
530 :type target_repo: str
528 :param source_ref: Set the source ref name.
531 :param source_ref: Set the source ref name.
529 :type source_ref: str
532 :type source_ref: str
530 :param target_ref: Set the target ref name.
533 :param target_ref: Set the target ref name.
531 :type target_ref: str
534 :type target_ref: str
532 :param title: Set the pull request title.
535 :param title: Set the pull request title.
533 :type title: str
536 :type title: str
534 :param description: Set the pull request description.
537 :param description: Set the pull request description.
535 :type description: Optional(str)
538 :type description: Optional(str)
536 :param reviewers: Set the new pull request reviewers list.
539 :param reviewers: Set the new pull request reviewers list.
537 :type reviewers: Optional(list)
540 :type reviewers: Optional(list)
538 Accepts username strings or objects of the format:
541 Accepts username strings or objects of the format:
539
542
540 {'username': 'nick', 'reasons': ['original author']}
543 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
541 """
544 """
542
545
543 source = get_repo_or_error(source_repo)
546 source = get_repo_or_error(source_repo)
544 target = get_repo_or_error(target_repo)
547 target = get_repo_or_error(target_repo)
545 if not has_superadmin_permission(apiuser):
548 if not has_superadmin_permission(apiuser):
546 _perms = ('repository.admin', 'repository.write', 'repository.read',)
549 _perms = ('repository.admin', 'repository.write', 'repository.read',)
547 validate_repo_permissions(apiuser, source_repo, source, _perms)
550 validate_repo_permissions(apiuser, source_repo, source, _perms)
548
551
549 full_source_ref = resolve_ref_or_error(source_ref, source)
552 full_source_ref = resolve_ref_or_error(source_ref, source)
550 full_target_ref = resolve_ref_or_error(target_ref, target)
553 full_target_ref = resolve_ref_or_error(target_ref, target)
551 source_commit = get_commit_or_error(full_source_ref, source)
554 source_commit = get_commit_or_error(full_source_ref, source)
552 target_commit = get_commit_or_error(full_target_ref, target)
555 target_commit = get_commit_or_error(full_target_ref, target)
553 source_scm = source.scm_instance()
556 source_scm = source.scm_instance()
554 target_scm = target.scm_instance()
557 target_scm = target.scm_instance()
555
558
556 commit_ranges = target_scm.compare(
559 commit_ranges = target_scm.compare(
557 target_commit.raw_id, source_commit.raw_id, source_scm,
560 target_commit.raw_id, source_commit.raw_id, source_scm,
558 merge=True, pre_load=[])
561 merge=True, pre_load=[])
559
562
560 ancestor = target_scm.get_common_ancestor(
563 ancestor = target_scm.get_common_ancestor(
561 target_commit.raw_id, source_commit.raw_id, source_scm)
564 target_commit.raw_id, source_commit.raw_id, source_scm)
562
565
563 if not commit_ranges:
566 if not commit_ranges:
564 raise JSONRPCError('no commits found')
567 raise JSONRPCError('no commits found')
565
568
566 if not ancestor:
569 if not ancestor:
567 raise JSONRPCError('no common ancestor found')
570 raise JSONRPCError('no common ancestor found')
568
571
569 reviewer_objects = Optional.extract(reviewers) or []
572 reviewer_objects = Optional.extract(reviewers) or []
570 if not isinstance(reviewer_objects, list):
573 if reviewer_objects:
571 raise JSONRPCError('reviewers should be specified as a list')
574 schema = ReviewerListSchema()
575 try:
576 reviewer_objects = schema.deserialize(reviewer_objects)
577 except Invalid as err:
578 raise JSONRPCValidationError(colander_exc=err)
572
579
573 reviewers_reasons = []
580 reviewers = []
574 for reviewer_object in reviewer_objects:
581 for reviewer_object in reviewer_objects:
575 reviewer_reasons = []
582 user = get_user_or_error(reviewer_object['username'])
576 if isinstance(reviewer_object, (basestring, int)):
583 reasons = reviewer_object['reasons']
577 reviewer_username = reviewer_object
584 mandatory = reviewer_object['mandatory']
578 else:
585 reviewers.append((user.user_id, reasons, mandatory))
579 reviewer_username = reviewer_object['username']
580 reviewer_reasons = reviewer_object.get('reasons', [])
581
582 user = get_user_or_error(reviewer_username)
583 reviewers_reasons.append((user.user_id, reviewer_reasons))
584
586
585 pull_request_model = PullRequestModel()
587 pull_request_model = PullRequestModel()
586 pull_request = pull_request_model.create(
588 pull_request = pull_request_model.create(
587 created_by=apiuser.user_id,
589 created_by=apiuser.user_id,
588 source_repo=source_repo,
590 source_repo=source_repo,
589 source_ref=full_source_ref,
591 source_ref=full_source_ref,
590 target_repo=target_repo,
592 target_repo=target_repo,
591 target_ref=full_target_ref,
593 target_ref=full_target_ref,
592 revisions=reversed(
594 revisions=reversed(
593 [commit.raw_id for commit in reversed(commit_ranges)]),
595 [commit.raw_id for commit in reversed(commit_ranges)]),
594 reviewers=reviewers_reasons,
596 reviewers=reviewers,
595 title=title,
597 title=title,
596 description=Optional.extract(description)
598 description=Optional.extract(description)
597 )
599 )
598
600
599 Session().commit()
601 Session().commit()
600 data = {
602 data = {
601 'msg': 'Created new pull request `{}`'.format(title),
603 'msg': 'Created new pull request `{}`'.format(title),
602 'pull_request_id': pull_request.pull_request_id,
604 'pull_request_id': pull_request.pull_request_id,
603 }
605 }
604 return data
606 return data
605
607
606
608
607 @jsonrpc_method()
609 @jsonrpc_method()
608 def update_pull_request(
610 def update_pull_request(
609 request, apiuser, repoid, pullrequestid, title=Optional(''),
611 request, apiuser, repoid, pullrequestid, title=Optional(''),
610 description=Optional(''), reviewers=Optional(None),
612 description=Optional(''), reviewers=Optional(None),
611 update_commits=Optional(None), close_pull_request=Optional(None)):
613 update_commits=Optional(None), close_pull_request=Optional(None)):
612 """
614 """
613 Updates a pull request.
615 Updates a pull request.
614
616
615 :param apiuser: This is filled automatically from the |authtoken|.
617 :param apiuser: This is filled automatically from the |authtoken|.
616 :type apiuser: AuthUser
618 :type apiuser: AuthUser
617 :param repoid: The repository name or repository ID.
619 :param repoid: The repository name or repository ID.
618 :type repoid: str or int
620 :type repoid: str or int
619 :param pullrequestid: The pull request ID.
621 :param pullrequestid: The pull request ID.
620 :type pullrequestid: int
622 :type pullrequestid: int
621 :param title: Set the pull request title.
623 :param title: Set the pull request title.
622 :type title: str
624 :type title: str
623 :param description: Update pull request description.
625 :param description: Update pull request description.
624 :type description: Optional(str)
626 :type description: Optional(str)
625 :param reviewers: Update pull request reviewers list with new value.
627 :param reviewers: Update pull request reviewers list with new value.
626 :type reviewers: Optional(list)
628 :type reviewers: Optional(list)
629 Accepts username strings or objects of the format:
630
631 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
632
627 :param update_commits: Trigger update of commits for this pull request
633 :param update_commits: Trigger update of commits for this pull request
628 :type: update_commits: Optional(bool)
634 :type: update_commits: Optional(bool)
629 :param close_pull_request: Close this pull request with rejected state
635 :param close_pull_request: Close this pull request with rejected state
630 :type: close_pull_request: Optional(bool)
636 :type: close_pull_request: Optional(bool)
631
637
632 Example output:
638 Example output:
633
639
634 .. code-block:: bash
640 .. code-block:: bash
635
641
636 id : <id_given_in_input>
642 id : <id_given_in_input>
637 result : {
643 result : {
638 "msg": "Updated pull request `63`",
644 "msg": "Updated pull request `63`",
639 "pull_request": <pull_request_object>,
645 "pull_request": <pull_request_object>,
640 "updated_reviewers": {
646 "updated_reviewers": {
641 "added": [
647 "added": [
642 "username"
648 "username"
643 ],
649 ],
644 "removed": []
650 "removed": []
645 },
651 },
646 "updated_commits": {
652 "updated_commits": {
647 "added": [
653 "added": [
648 "<sha1_hash>"
654 "<sha1_hash>"
649 ],
655 ],
650 "common": [
656 "common": [
651 "<sha1_hash>",
657 "<sha1_hash>",
652 "<sha1_hash>",
658 "<sha1_hash>",
653 ],
659 ],
654 "removed": []
660 "removed": []
655 }
661 }
656 }
662 }
657 error : null
663 error : null
658 """
664 """
659
665
660 repo = get_repo_or_error(repoid)
666 repo = get_repo_or_error(repoid)
661 pull_request = get_pull_request_or_error(pullrequestid)
667 pull_request = get_pull_request_or_error(pullrequestid)
662 if not PullRequestModel().check_user_update(
668 if not PullRequestModel().check_user_update(
663 pull_request, apiuser, api=True):
669 pull_request, apiuser, api=True):
664 raise JSONRPCError(
670 raise JSONRPCError(
665 'pull request `%s` update failed, no permission to update.' % (
671 'pull request `%s` update failed, no permission to update.' % (
666 pullrequestid,))
672 pullrequestid,))
667 if pull_request.is_closed():
673 if pull_request.is_closed():
668 raise JSONRPCError(
674 raise JSONRPCError(
669 'pull request `%s` update failed, pull request is closed' % (
675 'pull request `%s` update failed, pull request is closed' % (
670 pullrequestid,))
676 pullrequestid,))
671
677
678
672 reviewer_objects = Optional.extract(reviewers) or []
679 reviewer_objects = Optional.extract(reviewers) or []
673 if not isinstance(reviewer_objects, list):
680 if reviewer_objects:
674 raise JSONRPCError('reviewers should be specified as a list')
681 schema = ReviewerListSchema()
682 try:
683 reviewer_objects = schema.deserialize(reviewer_objects)
684 except Invalid as err:
685 raise JSONRPCValidationError(colander_exc=err)
675
686
676 reviewers_reasons = []
687 reviewers = []
677 reviewer_ids = set()
678 for reviewer_object in reviewer_objects:
688 for reviewer_object in reviewer_objects:
679 reviewer_reasons = []
689 user = get_user_or_error(reviewer_object['username'])
680 if isinstance(reviewer_object, (int, basestring)):
690 reasons = reviewer_object['reasons']
681 reviewer_username = reviewer_object
691 mandatory = reviewer_object['mandatory']
682 else:
692 reviewers.append((user.user_id, reasons, mandatory))
683 reviewer_username = reviewer_object['username']
684 reviewer_reasons = reviewer_object.get('reasons', [])
685
686 user = get_user_or_error(reviewer_username)
687 reviewer_ids.add(user.user_id)
688 reviewers_reasons.append((user.user_id, reviewer_reasons))
689
693
690 title = Optional.extract(title)
694 title = Optional.extract(title)
691 description = Optional.extract(description)
695 description = Optional.extract(description)
692 if title or description:
696 if title or description:
693 PullRequestModel().edit(
697 PullRequestModel().edit(
694 pull_request, title or pull_request.title,
698 pull_request, title or pull_request.title,
695 description or pull_request.description)
699 description or pull_request.description)
696 Session().commit()
700 Session().commit()
697
701
698 commit_changes = {"added": [], "common": [], "removed": []}
702 commit_changes = {"added": [], "common": [], "removed": []}
699 if str2bool(Optional.extract(update_commits)):
703 if str2bool(Optional.extract(update_commits)):
700 if PullRequestModel().has_valid_update_type(pull_request):
704 if PullRequestModel().has_valid_update_type(pull_request):
701 update_response = PullRequestModel().update_commits(
705 update_response = PullRequestModel().update_commits(
702 pull_request)
706 pull_request)
703 commit_changes = update_response.changes or commit_changes
707 commit_changes = update_response.changes or commit_changes
704 Session().commit()
708 Session().commit()
705
709
706 reviewers_changes = {"added": [], "removed": []}
710 reviewers_changes = {"added": [], "removed": []}
707 if reviewer_ids:
711 if reviewers:
708 added_reviewers, removed_reviewers = \
712 added_reviewers, removed_reviewers = \
709 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
713 PullRequestModel().update_reviewers(pull_request, reviewers)
710
714
711 reviewers_changes['added'] = sorted(
715 reviewers_changes['added'] = sorted(
712 [get_user_or_error(n).username for n in added_reviewers])
716 [get_user_or_error(n).username for n in added_reviewers])
713 reviewers_changes['removed'] = sorted(
717 reviewers_changes['removed'] = sorted(
714 [get_user_or_error(n).username for n in removed_reviewers])
718 [get_user_or_error(n).username for n in removed_reviewers])
715 Session().commit()
719 Session().commit()
716
720
717 if str2bool(Optional.extract(close_pull_request)):
721 if str2bool(Optional.extract(close_pull_request)):
718 PullRequestModel().close_pull_request_with_comment(
722 PullRequestModel().close_pull_request_with_comment(
719 pull_request, apiuser, repo)
723 pull_request, apiuser, repo)
720 Session().commit()
724 Session().commit()
721
725
722 data = {
726 data = {
723 'msg': 'Updated pull request `{}`'.format(
727 'msg': 'Updated pull request `{}`'.format(
724 pull_request.pull_request_id),
728 pull_request.pull_request_id),
725 'pull_request': pull_request.get_api_data(),
729 'pull_request': pull_request.get_api_data(),
726 'updated_commits': commit_changes,
730 'updated_commits': commit_changes,
727 'updated_reviewers': reviewers_changes
731 'updated_reviewers': reviewers_changes
728 }
732 }
729
733
730 return data
734 return data
@@ -1,301 +1,308 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23 from pylons import tmpl_context as c
23 from pylons import tmpl_context as c
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils import PartialRenderer
27 from rhodecode.lib.utils import PartialRenderer
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.model import repo
31 from rhodecode.model import repo
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33 from rhodecode.model.scm import ScmModel
33 from rhodecode.model.scm import ScmModel
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 ADMIN_PREFIX = '/_admin'
38 ADMIN_PREFIX = '/_admin'
39 STATIC_FILE_PREFIX = '/_static'
39 STATIC_FILE_PREFIX = '/_static'
40
40
41
41
42 def get_format_ref_id(repo):
42 def get_format_ref_id(repo):
43 """Returns a `repo` specific reference formatter function"""
43 """Returns a `repo` specific reference formatter function"""
44 if h.is_svn(repo):
44 if h.is_svn(repo):
45 return _format_ref_id_svn
45 return _format_ref_id_svn
46 else:
46 else:
47 return _format_ref_id
47 return _format_ref_id
48
48
49
49
50 def _format_ref_id(name, raw_id):
50 def _format_ref_id(name, raw_id):
51 """Default formatting of a given reference `name`"""
51 """Default formatting of a given reference `name`"""
52 return name
52 return name
53
53
54
54
55 def _format_ref_id_svn(name, raw_id):
55 def _format_ref_id_svn(name, raw_id):
56 """Special way of formatting a reference for Subversion including path"""
56 """Special way of formatting a reference for Subversion including path"""
57 return '%s@%s' % (name, raw_id)
57 return '%s@%s' % (name, raw_id)
58
58
59
59
60 class TemplateArgs(StrictAttributeDict):
60 class TemplateArgs(StrictAttributeDict):
61 pass
61 pass
62
62
63
63
64 class BaseAppView(object):
64 class BaseAppView(object):
65
65
66 def __init__(self, context, request):
66 def __init__(self, context, request):
67 self.request = request
67 self.request = request
68 self.context = context
68 self.context = context
69 self.session = request.session
69 self.session = request.session
70 self._rhodecode_user = request.user # auth user
70 self._rhodecode_user = request.user # auth user
71 self._rhodecode_db_user = self._rhodecode_user.get_instance()
71 self._rhodecode_db_user = self._rhodecode_user.get_instance()
72 self._maybe_needs_password_change(
72 self._maybe_needs_password_change(
73 request.matched_route.name, self._rhodecode_db_user)
73 request.matched_route.name, self._rhodecode_db_user)
74
74
75 def _maybe_needs_password_change(self, view_name, user_obj):
75 def _maybe_needs_password_change(self, view_name, user_obj):
76 log.debug('Checking if user %s needs password change on view %s',
76 log.debug('Checking if user %s needs password change on view %s',
77 user_obj, view_name)
77 user_obj, view_name)
78 skip_user_views = [
78 skip_user_views = [
79 'logout', 'login',
79 'logout', 'login',
80 'my_account_password', 'my_account_password_update'
80 'my_account_password', 'my_account_password_update'
81 ]
81 ]
82
82
83 if not user_obj:
83 if not user_obj:
84 return
84 return
85
85
86 if user_obj.username == User.DEFAULT_USER:
86 if user_obj.username == User.DEFAULT_USER:
87 return
87 return
88
88
89 now = time.time()
89 now = time.time()
90 should_change = user_obj.user_data.get('force_password_change')
90 should_change = user_obj.user_data.get('force_password_change')
91 change_after = safe_int(should_change) or 0
91 change_after = safe_int(should_change) or 0
92 if should_change and now > change_after:
92 if should_change and now > change_after:
93 log.debug('User %s requires password change', user_obj)
93 log.debug('User %s requires password change', user_obj)
94 h.flash('You are required to change your password', 'warning',
94 h.flash('You are required to change your password', 'warning',
95 ignore_duplicate=True)
95 ignore_duplicate=True)
96
96
97 if view_name not in skip_user_views:
97 if view_name not in skip_user_views:
98 raise HTTPFound(
98 raise HTTPFound(
99 self.request.route_path('my_account_password'))
99 self.request.route_path('my_account_password'))
100
100
101 def _get_local_tmpl_context(self):
101 def _get_local_tmpl_context(self):
102 c = TemplateArgs()
102 c = TemplateArgs()
103 c.auth_user = self.request.user
103 c.auth_user = self.request.user
104 return c
104 return c
105
105
106 def _register_global_c(self, tmpl_args):
106 def _register_global_c(self, tmpl_args):
107 """
107 """
108 Registers attributes to pylons global `c`
108 Registers attributes to pylons global `c`
109 """
109 """
110 # TODO(marcink): remove once pyramid migration is finished
110 # TODO(marcink): remove once pyramid migration is finished
111 for k, v in tmpl_args.items():
111 for k, v in tmpl_args.items():
112 setattr(c, k, v)
112 setattr(c, k, v)
113
113
114 def _get_template_context(self, tmpl_args):
114 def _get_template_context(self, tmpl_args):
115 self._register_global_c(tmpl_args)
115 self._register_global_c(tmpl_args)
116
116
117 local_tmpl_args = {
117 local_tmpl_args = {
118 'defaults': {},
118 'defaults': {},
119 'errors': {},
119 'errors': {},
120 }
120 }
121 local_tmpl_args.update(tmpl_args)
121 local_tmpl_args.update(tmpl_args)
122 return local_tmpl_args
122 return local_tmpl_args
123
123
124 def load_default_context(self):
124 def load_default_context(self):
125 """
125 """
126 example:
126 example:
127
127
128 def load_default_context(self):
128 def load_default_context(self):
129 c = self._get_local_tmpl_context()
129 c = self._get_local_tmpl_context()
130 c.custom_var = 'foobar'
130 c.custom_var = 'foobar'
131 self._register_global_c(c)
131 self._register_global_c(c)
132 return c
132 return c
133 """
133 """
134 raise NotImplementedError('Needs implementation in view class')
134 raise NotImplementedError('Needs implementation in view class')
135
135
136
136
137 class RepoAppView(BaseAppView):
137 class RepoAppView(BaseAppView):
138
138
139 def __init__(self, context, request):
139 def __init__(self, context, request):
140 super(RepoAppView, self).__init__(context, request)
140 super(RepoAppView, self).__init__(context, request)
141 self.db_repo = request.db_repo
141 self.db_repo = request.db_repo
142 self.db_repo_name = self.db_repo.repo_name
142 self.db_repo_name = self.db_repo.repo_name
143 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
143 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
144
144
145 def _handle_missing_requirements(self, error):
145 def _handle_missing_requirements(self, error):
146 log.error(
146 log.error(
147 'Requirements are missing for repository %s: %s',
147 'Requirements are missing for repository %s: %s',
148 self.db_repo_name, error.message)
148 self.db_repo_name, error.message)
149
149
150 def _get_local_tmpl_context(self):
150 def _get_local_tmpl_context(self):
151 c = super(RepoAppView, self)._get_local_tmpl_context()
151 c = super(RepoAppView, self)._get_local_tmpl_context()
152 # register common vars for this type of view
152 # register common vars for this type of view
153 c.rhodecode_db_repo = self.db_repo
153 c.rhodecode_db_repo = self.db_repo
154 c.repo_name = self.db_repo_name
154 c.repo_name = self.db_repo_name
155 c.repository_pull_requests = self.db_repo_pull_requests
155 c.repository_pull_requests = self.db_repo_pull_requests
156
156
157 c.repository_requirements_missing = False
157 c.repository_requirements_missing = False
158 try:
158 try:
159 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
159 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
160 except RepositoryRequirementError as e:
160 except RepositoryRequirementError as e:
161 c.repository_requirements_missing = True
161 c.repository_requirements_missing = True
162 self._handle_missing_requirements(e)
162 self._handle_missing_requirements(e)
163
163
164 return c
164 return c
165
165
166
166
167 class DataGridAppView(object):
167 class DataGridAppView(object):
168 """
168 """
169 Common class to have re-usable grid rendering components
169 Common class to have re-usable grid rendering components
170 """
170 """
171
171
172 def _extract_ordering(self, request, column_map=None):
172 def _extract_ordering(self, request, column_map=None):
173 column_map = column_map or {}
173 column_map = column_map or {}
174 column_index = safe_int(request.GET.get('order[0][column]'))
174 column_index = safe_int(request.GET.get('order[0][column]'))
175 order_dir = request.GET.get(
175 order_dir = request.GET.get(
176 'order[0][dir]', 'desc')
176 'order[0][dir]', 'desc')
177 order_by = request.GET.get(
177 order_by = request.GET.get(
178 'columns[%s][data][sort]' % column_index, 'name_raw')
178 'columns[%s][data][sort]' % column_index, 'name_raw')
179
179
180 # translate datatable to DB columns
180 # translate datatable to DB columns
181 order_by = column_map.get(order_by) or order_by
181 order_by = column_map.get(order_by) or order_by
182
182
183 search_q = request.GET.get('search[value]')
183 search_q = request.GET.get('search[value]')
184 return search_q, order_by, order_dir
184 return search_q, order_by, order_dir
185
185
186 def _extract_chunk(self, request):
186 def _extract_chunk(self, request):
187 start = safe_int(request.GET.get('start'), 0)
187 start = safe_int(request.GET.get('start'), 0)
188 length = safe_int(request.GET.get('length'), 25)
188 length = safe_int(request.GET.get('length'), 25)
189 draw = safe_int(request.GET.get('draw'))
189 draw = safe_int(request.GET.get('draw'))
190 return draw, start, length
190 return draw, start, length
191
191
192
192
193 class BaseReferencesView(RepoAppView):
193 class BaseReferencesView(RepoAppView):
194 """
194 """
195 Base for reference view for branches, tags and bookmarks.
195 Base for reference view for branches, tags and bookmarks.
196 """
196 """
197 def load_default_context(self):
197 def load_default_context(self):
198 c = self._get_local_tmpl_context()
198 c = self._get_local_tmpl_context()
199
199
200 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
200 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
201 c.repo_info = self.db_repo
201 c.repo_info = self.db_repo
202
202
203 self._register_global_c(c)
203 self._register_global_c(c)
204 return c
204 return c
205
205
206 def load_refs_context(self, ref_items, partials_template):
206 def load_refs_context(self, ref_items, partials_template):
207 _render = PartialRenderer(partials_template)
207 _render = PartialRenderer(partials_template)
208 _data = []
208 _data = []
209 pre_load = ["author", "date", "message"]
209 pre_load = ["author", "date", "message"]
210
210
211 is_svn = h.is_svn(self.rhodecode_vcs_repo)
211 is_svn = h.is_svn(self.rhodecode_vcs_repo)
212 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
212 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
213
213
214 for ref_name, commit_id in ref_items:
214 for ref_name, commit_id in ref_items:
215 commit = self.rhodecode_vcs_repo.get_commit(
215 commit = self.rhodecode_vcs_repo.get_commit(
216 commit_id=commit_id, pre_load=pre_load)
216 commit_id=commit_id, pre_load=pre_load)
217
217
218 # TODO: johbo: Unify generation of reference links
218 # TODO: johbo: Unify generation of reference links
219 use_commit_id = '/' in ref_name or is_svn
219 use_commit_id = '/' in ref_name or is_svn
220 files_url = h.url(
220 files_url = h.url(
221 'files_home',
221 'files_home',
222 repo_name=c.repo_name,
222 repo_name=c.repo_name,
223 f_path=ref_name if is_svn else '',
223 f_path=ref_name if is_svn else '',
224 revision=commit_id if use_commit_id else ref_name,
224 revision=commit_id if use_commit_id else ref_name,
225 at=ref_name)
225 at=ref_name)
226
226
227 _data.append({
227 _data.append({
228 "name": _render('name', ref_name, files_url),
228 "name": _render('name', ref_name, files_url),
229 "name_raw": ref_name,
229 "name_raw": ref_name,
230 "date": _render('date', commit.date),
230 "date": _render('date', commit.date),
231 "date_raw": datetime_to_time(commit.date),
231 "date_raw": datetime_to_time(commit.date),
232 "author": _render('author', commit.author),
232 "author": _render('author', commit.author),
233 "commit": _render(
233 "commit": _render(
234 'commit', commit.message, commit.raw_id, commit.idx),
234 'commit', commit.message, commit.raw_id, commit.idx),
235 "commit_raw": commit.idx,
235 "commit_raw": commit.idx,
236 "compare": _render(
236 "compare": _render(
237 'compare', format_ref_id(ref_name, commit.raw_id)),
237 'compare', format_ref_id(ref_name, commit.raw_id)),
238 })
238 })
239 c.has_references = bool(_data)
239 c.has_references = bool(_data)
240 c.data = json.dumps(_data)
240 c.data = json.dumps(_data)
241
241
242
242
243 class RepoRoutePredicate(object):
243 class RepoRoutePredicate(object):
244 def __init__(self, val, config):
244 def __init__(self, val, config):
245 self.val = val
245 self.val = val
246
246
247 def text(self):
247 def text(self):
248 return 'repo_route = %s' % self.val
248 return 'repo_route = %s' % self.val
249
249
250 phash = text
250 phash = text
251
251
252 def __call__(self, info, request):
252 def __call__(self, info, request):
253 repo_name = info['match']['repo_name']
253 repo_name = info['match']['repo_name']
254 repo_model = repo.RepoModel()
254 repo_model = repo.RepoModel()
255 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
255 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
256 # if we match quickly from database, short circuit the operation,
256 # if we match quickly from database, short circuit the operation,
257 # and validate repo based on the type.
257 # and validate repo based on the type.
258 if by_name_match:
258 if by_name_match:
259 # register this as request object we can re-use later
259 # register this as request object we can re-use later
260 request.db_repo = by_name_match
260 request.db_repo = by_name_match
261 return True
261 return True
262
262
263 by_id_match = repo_model.get_repo_by_id(repo_name)
263 by_id_match = repo_model.get_repo_by_id(repo_name)
264 if by_id_match:
264 if by_id_match:
265 request.db_repo = by_id_match
265 request.db_repo = by_id_match
266 return True
266 return True
267
267
268 return False
268 return False
269
269
270
270
271 class RepoTypeRoutePredicate(object):
271 class RepoTypeRoutePredicate(object):
272 def __init__(self, val, config):
272 def __init__(self, val, config):
273 self.val = val or ['hg', 'git', 'svn']
273 self.val = val or ['hg', 'git', 'svn']
274
274
275 def text(self):
275 def text(self):
276 return 'repo_accepted_type = %s' % self.val
276 return 'repo_accepted_type = %s' % self.val
277
277
278 phash = text
278 phash = text
279
279
280 def __call__(self, info, request):
280 def __call__(self, info, request):
281
281
282 rhodecode_db_repo = request.db_repo
282 rhodecode_db_repo = request.db_repo
283
283
284 log.debug(
284 log.debug(
285 '%s checking repo type for %s in %s',
285 '%s checking repo type for %s in %s',
286 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
286 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
287
287
288 if rhodecode_db_repo.repo_type in self.val:
288 if rhodecode_db_repo.repo_type in self.val:
289 return True
289 return True
290 else:
290 else:
291 log.warning('Current view is not supported for repo type:%s',
291 log.warning('Current view is not supported for repo type:%s',
292 rhodecode_db_repo.repo_type)
292 rhodecode_db_repo.repo_type)
293 #
294 # h.flash(h.literal(
295 # _('Action not supported for %s.' % rhodecode_repo.alias)),
296 # category='warning')
297 # return redirect(
298 # url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
299
293 return False
300 return False
294
301
295
302
296
303
297 def includeme(config):
304 def includeme(config):
298 config.add_route_predicate(
305 config.add_route_predicate(
299 'repo_route', RepoRoutePredicate)
306 'repo_route', RepoRoutePredicate)
300 config.add_route_predicate(
307 config.add_route_predicate(
301 'repo_accepted_types', RepoTypeRoutePredicate) No newline at end of file
308 'repo_accepted_types', RepoTypeRoutePredicate)
@@ -1,69 +1,64 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24
24
25 from rhodecode.apps._base import RepoAppView
25 from rhodecode.apps._base import RepoAppView
26 from rhodecode.controllers import utils
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class RepoReviewRulesView(RepoAppView):
32 class RepoReviewRulesView(RepoAppView):
33 def load_default_context(self):
33 def load_default_context(self):
34 c = self._get_local_tmpl_context()
34 c = self._get_local_tmpl_context()
35
35
36 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
36 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
37 c.repo_info = self.db_repo
37 c.repo_info = self.db_repo
38
38
39 self._register_global_c(c)
39 self._register_global_c(c)
40 return c
40 return c
41
41
42 @LoginRequired()
42 @LoginRequired()
43 @HasRepoPermissionAnyDecorator('repository.admin')
43 @HasRepoPermissionAnyDecorator('repository.admin')
44 @view_config(
44 @view_config(
45 route_name='repo_reviewers', request_method='GET',
45 route_name='repo_reviewers', request_method='GET',
46 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
46 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
47 def repo_review_rules(self):
47 def repo_review_rules(self):
48 c = self.load_default_context()
48 c = self.load_default_context()
49 c.active = 'reviewers'
49 c.active = 'reviewers'
50
50
51 return self._get_template_context(c)
51 return self._get_template_context(c)
52
52
53 @LoginRequired()
53 @LoginRequired()
54 @HasRepoPermissionAnyDecorator(
54 @HasRepoPermissionAnyDecorator(
55 'repository.read', 'repository.write', 'repository.admin')
55 'repository.read', 'repository.write', 'repository.admin')
56 @view_config(
56 @view_config(
57 route_name='repo_default_reviewers_data', request_method='GET',
57 route_name='repo_default_reviewers_data', request_method='GET',
58 renderer='json_ext')
58 renderer='json_ext')
59 def repo_default_reviewers_data(self):
59 def repo_default_reviewers_data(self):
60 reasons = ['Default reviewer', 'Repository owner']
60 review_data = get_default_reviewers_data(
61 default = utils.reviewer_as_json(
61 self.db_repo.user, None, None, None, None)
62 user=self.db_repo.user, reasons=reasons, mandatory=False)
62 return review_data
63
63
64 return {
64
65 'api_ver': 'v1', # define version for later possible schema upgrade
66 'reviewers': [default],
67 'rules': {},
68 'rules_data': {},
69 }
@@ -1,978 +1,1018 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @NotAnonymous()
76 @NotAnonymous()
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 'repository.admin')
78 'repository.admin')
79 @HasAcceptedRepoType('git', 'hg')
79 @HasAcceptedRepoType('git', 'hg')
80 def index(self):
80 def index(self):
81 source_repo = c.rhodecode_db_repo
81 source_repo = c.rhodecode_db_repo
82
82
83 try:
83 try:
84 source_repo.scm_instance().get_commit()
84 source_repo.scm_instance().get_commit()
85 except EmptyRepositoryError:
85 except EmptyRepositoryError:
86 h.flash(h.literal(_('There are no commits yet')),
86 h.flash(h.literal(_('There are no commits yet')),
87 category='warning')
87 category='warning')
88 redirect(url('summary_home', repo_name=source_repo.repo_name))
88 redirect(url('summary_home', repo_name=source_repo.repo_name))
89
89
90 commit_id = request.GET.get('commit')
90 commit_id = request.GET.get('commit')
91 branch_ref = request.GET.get('branch')
91 branch_ref = request.GET.get('branch')
92 bookmark_ref = request.GET.get('bookmark')
92 bookmark_ref = request.GET.get('bookmark')
93
93
94 try:
94 try:
95 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo_data = PullRequestModel().generate_repo_data(
96 source_repo, commit_id=commit_id,
96 source_repo, commit_id=commit_id,
97 branch=branch_ref, bookmark=bookmark_ref)
97 branch=branch_ref, bookmark=bookmark_ref)
98 except CommitDoesNotExistError as e:
98 except CommitDoesNotExistError as e:
99 log.exception(e)
99 log.exception(e)
100 h.flash(_('Commit does not exist'), 'error')
100 h.flash(_('Commit does not exist'), 'error')
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102
102
103 default_target_repo = source_repo
103 default_target_repo = source_repo
104
104
105 if source_repo.parent:
105 if source_repo.parent:
106 parent_vcs_obj = source_repo.parent.scm_instance()
106 parent_vcs_obj = source_repo.parent.scm_instance()
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 # change default if we have a parent repo
108 # change default if we have a parent repo
109 default_target_repo = source_repo.parent
109 default_target_repo = source_repo.parent
110
110
111 target_repo_data = PullRequestModel().generate_repo_data(
111 target_repo_data = PullRequestModel().generate_repo_data(
112 default_target_repo)
112 default_target_repo)
113
113
114 selected_source_ref = source_repo_data['refs']['selected_ref']
114 selected_source_ref = source_repo_data['refs']['selected_ref']
115
115
116 title_source_ref = selected_source_ref.split(':', 2)[1]
116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 c.default_title = PullRequestModel().generate_pullrequest_title(
117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 source=source_repo.repo_name,
118 source=source_repo.repo_name,
119 source_ref=title_source_ref,
119 source_ref=title_source_ref,
120 target=default_target_repo.repo_name
120 target=default_target_repo.repo_name
121 )
121 )
122
122
123 c.default_repo_data = {
123 c.default_repo_data = {
124 'source_repo_name': source_repo.repo_name,
124 'source_repo_name': source_repo.repo_name,
125 'source_refs_json': json.dumps(source_repo_data),
125 'source_refs_json': json.dumps(source_repo_data),
126 'target_repo_name': default_target_repo.repo_name,
126 'target_repo_name': default_target_repo.repo_name,
127 'target_refs_json': json.dumps(target_repo_data),
127 'target_refs_json': json.dumps(target_repo_data),
128 }
128 }
129 c.default_source_ref = selected_source_ref
129 c.default_source_ref = selected_source_ref
130
130
131 return render('/pullrequests/pullrequest.mako')
131 return render('/pullrequests/pullrequest.mako')
132
132
133 @LoginRequired()
133 @LoginRequired()
134 @NotAnonymous()
134 @NotAnonymous()
135 @XHRRequired()
135 @XHRRequired()
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 'repository.admin')
137 'repository.admin')
138 @jsonify
138 @jsonify
139 def get_repo_refs(self, repo_name, target_repo_name):
139 def get_repo_refs(self, repo_name, target_repo_name):
140 repo = Repository.get_by_repo_name(target_repo_name)
140 repo = Repository.get_by_repo_name(target_repo_name)
141 if not repo:
141 if not repo:
142 raise HTTPNotFound
142 raise HTTPNotFound
143 return PullRequestModel().generate_repo_data(repo)
143 return PullRequestModel().generate_repo_data(repo)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @XHRRequired()
147 @XHRRequired()
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 'repository.admin')
149 'repository.admin')
150 @jsonify
150 @jsonify
151 def get_repo_destinations(self, repo_name):
151 def get_repo_destinations(self, repo_name):
152 repo = Repository.get_by_repo_name(repo_name)
152 repo = Repository.get_by_repo_name(repo_name)
153 if not repo:
153 if not repo:
154 raise HTTPNotFound
154 raise HTTPNotFound
155 filter_query = request.GET.get('query')
155 filter_query = request.GET.get('query')
156
156
157 query = Repository.query() \
157 query = Repository.query() \
158 .order_by(func.length(Repository.repo_name)) \
158 .order_by(func.length(Repository.repo_name)) \
159 .filter(or_(
159 .filter(or_(
160 Repository.repo_name == repo.repo_name,
160 Repository.repo_name == repo.repo_name,
161 Repository.fork_id == repo.repo_id))
161 Repository.fork_id == repo.repo_id))
162
162
163 if filter_query:
163 if filter_query:
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 query = query.filter(
165 query = query.filter(
166 Repository.repo_name.ilike(ilike_expression))
166 Repository.repo_name.ilike(ilike_expression))
167
167
168 add_parent = False
168 add_parent = False
169 if repo.parent:
169 if repo.parent:
170 if filter_query in repo.parent.repo_name:
170 if filter_query in repo.parent.repo_name:
171 parent_vcs_obj = repo.parent.scm_instance()
171 parent_vcs_obj = repo.parent.scm_instance()
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 add_parent = True
173 add_parent = True
174
174
175 limit = 20 - 1 if add_parent else 20
175 limit = 20 - 1 if add_parent else 20
176 all_repos = query.limit(limit).all()
176 all_repos = query.limit(limit).all()
177 if add_parent:
177 if add_parent:
178 all_repos += [repo.parent]
178 all_repos += [repo.parent]
179
179
180 repos = []
180 repos = []
181 for obj in self.scm_model.get_repos(all_repos):
181 for obj in self.scm_model.get_repos(all_repos):
182 repos.append({
182 repos.append({
183 'id': obj['name'],
183 'id': obj['name'],
184 'text': obj['name'],
184 'text': obj['name'],
185 'type': 'repo',
185 'type': 'repo',
186 'obj': obj['dbrepo']
186 'obj': obj['dbrepo']
187 })
187 })
188
188
189 data = {
189 data = {
190 'more': False,
190 'more': False,
191 'results': [{
191 'results': [{
192 'text': _('Repositories'),
192 'text': _('Repositories'),
193 'children': repos
193 'children': repos
194 }] if repos else []
194 }] if repos else []
195 }
195 }
196 return data
196 return data
197
197
198 @LoginRequired()
198 @LoginRequired()
199 @NotAnonymous()
199 @NotAnonymous()
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 'repository.admin')
201 'repository.admin')
202 @HasAcceptedRepoType('git', 'hg')
202 @HasAcceptedRepoType('git', 'hg')
203 @auth.CSRFRequired()
203 @auth.CSRFRequired()
204 def create(self, repo_name):
204 def create(self, repo_name):
205 repo = Repository.get_by_repo_name(repo_name)
205 repo = Repository.get_by_repo_name(repo_name)
206 if not repo:
206 if not repo:
207 raise HTTPNotFound
207 raise HTTPNotFound
208
208
209 controls = peppercorn.parse(request.POST.items())
209 controls = peppercorn.parse(request.POST.items())
210
210
211 try:
211 try:
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 except formencode.Invalid as errors:
213 except formencode.Invalid as errors:
214 if errors.error_dict.get('revisions'):
214 if errors.error_dict.get('revisions'):
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 elif errors.error_dict.get('pullrequest_title'):
216 elif errors.error_dict.get('pullrequest_title'):
217 msg = _('Pull request requires a title with min. 3 chars')
217 msg = _('Pull request requires a title with min. 3 chars')
218 else:
218 else:
219 msg = _('Error creating pull request: {}').format(errors)
219 msg = _('Error creating pull request: {}').format(errors)
220 log.exception(msg)
220 log.exception(msg)
221 h.flash(msg, 'error')
221 h.flash(msg, 'error')
222
222
223 # would rather just go back to form ...
223 # would rather just go back to form ...
224 return redirect(url('pullrequest_home', repo_name=repo_name))
224 return redirect(url('pullrequest_home', repo_name=repo_name))
225
225
226 source_repo = _form['source_repo']
226 source_repo = _form['source_repo']
227 source_ref = _form['source_ref']
227 source_ref = _form['source_ref']
228 target_repo = _form['target_repo']
228 target_repo = _form['target_repo']
229 target_ref = _form['target_ref']
229 target_ref = _form['target_ref']
230 commit_ids = _form['revisions'][::-1]
230 commit_ids = _form['revisions'][::-1]
231 reviewers = [
232 (r['user_id'], r['reasons']) for r in _form['review_members']]
233
231
234 # find the ancestor for this pr
232 # find the ancestor for this pr
235 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
236 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
237
235
238 source_scm = source_db_repo.scm_instance()
236 source_scm = source_db_repo.scm_instance()
239 target_scm = target_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
240
238
241 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
242 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
243
241
244 ancestor = source_scm.get_common_ancestor(
242 ancestor = source_scm.get_common_ancestor(
245 source_commit.raw_id, target_commit.raw_id, target_scm)
243 source_commit.raw_id, target_commit.raw_id, target_scm)
246
244
247 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
248 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
249
247
250 pullrequest_title = _form['pullrequest_title']
248 pullrequest_title = _form['pullrequest_title']
251 title_source_ref = source_ref.split(':', 2)[1]
249 title_source_ref = source_ref.split(':', 2)[1]
252 if not pullrequest_title:
250 if not pullrequest_title:
253 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
254 source=source_repo,
252 source=source_repo,
255 source_ref=title_source_ref,
253 source_ref=title_source_ref,
256 target=target_repo
254 target=target_repo
257 )
255 )
258
256
259 description = _form['pullrequest_desc']
257 description = _form['pullrequest_desc']
258
259 get_default_reviewers_data, validate_default_reviewers = \
260 PullRequestModel().get_reviewer_functions()
261
262 # recalculate reviewers logic, to make sure we can validate this
263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user, source_db_repo, source_commit, target_db_repo,
265 target_commit)
266
267 reviewers = validate_default_reviewers(
268 _form['review_members'], reviewer_rules)
269
260 try:
270 try:
261 pull_request = PullRequestModel().create(
271 pull_request = PullRequestModel().create(
262 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
263 target_ref, commit_ids, reviewers, pullrequest_title,
273 target_ref, commit_ids, reviewers, pullrequest_title,
264 description
274 description, reviewer_rules
265 )
275 )
266 Session().commit()
276 Session().commit()
267 h.flash(_('Successfully opened new pull request'),
277 h.flash(_('Successfully opened new pull request'),
268 category='success')
278 category='success')
269 except Exception as e:
279 except Exception as e:
270 msg = _('Error occurred during sending pull request')
280 msg = _('Error occurred during creation of this pull request.')
271 log.exception(msg)
281 log.exception(msg)
272 h.flash(msg, category='error')
282 h.flash(msg, category='error')
273 return redirect(url('pullrequest_home', repo_name=repo_name))
283 return redirect(url('pullrequest_home', repo_name=repo_name))
274
284
275 return redirect(url('pullrequest_show', repo_name=target_repo,
285 return redirect(url('pullrequest_show', repo_name=target_repo,
276 pull_request_id=pull_request.pull_request_id))
286 pull_request_id=pull_request.pull_request_id))
277
287
278 @LoginRequired()
288 @LoginRequired()
279 @NotAnonymous()
289 @NotAnonymous()
280 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
281 'repository.admin')
291 'repository.admin')
282 @auth.CSRFRequired()
292 @auth.CSRFRequired()
283 @jsonify
293 @jsonify
284 def update(self, repo_name, pull_request_id):
294 def update(self, repo_name, pull_request_id):
285 pull_request_id = safe_int(pull_request_id)
295 pull_request_id = safe_int(pull_request_id)
286 pull_request = PullRequest.get_or_404(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
287 # only owner or admin can update it
297 # only owner or admin can update it
288 allowed_to_update = PullRequestModel().check_user_update(
298 allowed_to_update = PullRequestModel().check_user_update(
289 pull_request, c.rhodecode_user)
299 pull_request, c.rhodecode_user)
290 if allowed_to_update:
300 if allowed_to_update:
291 controls = peppercorn.parse(request.POST.items())
301 controls = peppercorn.parse(request.POST.items())
292
302
293 if 'review_members' in controls:
303 if 'review_members' in controls:
294 self._update_reviewers(
304 self._update_reviewers(
295 pull_request_id, controls['review_members'])
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
296 elif str2bool(request.POST.get('update_commits', 'false')):
307 elif str2bool(request.POST.get('update_commits', 'false')):
297 self._update_commits(pull_request)
308 self._update_commits(pull_request)
298 elif str2bool(request.POST.get('close_pull_request', 'false')):
309 elif str2bool(request.POST.get('close_pull_request', 'false')):
299 self._reject_close(pull_request)
310 self._reject_close(pull_request)
300 elif str2bool(request.POST.get('edit_pull_request', 'false')):
311 elif str2bool(request.POST.get('edit_pull_request', 'false')):
301 self._edit_pull_request(pull_request)
312 self._edit_pull_request(pull_request)
302 else:
313 else:
303 raise HTTPBadRequest()
314 raise HTTPBadRequest()
304 return True
315 return True
305 raise HTTPForbidden()
316 raise HTTPForbidden()
306
317
307 def _edit_pull_request(self, pull_request):
318 def _edit_pull_request(self, pull_request):
308 try:
319 try:
309 PullRequestModel().edit(
320 PullRequestModel().edit(
310 pull_request, request.POST.get('title'),
321 pull_request, request.POST.get('title'),
311 request.POST.get('description'))
322 request.POST.get('description'))
312 except ValueError:
323 except ValueError:
313 msg = _(u'Cannot update closed pull requests.')
324 msg = _(u'Cannot update closed pull requests.')
314 h.flash(msg, category='error')
325 h.flash(msg, category='error')
315 return
326 return
316 else:
327 else:
317 Session().commit()
328 Session().commit()
318
329
319 msg = _(u'Pull request title & description updated.')
330 msg = _(u'Pull request title & description updated.')
320 h.flash(msg, category='success')
331 h.flash(msg, category='success')
321 return
332 return
322
333
323 def _update_commits(self, pull_request):
334 def _update_commits(self, pull_request):
324 resp = PullRequestModel().update_commits(pull_request)
335 resp = PullRequestModel().update_commits(pull_request)
325
336
326 if resp.executed:
337 if resp.executed:
327
338
328 if resp.target_changed and resp.source_changed:
339 if resp.target_changed and resp.source_changed:
329 changed = 'target and source repositories'
340 changed = 'target and source repositories'
330 elif resp.target_changed and not resp.source_changed:
341 elif resp.target_changed and not resp.source_changed:
331 changed = 'target repository'
342 changed = 'target repository'
332 elif not resp.target_changed and resp.source_changed:
343 elif not resp.target_changed and resp.source_changed:
333 changed = 'source repository'
344 changed = 'source repository'
334 else:
345 else:
335 changed = 'nothing'
346 changed = 'nothing'
336
347
337 msg = _(
348 msg = _(
338 u'Pull request updated to "{source_commit_id}" with '
349 u'Pull request updated to "{source_commit_id}" with '
339 u'{count_added} added, {count_removed} removed commits. '
350 u'{count_added} added, {count_removed} removed commits. '
340 u'Source of changes: {change_source}')
351 u'Source of changes: {change_source}')
341 msg = msg.format(
352 msg = msg.format(
342 source_commit_id=pull_request.source_ref_parts.commit_id,
353 source_commit_id=pull_request.source_ref_parts.commit_id,
343 count_added=len(resp.changes.added),
354 count_added=len(resp.changes.added),
344 count_removed=len(resp.changes.removed),
355 count_removed=len(resp.changes.removed),
345 change_source=changed)
356 change_source=changed)
346 h.flash(msg, category='success')
357 h.flash(msg, category='success')
347
358
348 registry = get_current_registry()
359 registry = get_current_registry()
349 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
360 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
350 channelstream_config = rhodecode_plugins.get('channelstream', {})
361 channelstream_config = rhodecode_plugins.get('channelstream', {})
351 if channelstream_config.get('enabled'):
362 if channelstream_config.get('enabled'):
352 message = msg + (
363 message = msg + (
353 ' - <a onclick="window.location.reload()">'
364 ' - <a onclick="window.location.reload()">'
354 '<strong>{}</strong></a>'.format(_('Reload page')))
365 '<strong>{}</strong></a>'.format(_('Reload page')))
355 channel = '/repo${}$/pr/{}'.format(
366 channel = '/repo${}$/pr/{}'.format(
356 pull_request.target_repo.repo_name,
367 pull_request.target_repo.repo_name,
357 pull_request.pull_request_id
368 pull_request.pull_request_id
358 )
369 )
359 payload = {
370 payload = {
360 'type': 'message',
371 'type': 'message',
361 'user': 'system',
372 'user': 'system',
362 'exclude_users': [request.user.username],
373 'exclude_users': [request.user.username],
363 'channel': channel,
374 'channel': channel,
364 'message': {
375 'message': {
365 'message': message,
376 'message': message,
366 'level': 'success',
377 'level': 'success',
367 'topic': '/notifications'
378 'topic': '/notifications'
368 }
379 }
369 }
380 }
370 channelstream_request(
381 channelstream_request(
371 channelstream_config, [payload], '/message',
382 channelstream_config, [payload], '/message',
372 raise_exc=False)
383 raise_exc=False)
373 else:
384 else:
374 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
385 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
375 warning_reasons = [
386 warning_reasons = [
376 UpdateFailureReason.NO_CHANGE,
387 UpdateFailureReason.NO_CHANGE,
377 UpdateFailureReason.WRONG_REF_TYPE,
388 UpdateFailureReason.WRONG_REF_TYPE,
378 ]
389 ]
379 category = 'warning' if resp.reason in warning_reasons else 'error'
390 category = 'warning' if resp.reason in warning_reasons else 'error'
380 h.flash(msg, category=category)
391 h.flash(msg, category=category)
381
392
382 @auth.CSRFRequired()
393 @auth.CSRFRequired()
383 @LoginRequired()
394 @LoginRequired()
384 @NotAnonymous()
395 @NotAnonymous()
385 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
386 'repository.admin')
397 'repository.admin')
387 def merge(self, repo_name, pull_request_id):
398 def merge(self, repo_name, pull_request_id):
388 """
399 """
389 POST /{repo_name}/pull-request/{pull_request_id}
400 POST /{repo_name}/pull-request/{pull_request_id}
390
401
391 Merge will perform a server-side merge of the specified
402 Merge will perform a server-side merge of the specified
392 pull request, if the pull request is approved and mergeable.
403 pull request, if the pull request is approved and mergeable.
393 After successful merging, the pull request is automatically
404 After successful merging, the pull request is automatically
394 closed, with a relevant comment.
405 closed, with a relevant comment.
395 """
406 """
396 pull_request_id = safe_int(pull_request_id)
407 pull_request_id = safe_int(pull_request_id)
397 pull_request = PullRequest.get_or_404(pull_request_id)
408 pull_request = PullRequest.get_or_404(pull_request_id)
398 user = c.rhodecode_user
409 user = c.rhodecode_user
399
410
400 check = MergeCheck.validate(pull_request, user)
411 check = MergeCheck.validate(pull_request, user)
401 merge_possible = not check.failed
412 merge_possible = not check.failed
402
413
403 for err_type, error_msg in check.errors:
414 for err_type, error_msg in check.errors:
404 h.flash(error_msg, category=err_type)
415 h.flash(error_msg, category=err_type)
405
416
406 if merge_possible:
417 if merge_possible:
407 log.debug("Pre-conditions checked, trying to merge.")
418 log.debug("Pre-conditions checked, trying to merge.")
408 extras = vcs_operation_context(
419 extras = vcs_operation_context(
409 request.environ, repo_name=pull_request.target_repo.repo_name,
420 request.environ, repo_name=pull_request.target_repo.repo_name,
410 username=user.username, action='push',
421 username=user.username, action='push',
411 scm=pull_request.target_repo.repo_type)
422 scm=pull_request.target_repo.repo_type)
412 self._merge_pull_request(pull_request, user, extras)
423 self._merge_pull_request(pull_request, user, extras)
413
424
414 return redirect(url(
425 return redirect(url(
415 'pullrequest_show',
426 'pullrequest_show',
416 repo_name=pull_request.target_repo.repo_name,
427 repo_name=pull_request.target_repo.repo_name,
417 pull_request_id=pull_request.pull_request_id))
428 pull_request_id=pull_request.pull_request_id))
418
429
419 def _merge_pull_request(self, pull_request, user, extras):
430 def _merge_pull_request(self, pull_request, user, extras):
420 merge_resp = PullRequestModel().merge(
431 merge_resp = PullRequestModel().merge(
421 pull_request, user, extras=extras)
432 pull_request, user, extras=extras)
422
433
423 if merge_resp.executed:
434 if merge_resp.executed:
424 log.debug("The merge was successful, closing the pull request.")
435 log.debug("The merge was successful, closing the pull request.")
425 PullRequestModel().close_pull_request(
436 PullRequestModel().close_pull_request(
426 pull_request.pull_request_id, user)
437 pull_request.pull_request_id, user)
427 Session().commit()
438 Session().commit()
428 msg = _('Pull request was successfully merged and closed.')
439 msg = _('Pull request was successfully merged and closed.')
429 h.flash(msg, category='success')
440 h.flash(msg, category='success')
430 else:
441 else:
431 log.debug(
442 log.debug(
432 "The merge was not successful. Merge response: %s",
443 "The merge was not successful. Merge response: %s",
433 merge_resp)
444 merge_resp)
434 msg = PullRequestModel().merge_status_message(
445 msg = PullRequestModel().merge_status_message(
435 merge_resp.failure_reason)
446 merge_resp.failure_reason)
436 h.flash(msg, category='error')
447 h.flash(msg, category='error')
437
448
438 def _update_reviewers(self, pull_request_id, review_members):
449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
439 reviewers = [
450
440 (int(r['user_id']), r['reasons']) for r in review_members]
451 get_default_reviewers_data, validate_default_reviewers = \
452 PullRequestModel().get_reviewer_functions()
453
454 try:
455 reviewers = validate_default_reviewers(review_members, reviewer_rules)
456 except ValueError as e:
457 log.error('Reviewers Validation:{}'.format(e))
458 h.flash(e, category='error')
459 return
460
441 PullRequestModel().update_reviewers(pull_request_id, reviewers)
461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
462 h.flash(_('Pull request reviewers updated.'), category='success')
442 Session().commit()
463 Session().commit()
443
464
444 def _reject_close(self, pull_request):
465 def _reject_close(self, pull_request):
445 if pull_request.is_closed():
466 if pull_request.is_closed():
446 raise HTTPForbidden()
467 raise HTTPForbidden()
447
468
448 PullRequestModel().close_pull_request_with_comment(
469 PullRequestModel().close_pull_request_with_comment(
449 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
470 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
450 Session().commit()
471 Session().commit()
451
472
452 @LoginRequired()
473 @LoginRequired()
453 @NotAnonymous()
474 @NotAnonymous()
454 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
475 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
455 'repository.admin')
476 'repository.admin')
456 @auth.CSRFRequired()
477 @auth.CSRFRequired()
457 @jsonify
478 @jsonify
458 def delete(self, repo_name, pull_request_id):
479 def delete(self, repo_name, pull_request_id):
459 pull_request_id = safe_int(pull_request_id)
480 pull_request_id = safe_int(pull_request_id)
460 pull_request = PullRequest.get_or_404(pull_request_id)
481 pull_request = PullRequest.get_or_404(pull_request_id)
461
482
462 pr_closed = pull_request.is_closed()
483 pr_closed = pull_request.is_closed()
463 allowed_to_delete = PullRequestModel().check_user_delete(
484 allowed_to_delete = PullRequestModel().check_user_delete(
464 pull_request, c.rhodecode_user) and not pr_closed
485 pull_request, c.rhodecode_user) and not pr_closed
465
486
466 # only owner can delete it !
487 # only owner can delete it !
467 if allowed_to_delete:
488 if allowed_to_delete:
468 PullRequestModel().delete(pull_request)
489 PullRequestModel().delete(pull_request)
469 Session().commit()
490 Session().commit()
470 h.flash(_('Successfully deleted pull request'),
491 h.flash(_('Successfully deleted pull request'),
471 category='success')
492 category='success')
472 return redirect(url('my_account_pullrequests'))
493 return redirect(url('my_account_pullrequests'))
473
494
474 h.flash(_('Your are not allowed to delete this pull request'),
495 h.flash(_('Your are not allowed to delete this pull request'),
475 category='error')
496 category='error')
476 raise HTTPForbidden()
497 raise HTTPForbidden()
477
498
478 def _get_pr_version(self, pull_request_id, version=None):
499 def _get_pr_version(self, pull_request_id, version=None):
479 pull_request_id = safe_int(pull_request_id)
500 pull_request_id = safe_int(pull_request_id)
480 at_version = None
501 at_version = None
481
502
482 if version and version == 'latest':
503 if version and version == 'latest':
483 pull_request_ver = PullRequest.get(pull_request_id)
504 pull_request_ver = PullRequest.get(pull_request_id)
484 pull_request_obj = pull_request_ver
505 pull_request_obj = pull_request_ver
485 _org_pull_request_obj = pull_request_obj
506 _org_pull_request_obj = pull_request_obj
486 at_version = 'latest'
507 at_version = 'latest'
487 elif version:
508 elif version:
488 pull_request_ver = PullRequestVersion.get_or_404(version)
509 pull_request_ver = PullRequestVersion.get_or_404(version)
489 pull_request_obj = pull_request_ver
510 pull_request_obj = pull_request_ver
490 _org_pull_request_obj = pull_request_ver.pull_request
511 _org_pull_request_obj = pull_request_ver.pull_request
491 at_version = pull_request_ver.pull_request_version_id
512 at_version = pull_request_ver.pull_request_version_id
492 else:
513 else:
493 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
514 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
515 pull_request_id)
494
516
495 pull_request_display_obj = PullRequest.get_pr_display_object(
517 pull_request_display_obj = PullRequest.get_pr_display_object(
496 pull_request_obj, _org_pull_request_obj)
518 pull_request_obj, _org_pull_request_obj)
497
519
498 return _org_pull_request_obj, pull_request_obj, \
520 return _org_pull_request_obj, pull_request_obj, \
499 pull_request_display_obj, at_version
521 pull_request_display_obj, at_version
500
522
501 def _get_diffset(
523 def _get_diffset(
502 self, source_repo, source_ref_id, target_ref_id, target_commit,
524 self, source_repo, source_ref_id, target_ref_id, target_commit,
503 source_commit, diff_limit, file_limit, display_inline_comments):
525 source_commit, diff_limit, file_limit, display_inline_comments):
504 vcs_diff = PullRequestModel().get_diff(
526 vcs_diff = PullRequestModel().get_diff(
505 source_repo, source_ref_id, target_ref_id)
527 source_repo, source_ref_id, target_ref_id)
506
528
507 diff_processor = diffs.DiffProcessor(
529 diff_processor = diffs.DiffProcessor(
508 vcs_diff, format='newdiff', diff_limit=diff_limit,
530 vcs_diff, format='newdiff', diff_limit=diff_limit,
509 file_limit=file_limit, show_full_diff=c.fulldiff)
531 file_limit=file_limit, show_full_diff=c.fulldiff)
510
532
511 _parsed = diff_processor.prepare()
533 _parsed = diff_processor.prepare()
512
534
513 def _node_getter(commit):
535 def _node_getter(commit):
514 def get_node(fname):
536 def get_node(fname):
515 try:
537 try:
516 return commit.get_node(fname)
538 return commit.get_node(fname)
517 except NodeDoesNotExistError:
539 except NodeDoesNotExistError:
518 return None
540 return None
519
541
520 return get_node
542 return get_node
521
543
522 diffset = codeblocks.DiffSet(
544 diffset = codeblocks.DiffSet(
523 repo_name=c.repo_name,
545 repo_name=c.repo_name,
524 source_repo_name=c.source_repo.repo_name,
546 source_repo_name=c.source_repo.repo_name,
525 source_node_getter=_node_getter(target_commit),
547 source_node_getter=_node_getter(target_commit),
526 target_node_getter=_node_getter(source_commit),
548 target_node_getter=_node_getter(source_commit),
527 comments=display_inline_comments
549 comments=display_inline_comments
528 )
550 )
529 diffset = diffset.render_patchset(
551 diffset = diffset.render_patchset(
530 _parsed, target_commit.raw_id, source_commit.raw_id)
552 _parsed, target_commit.raw_id, source_commit.raw_id)
531
553
532 return diffset
554 return diffset
533
555
534 @LoginRequired()
556 @LoginRequired()
535 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
557 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 'repository.admin')
558 'repository.admin')
537 def show(self, repo_name, pull_request_id):
559 def show(self, repo_name, pull_request_id):
538 pull_request_id = safe_int(pull_request_id)
560 pull_request_id = safe_int(pull_request_id)
539 version = request.GET.get('version')
561 version = request.GET.get('version')
540 from_version = request.GET.get('from_version') or version
562 from_version = request.GET.get('from_version') or version
541 merge_checks = request.GET.get('merge_checks')
563 merge_checks = request.GET.get('merge_checks')
542 c.fulldiff = str2bool(request.GET.get('fulldiff'))
564 c.fulldiff = str2bool(request.GET.get('fulldiff'))
543
565
544 (pull_request_latest,
566 (pull_request_latest,
545 pull_request_at_ver,
567 pull_request_at_ver,
546 pull_request_display_obj,
568 pull_request_display_obj,
547 at_version) = self._get_pr_version(
569 at_version) = self._get_pr_version(
548 pull_request_id, version=version)
570 pull_request_id, version=version)
549 pr_closed = pull_request_latest.is_closed()
571 pr_closed = pull_request_latest.is_closed()
550
572
551 if pr_closed and (version or from_version):
573 if pr_closed and (version or from_version):
552 # not allow to browse versions
574 # not allow to browse versions
553 return redirect(h.url('pullrequest_show', repo_name=repo_name,
575 return redirect(h.url('pullrequest_show', repo_name=repo_name,
554 pull_request_id=pull_request_id))
576 pull_request_id=pull_request_id))
555
577
556 versions = pull_request_display_obj.versions()
578 versions = pull_request_display_obj.versions()
557
579
558 c.at_version = at_version
580 c.at_version = at_version
559 c.at_version_num = (at_version
581 c.at_version_num = (at_version
560 if at_version and at_version != 'latest'
582 if at_version and at_version != 'latest'
561 else None)
583 else None)
562 c.at_version_pos = ChangesetComment.get_index_from_version(
584 c.at_version_pos = ChangesetComment.get_index_from_version(
563 c.at_version_num, versions)
585 c.at_version_num, versions)
564
586
565 (prev_pull_request_latest,
587 (prev_pull_request_latest,
566 prev_pull_request_at_ver,
588 prev_pull_request_at_ver,
567 prev_pull_request_display_obj,
589 prev_pull_request_display_obj,
568 prev_at_version) = self._get_pr_version(
590 prev_at_version) = self._get_pr_version(
569 pull_request_id, version=from_version)
591 pull_request_id, version=from_version)
570
592
571 c.from_version = prev_at_version
593 c.from_version = prev_at_version
572 c.from_version_num = (prev_at_version
594 c.from_version_num = (prev_at_version
573 if prev_at_version and prev_at_version != 'latest'
595 if prev_at_version and prev_at_version != 'latest'
574 else None)
596 else None)
575 c.from_version_pos = ChangesetComment.get_index_from_version(
597 c.from_version_pos = ChangesetComment.get_index_from_version(
576 c.from_version_num, versions)
598 c.from_version_num, versions)
577
599
578 # define if we're in COMPARE mode or VIEW at version mode
600 # define if we're in COMPARE mode or VIEW at version mode
579 compare = at_version != prev_at_version
601 compare = at_version != prev_at_version
580
602
581 # pull_requests repo_name we opened it against
603 # pull_requests repo_name we opened it against
582 # ie. target_repo must match
604 # ie. target_repo must match
583 if repo_name != pull_request_at_ver.target_repo.repo_name:
605 if repo_name != pull_request_at_ver.target_repo.repo_name:
584 raise HTTPNotFound
606 raise HTTPNotFound
585
607
586 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
608 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
587 pull_request_at_ver)
609 pull_request_at_ver)
588
610
589 c.pull_request = pull_request_display_obj
611 c.pull_request = pull_request_display_obj
590 c.pull_request_latest = pull_request_latest
612 c.pull_request_latest = pull_request_latest
591
613
592 if compare or (at_version and not at_version == 'latest'):
614 if compare or (at_version and not at_version == 'latest'):
593 c.allowed_to_change_status = False
615 c.allowed_to_change_status = False
594 c.allowed_to_update = False
616 c.allowed_to_update = False
595 c.allowed_to_merge = False
617 c.allowed_to_merge = False
596 c.allowed_to_delete = False
618 c.allowed_to_delete = False
597 c.allowed_to_comment = False
619 c.allowed_to_comment = False
598 c.allowed_to_close = False
620 c.allowed_to_close = False
599 else:
621 else:
600 c.allowed_to_change_status = PullRequestModel(). \
622 can_change_status = PullRequestModel().check_user_change_status(
601 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
623 pull_request_at_ver, c.rhodecode_user)
602 and not pr_closed
624 c.allowed_to_change_status = can_change_status and not pr_closed
603
625
604 c.allowed_to_update = PullRequestModel().check_user_update(
626 c.allowed_to_update = PullRequestModel().check_user_update(
605 pull_request_latest, c.rhodecode_user) and not pr_closed
627 pull_request_latest, c.rhodecode_user) and not pr_closed
606 c.allowed_to_merge = PullRequestModel().check_user_merge(
628 c.allowed_to_merge = PullRequestModel().check_user_merge(
607 pull_request_latest, c.rhodecode_user) and not pr_closed
629 pull_request_latest, c.rhodecode_user) and not pr_closed
608 c.allowed_to_delete = PullRequestModel().check_user_delete(
630 c.allowed_to_delete = PullRequestModel().check_user_delete(
609 pull_request_latest, c.rhodecode_user) and not pr_closed
631 pull_request_latest, c.rhodecode_user) and not pr_closed
610 c.allowed_to_comment = not pr_closed
632 c.allowed_to_comment = not pr_closed
611 c.allowed_to_close = c.allowed_to_merge and not pr_closed
633 c.allowed_to_close = c.allowed_to_merge and not pr_closed
612
634
635 c.forbid_adding_reviewers = False
636 c.forbid_author_to_review = False
637
638 if pull_request_latest.reviewer_data and \
639 'rules' in pull_request_latest.reviewer_data:
640 rules = pull_request_latest.reviewer_data['rules'] or {}
641 try:
642 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
643 c.forbid_author_to_review = rules.get('forbid_author_to_review')
644 except Exception:
645 pass
646
613 # check merge capabilities
647 # check merge capabilities
614 _merge_check = MergeCheck.validate(
648 _merge_check = MergeCheck.validate(
615 pull_request_latest, user=c.rhodecode_user)
649 pull_request_latest, user=c.rhodecode_user)
616 c.pr_merge_errors = _merge_check.error_details
650 c.pr_merge_errors = _merge_check.error_details
617 c.pr_merge_possible = not _merge_check.failed
651 c.pr_merge_possible = not _merge_check.failed
618 c.pr_merge_message = _merge_check.merge_msg
652 c.pr_merge_message = _merge_check.merge_msg
619
653
620 c.pull_request_review_status = _merge_check.review_status
654 c.pull_request_review_status = _merge_check.review_status
621 if merge_checks:
655 if merge_checks:
622 return render('/pullrequests/pullrequest_merge_checks.mako')
656 return render('/pullrequests/pullrequest_merge_checks.mako')
623
657
624 comments_model = CommentsModel()
658 comments_model = CommentsModel()
625
659
626 # reviewers and statuses
660 # reviewers and statuses
627 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
661 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
628 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
662 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
629
663
630 # GENERAL COMMENTS with versions #
664 # GENERAL COMMENTS with versions #
631 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
665 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
632 q = q.order_by(ChangesetComment.comment_id.asc())
666 q = q.order_by(ChangesetComment.comment_id.asc())
633 general_comments = q
667 general_comments = q
634
668
635 # pick comments we want to render at current version
669 # pick comments we want to render at current version
636 c.comment_versions = comments_model.aggregate_comments(
670 c.comment_versions = comments_model.aggregate_comments(
637 general_comments, versions, c.at_version_num)
671 general_comments, versions, c.at_version_num)
638 c.comments = c.comment_versions[c.at_version_num]['until']
672 c.comments = c.comment_versions[c.at_version_num]['until']
639
673
640 # INLINE COMMENTS with versions #
674 # INLINE COMMENTS with versions #
641 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
675 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
642 q = q.order_by(ChangesetComment.comment_id.asc())
676 q = q.order_by(ChangesetComment.comment_id.asc())
643 inline_comments = q
677 inline_comments = q
644
678
645 c.inline_versions = comments_model.aggregate_comments(
679 c.inline_versions = comments_model.aggregate_comments(
646 inline_comments, versions, c.at_version_num, inline=True)
680 inline_comments, versions, c.at_version_num, inline=True)
647
681
648 # inject latest version
682 # inject latest version
649 latest_ver = PullRequest.get_pr_display_object(
683 latest_ver = PullRequest.get_pr_display_object(
650 pull_request_latest, pull_request_latest)
684 pull_request_latest, pull_request_latest)
651
685
652 c.versions = versions + [latest_ver]
686 c.versions = versions + [latest_ver]
653
687
654 # if we use version, then do not show later comments
688 # if we use version, then do not show later comments
655 # than current version
689 # than current version
656 display_inline_comments = collections.defaultdict(
690 display_inline_comments = collections.defaultdict(
657 lambda: collections.defaultdict(list))
691 lambda: collections.defaultdict(list))
658 for co in inline_comments:
692 for co in inline_comments:
659 if c.at_version_num:
693 if c.at_version_num:
660 # pick comments that are at least UPTO given version, so we
694 # pick comments that are at least UPTO given version, so we
661 # don't render comments for higher version
695 # don't render comments for higher version
662 should_render = co.pull_request_version_id and \
696 should_render = co.pull_request_version_id and \
663 co.pull_request_version_id <= c.at_version_num
697 co.pull_request_version_id <= c.at_version_num
664 else:
698 else:
665 # showing all, for 'latest'
699 # showing all, for 'latest'
666 should_render = True
700 should_render = True
667
701
668 if should_render:
702 if should_render:
669 display_inline_comments[co.f_path][co.line_no].append(co)
703 display_inline_comments[co.f_path][co.line_no].append(co)
670
704
671 # load diff data into template context, if we use compare mode then
705 # load diff data into template context, if we use compare mode then
672 # diff is calculated based on changes between versions of PR
706 # diff is calculated based on changes between versions of PR
673
707
674 source_repo = pull_request_at_ver.source_repo
708 source_repo = pull_request_at_ver.source_repo
675 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
709 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
676
710
677 target_repo = pull_request_at_ver.target_repo
711 target_repo = pull_request_at_ver.target_repo
678 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
712 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
679
713
680 if compare:
714 if compare:
681 # in compare switch the diff base to latest commit from prev version
715 # in compare switch the diff base to latest commit from prev version
682 target_ref_id = prev_pull_request_display_obj.revisions[0]
716 target_ref_id = prev_pull_request_display_obj.revisions[0]
683
717
684 # despite opening commits for bookmarks/branches/tags, we always
718 # despite opening commits for bookmarks/branches/tags, we always
685 # convert this to rev to prevent changes after bookmark or branch change
719 # convert this to rev to prevent changes after bookmark or branch change
686 c.source_ref_type = 'rev'
720 c.source_ref_type = 'rev'
687 c.source_ref = source_ref_id
721 c.source_ref = source_ref_id
688
722
689 c.target_ref_type = 'rev'
723 c.target_ref_type = 'rev'
690 c.target_ref = target_ref_id
724 c.target_ref = target_ref_id
691
725
692 c.source_repo = source_repo
726 c.source_repo = source_repo
693 c.target_repo = target_repo
727 c.target_repo = target_repo
694
728
695 # diff_limit is the old behavior, will cut off the whole diff
729 # diff_limit is the old behavior, will cut off the whole diff
696 # if the limit is applied otherwise will just hide the
730 # if the limit is applied otherwise will just hide the
697 # big files from the front-end
731 # big files from the front-end
698 diff_limit = self.cut_off_limit_diff
732 diff_limit = self.cut_off_limit_diff
699 file_limit = self.cut_off_limit_file
733 file_limit = self.cut_off_limit_file
700
734
701 c.commit_ranges = []
735 c.commit_ranges = []
702 source_commit = EmptyCommit()
736 source_commit = EmptyCommit()
703 target_commit = EmptyCommit()
737 target_commit = EmptyCommit()
704 c.missing_requirements = False
738 c.missing_requirements = False
705
739
706 source_scm = source_repo.scm_instance()
740 source_scm = source_repo.scm_instance()
707 target_scm = target_repo.scm_instance()
741 target_scm = target_repo.scm_instance()
708
742
709 # try first shadow repo, fallback to regular repo
743 # try first shadow repo, fallback to regular repo
710 try:
744 try:
711 commits_source_repo = pull_request_latest.get_shadow_repo()
745 commits_source_repo = pull_request_latest.get_shadow_repo()
712 except Exception:
746 except Exception:
713 log.debug('Failed to get shadow repo', exc_info=True)
747 log.debug('Failed to get shadow repo', exc_info=True)
714 commits_source_repo = source_scm
748 commits_source_repo = source_scm
715
749
716 c.commits_source_repo = commits_source_repo
750 c.commits_source_repo = commits_source_repo
717 commit_cache = {}
751 commit_cache = {}
718 try:
752 try:
719 pre_load = ["author", "branch", "date", "message"]
753 pre_load = ["author", "branch", "date", "message"]
720 show_revs = pull_request_at_ver.revisions
754 show_revs = pull_request_at_ver.revisions
721 for rev in show_revs:
755 for rev in show_revs:
722 comm = commits_source_repo.get_commit(
756 comm = commits_source_repo.get_commit(
723 commit_id=rev, pre_load=pre_load)
757 commit_id=rev, pre_load=pre_load)
724 c.commit_ranges.append(comm)
758 c.commit_ranges.append(comm)
725 commit_cache[comm.raw_id] = comm
759 commit_cache[comm.raw_id] = comm
726
760
761 # Order here matters, we first need to get target, and then
762 # the source
727 target_commit = commits_source_repo.get_commit(
763 target_commit = commits_source_repo.get_commit(
728 commit_id=safe_str(target_ref_id))
764 commit_id=safe_str(target_ref_id))
765
729 source_commit = commits_source_repo.get_commit(
766 source_commit = commits_source_repo.get_commit(
730 commit_id=safe_str(source_ref_id))
767 commit_id=safe_str(source_ref_id))
768
731 except CommitDoesNotExistError:
769 except CommitDoesNotExistError:
732 pass
770 log.warning(
771 'Failed to get commit from `{}` repo'.format(
772 commits_source_repo), exc_info=True)
733 except RepositoryRequirementError:
773 except RepositoryRequirementError:
734 log.warning(
774 log.warning(
735 'Failed to get all required data from repo', exc_info=True)
775 'Failed to get all required data from repo', exc_info=True)
736 c.missing_requirements = True
776 c.missing_requirements = True
737
777
738 c.ancestor = None # set it to None, to hide it from PR view
778 c.ancestor = None # set it to None, to hide it from PR view
739
779
740 try:
780 try:
741 ancestor_id = source_scm.get_common_ancestor(
781 ancestor_id = source_scm.get_common_ancestor(
742 source_commit.raw_id, target_commit.raw_id, target_scm)
782 source_commit.raw_id, target_commit.raw_id, target_scm)
743 c.ancestor_commit = source_scm.get_commit(ancestor_id)
783 c.ancestor_commit = source_scm.get_commit(ancestor_id)
744 except Exception:
784 except Exception:
745 c.ancestor_commit = None
785 c.ancestor_commit = None
746
786
747 c.statuses = source_repo.statuses(
787 c.statuses = source_repo.statuses(
748 [x.raw_id for x in c.commit_ranges])
788 [x.raw_id for x in c.commit_ranges])
749
789
750 # auto collapse if we have more than limit
790 # auto collapse if we have more than limit
751 collapse_limit = diffs.DiffProcessor._collapse_commits_over
791 collapse_limit = diffs.DiffProcessor._collapse_commits_over
752 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
792 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
753 c.compare_mode = compare
793 c.compare_mode = compare
754
794
755 c.missing_commits = False
795 c.missing_commits = False
756 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
796 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
757 or source_commit == target_commit):
797 or source_commit == target_commit):
758
798
759 c.missing_commits = True
799 c.missing_commits = True
760 else:
800 else:
761
801
762 c.diffset = self._get_diffset(
802 c.diffset = self._get_diffset(
763 commits_source_repo, source_ref_id, target_ref_id,
803 commits_source_repo, source_ref_id, target_ref_id,
764 target_commit, source_commit,
804 target_commit, source_commit,
765 diff_limit, file_limit, display_inline_comments)
805 diff_limit, file_limit, display_inline_comments)
766
806
767 c.limited_diff = c.diffset.limited_diff
807 c.limited_diff = c.diffset.limited_diff
768
808
769 # calculate removed files that are bound to comments
809 # calculate removed files that are bound to comments
770 comment_deleted_files = [
810 comment_deleted_files = [
771 fname for fname in display_inline_comments
811 fname for fname in display_inline_comments
772 if fname not in c.diffset.file_stats]
812 if fname not in c.diffset.file_stats]
773
813
774 c.deleted_files_comments = collections.defaultdict(dict)
814 c.deleted_files_comments = collections.defaultdict(dict)
775 for fname, per_line_comments in display_inline_comments.items():
815 for fname, per_line_comments in display_inline_comments.items():
776 if fname in comment_deleted_files:
816 if fname in comment_deleted_files:
777 c.deleted_files_comments[fname]['stats'] = 0
817 c.deleted_files_comments[fname]['stats'] = 0
778 c.deleted_files_comments[fname]['comments'] = list()
818 c.deleted_files_comments[fname]['comments'] = list()
779 for lno, comments in per_line_comments.items():
819 for lno, comments in per_line_comments.items():
780 c.deleted_files_comments[fname]['comments'].extend(
820 c.deleted_files_comments[fname]['comments'].extend(
781 comments)
821 comments)
782
822
783 # this is a hack to properly display links, when creating PR, the
823 # this is a hack to properly display links, when creating PR, the
784 # compare view and others uses different notation, and
824 # compare view and others uses different notation, and
785 # compare_commits.mako renders links based on the target_repo.
825 # compare_commits.mako renders links based on the target_repo.
786 # We need to swap that here to generate it properly on the html side
826 # We need to swap that here to generate it properly on the html side
787 c.target_repo = c.source_repo
827 c.target_repo = c.source_repo
788
828
789 c.commit_statuses = ChangesetStatus.STATUSES
829 c.commit_statuses = ChangesetStatus.STATUSES
790
830
791 c.show_version_changes = not pr_closed
831 c.show_version_changes = not pr_closed
792 if c.show_version_changes:
832 if c.show_version_changes:
793 cur_obj = pull_request_at_ver
833 cur_obj = pull_request_at_ver
794 prev_obj = prev_pull_request_at_ver
834 prev_obj = prev_pull_request_at_ver
795
835
796 old_commit_ids = prev_obj.revisions
836 old_commit_ids = prev_obj.revisions
797 new_commit_ids = cur_obj.revisions
837 new_commit_ids = cur_obj.revisions
798 commit_changes = PullRequestModel()._calculate_commit_id_changes(
838 commit_changes = PullRequestModel()._calculate_commit_id_changes(
799 old_commit_ids, new_commit_ids)
839 old_commit_ids, new_commit_ids)
800 c.commit_changes_summary = commit_changes
840 c.commit_changes_summary = commit_changes
801
841
802 # calculate the diff for commits between versions
842 # calculate the diff for commits between versions
803 c.commit_changes = []
843 c.commit_changes = []
804 mark = lambda cs, fw: list(
844 mark = lambda cs, fw: list(
805 h.itertools.izip_longest([], cs, fillvalue=fw))
845 h.itertools.izip_longest([], cs, fillvalue=fw))
806 for c_type, raw_id in mark(commit_changes.added, 'a') \
846 for c_type, raw_id in mark(commit_changes.added, 'a') \
807 + mark(commit_changes.removed, 'r') \
847 + mark(commit_changes.removed, 'r') \
808 + mark(commit_changes.common, 'c'):
848 + mark(commit_changes.common, 'c'):
809
849
810 if raw_id in commit_cache:
850 if raw_id in commit_cache:
811 commit = commit_cache[raw_id]
851 commit = commit_cache[raw_id]
812 else:
852 else:
813 try:
853 try:
814 commit = commits_source_repo.get_commit(raw_id)
854 commit = commits_source_repo.get_commit(raw_id)
815 except CommitDoesNotExistError:
855 except CommitDoesNotExistError:
816 # in case we fail extracting still use "dummy" commit
856 # in case we fail extracting still use "dummy" commit
817 # for display in commit diff
857 # for display in commit diff
818 commit = h.AttributeDict(
858 commit = h.AttributeDict(
819 {'raw_id': raw_id,
859 {'raw_id': raw_id,
820 'message': 'EMPTY or MISSING COMMIT'})
860 'message': 'EMPTY or MISSING COMMIT'})
821 c.commit_changes.append([c_type, commit])
861 c.commit_changes.append([c_type, commit])
822
862
823 # current user review statuses for each version
863 # current user review statuses for each version
824 c.review_versions = {}
864 c.review_versions = {}
825 if c.rhodecode_user.user_id in allowed_reviewers:
865 if c.rhodecode_user.user_id in allowed_reviewers:
826 for co in general_comments:
866 for co in general_comments:
827 if co.author.user_id == c.rhodecode_user.user_id:
867 if co.author.user_id == c.rhodecode_user.user_id:
828 # each comment has a status change
868 # each comment has a status change
829 status = co.status_change
869 status = co.status_change
830 if status:
870 if status:
831 _ver_pr = status[0].comment.pull_request_version_id
871 _ver_pr = status[0].comment.pull_request_version_id
832 c.review_versions[_ver_pr] = status[0]
872 c.review_versions[_ver_pr] = status[0]
833
873
834 return render('/pullrequests/pullrequest_show.mako')
874 return render('/pullrequests/pullrequest_show.mako')
835
875
836 @LoginRequired()
876 @LoginRequired()
837 @NotAnonymous()
877 @NotAnonymous()
838 @HasRepoPermissionAnyDecorator(
878 @HasRepoPermissionAnyDecorator(
839 'repository.read', 'repository.write', 'repository.admin')
879 'repository.read', 'repository.write', 'repository.admin')
840 @auth.CSRFRequired()
880 @auth.CSRFRequired()
841 @jsonify
881 @jsonify
842 def comment(self, repo_name, pull_request_id):
882 def comment(self, repo_name, pull_request_id):
843 pull_request_id = safe_int(pull_request_id)
883 pull_request_id = safe_int(pull_request_id)
844 pull_request = PullRequest.get_or_404(pull_request_id)
884 pull_request = PullRequest.get_or_404(pull_request_id)
845 if pull_request.is_closed():
885 if pull_request.is_closed():
846 raise HTTPForbidden()
886 raise HTTPForbidden()
847
887
848 status = request.POST.get('changeset_status', None)
888 status = request.POST.get('changeset_status', None)
849 text = request.POST.get('text')
889 text = request.POST.get('text')
850 comment_type = request.POST.get('comment_type')
890 comment_type = request.POST.get('comment_type')
851 resolves_comment_id = request.POST.get('resolves_comment_id', None)
891 resolves_comment_id = request.POST.get('resolves_comment_id', None)
852 close_pull_request = request.POST.get('close_pull_request')
892 close_pull_request = request.POST.get('close_pull_request')
853
893
854 close_pr = False
894 close_pr = False
855 # only owner or admin or person with write permissions
895 # only owner or admin or person with write permissions
856 allowed_to_close = PullRequestModel().check_user_update(
896 allowed_to_close = PullRequestModel().check_user_update(
857 pull_request, c.rhodecode_user)
897 pull_request, c.rhodecode_user)
858
898
859 if close_pull_request and allowed_to_close:
899 if close_pull_request and allowed_to_close:
860 close_pr = True
900 close_pr = True
861 pull_request_review_status = pull_request.calculated_review_status()
901 pull_request_review_status = pull_request.calculated_review_status()
862 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
902 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
863 # approved only if we have voting consent
903 # approved only if we have voting consent
864 status = ChangesetStatus.STATUS_APPROVED
904 status = ChangesetStatus.STATUS_APPROVED
865 else:
905 else:
866 status = ChangesetStatus.STATUS_REJECTED
906 status = ChangesetStatus.STATUS_REJECTED
867
907
868 allowed_to_change_status = PullRequestModel().check_user_change_status(
908 allowed_to_change_status = PullRequestModel().check_user_change_status(
869 pull_request, c.rhodecode_user)
909 pull_request, c.rhodecode_user)
870
910
871 if status and allowed_to_change_status:
911 if status and allowed_to_change_status:
872 message = (_('Status change %(transition_icon)s %(status)s')
912 message = (_('Status change %(transition_icon)s %(status)s')
873 % {'transition_icon': '>',
913 % {'transition_icon': '>',
874 'status': ChangesetStatus.get_status_lbl(status)})
914 'status': ChangesetStatus.get_status_lbl(status)})
875 if close_pr:
915 if close_pr:
876 message = _('Closing with') + ' ' + message
916 message = _('Closing with') + ' ' + message
877 text = text or message
917 text = text or message
878 comm = CommentsModel().create(
918 comm = CommentsModel().create(
879 text=text,
919 text=text,
880 repo=c.rhodecode_db_repo.repo_id,
920 repo=c.rhodecode_db_repo.repo_id,
881 user=c.rhodecode_user.user_id,
921 user=c.rhodecode_user.user_id,
882 pull_request=pull_request_id,
922 pull_request=pull_request_id,
883 f_path=request.POST.get('f_path'),
923 f_path=request.POST.get('f_path'),
884 line_no=request.POST.get('line'),
924 line_no=request.POST.get('line'),
885 status_change=(ChangesetStatus.get_status_lbl(status)
925 status_change=(ChangesetStatus.get_status_lbl(status)
886 if status and allowed_to_change_status else None),
926 if status and allowed_to_change_status else None),
887 status_change_type=(status
927 status_change_type=(status
888 if status and allowed_to_change_status else None),
928 if status and allowed_to_change_status else None),
889 closing_pr=close_pr,
929 closing_pr=close_pr,
890 comment_type=comment_type,
930 comment_type=comment_type,
891 resolves_comment_id=resolves_comment_id
931 resolves_comment_id=resolves_comment_id
892 )
932 )
893
933
894 if allowed_to_change_status:
934 if allowed_to_change_status:
895 old_calculated_status = pull_request.calculated_review_status()
935 old_calculated_status = pull_request.calculated_review_status()
896 # get status if set !
936 # get status if set !
897 if status:
937 if status:
898 ChangesetStatusModel().set_status(
938 ChangesetStatusModel().set_status(
899 c.rhodecode_db_repo.repo_id,
939 c.rhodecode_db_repo.repo_id,
900 status,
940 status,
901 c.rhodecode_user.user_id,
941 c.rhodecode_user.user_id,
902 comm,
942 comm,
903 pull_request=pull_request_id
943 pull_request=pull_request_id
904 )
944 )
905
945
906 Session().flush()
946 Session().flush()
907 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
947 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
908 # we now calculate the status of pull request, and based on that
948 # we now calculate the status of pull request, and based on that
909 # calculation we set the commits status
949 # calculation we set the commits status
910 calculated_status = pull_request.calculated_review_status()
950 calculated_status = pull_request.calculated_review_status()
911 if old_calculated_status != calculated_status:
951 if old_calculated_status != calculated_status:
912 PullRequestModel()._trigger_pull_request_hook(
952 PullRequestModel()._trigger_pull_request_hook(
913 pull_request, c.rhodecode_user, 'review_status_change')
953 pull_request, c.rhodecode_user, 'review_status_change')
914
954
915 calculated_status_lbl = ChangesetStatus.get_status_lbl(
955 calculated_status_lbl = ChangesetStatus.get_status_lbl(
916 calculated_status)
956 calculated_status)
917
957
918 if close_pr:
958 if close_pr:
919 status_completed = (
959 status_completed = (
920 calculated_status in [ChangesetStatus.STATUS_APPROVED,
960 calculated_status in [ChangesetStatus.STATUS_APPROVED,
921 ChangesetStatus.STATUS_REJECTED])
961 ChangesetStatus.STATUS_REJECTED])
922 if close_pull_request or status_completed:
962 if close_pull_request or status_completed:
923 PullRequestModel().close_pull_request(
963 PullRequestModel().close_pull_request(
924 pull_request_id, c.rhodecode_user)
964 pull_request_id, c.rhodecode_user)
925 else:
965 else:
926 h.flash(_('Closing pull request on other statuses than '
966 h.flash(_('Closing pull request on other statuses than '
927 'rejected or approved is forbidden. '
967 'rejected or approved is forbidden. '
928 'Calculated status from all reviewers '
968 'Calculated status from all reviewers '
929 'is currently: %s') % calculated_status_lbl,
969 'is currently: %s') % calculated_status_lbl,
930 category='warning')
970 category='warning')
931
971
932 Session().commit()
972 Session().commit()
933
973
934 if not request.is_xhr:
974 if not request.is_xhr:
935 return redirect(h.url('pullrequest_show', repo_name=repo_name,
975 return redirect(h.url('pullrequest_show', repo_name=repo_name,
936 pull_request_id=pull_request_id))
976 pull_request_id=pull_request_id))
937
977
938 data = {
978 data = {
939 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
979 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
940 }
980 }
941 if comm:
981 if comm:
942 c.co = comm
982 c.co = comm
943 c.inline_comment = True if comm.line_no else False
983 c.inline_comment = True if comm.line_no else False
944 data.update(comm.get_dict())
984 data.update(comm.get_dict())
945 data.update({'rendered_text':
985 data.update({'rendered_text':
946 render('changeset/changeset_comment_block.mako')})
986 render('changeset/changeset_comment_block.mako')})
947
987
948 return data
988 return data
949
989
950 @LoginRequired()
990 @LoginRequired()
951 @NotAnonymous()
991 @NotAnonymous()
952 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
992 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
953 'repository.admin')
993 'repository.admin')
954 @auth.CSRFRequired()
994 @auth.CSRFRequired()
955 @jsonify
995 @jsonify
956 def delete_comment(self, repo_name, comment_id):
996 def delete_comment(self, repo_name, comment_id):
957 return self._delete_comment(comment_id)
997 return self._delete_comment(comment_id)
958
998
959 def _delete_comment(self, comment_id):
999 def _delete_comment(self, comment_id):
960 comment_id = safe_int(comment_id)
1000 comment_id = safe_int(comment_id)
961 co = ChangesetComment.get_or_404(comment_id)
1001 co = ChangesetComment.get_or_404(comment_id)
962 if co.pull_request.is_closed():
1002 if co.pull_request.is_closed():
963 # don't allow deleting comments on closed pull request
1003 # don't allow deleting comments on closed pull request
964 raise HTTPForbidden()
1004 raise HTTPForbidden()
965
1005
966 is_owner = co.author.user_id == c.rhodecode_user.user_id
1006 is_owner = co.author.user_id == c.rhodecode_user.user_id
967 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1007 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
968 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1008 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
969 old_calculated_status = co.pull_request.calculated_review_status()
1009 old_calculated_status = co.pull_request.calculated_review_status()
970 CommentsModel().delete(comment=co)
1010 CommentsModel().delete(comment=co)
971 Session().commit()
1011 Session().commit()
972 calculated_status = co.pull_request.calculated_review_status()
1012 calculated_status = co.pull_request.calculated_review_status()
973 if old_calculated_status != calculated_status:
1013 if old_calculated_status != calculated_status:
974 PullRequestModel()._trigger_pull_request_hook(
1014 PullRequestModel()._trigger_pull_request_hook(
975 co.pull_request, c.rhodecode_user, 'review_status_change')
1015 co.pull_request, c.rhodecode_user, 'review_status_change')
976 return True
1016 return True
977 else:
1017 else:
978 raise HTTPForbidden()
1018 raise HTTPForbidden()
@@ -1,109 +1,89 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Utilities to be shared by multiple controllers.
22 Utilities to be shared by multiple controllers.
23
23
24 Should only contain utilities to be shared in the controller layer.
24 Should only contain utilities to be shared in the controller layer.
25 """
25 """
26
26
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.vcs.exceptions import RepositoryError
28 from rhodecode.lib.vcs.exceptions import RepositoryError
29
29
30
30
31 def parse_path_ref(ref, default_path=None):
31 def parse_path_ref(ref, default_path=None):
32 """
32 """
33 Parse out a path and reference combination and return both parts of it.
33 Parse out a path and reference combination and return both parts of it.
34
34
35 This is used to allow support of path based comparisons for Subversion
35 This is used to allow support of path based comparisons for Subversion
36 as an iterim solution in parameter handling.
36 as an iterim solution in parameter handling.
37 """
37 """
38 if '@' in ref:
38 if '@' in ref:
39 return ref.rsplit('@', 1)
39 return ref.rsplit('@', 1)
40 else:
40 else:
41 return default_path, ref
41 return default_path, ref
42
42
43
43
44 def get_format_ref_id(repo):
44 def get_format_ref_id(repo):
45 """Returns a `repo` specific reference formatter function"""
45 """Returns a `repo` specific reference formatter function"""
46 if h.is_svn(repo):
46 if h.is_svn(repo):
47 return _format_ref_id_svn
47 return _format_ref_id_svn
48 else:
48 else:
49 return _format_ref_id
49 return _format_ref_id
50
50
51
51
52 def _format_ref_id(name, raw_id):
52 def _format_ref_id(name, raw_id):
53 """Default formatting of a given reference `name`"""
53 """Default formatting of a given reference `name`"""
54 return name
54 return name
55
55
56
56
57 def _format_ref_id_svn(name, raw_id):
57 def _format_ref_id_svn(name, raw_id):
58 """Special way of formatting a reference for Subversion including path"""
58 """Special way of formatting a reference for Subversion including path"""
59 return '%s@%s' % (name, raw_id)
59 return '%s@%s' % (name, raw_id)
60
60
61
61
62 def get_commit_from_ref_name(repo, ref_name, ref_type=None):
62 def get_commit_from_ref_name(repo, ref_name, ref_type=None):
63 """
63 """
64 Gets the commit for a `ref_name` taking into account `ref_type`.
64 Gets the commit for a `ref_name` taking into account `ref_type`.
65 Needed in case a bookmark / tag share the same name.
65 Needed in case a bookmark / tag share the same name.
66
66
67 :param repo: the repo instance
67 :param repo: the repo instance
68 :param ref_name: the name of the ref to get
68 :param ref_name: the name of the ref to get
69 :param ref_type: optional, used to disambiguate colliding refs
69 :param ref_type: optional, used to disambiguate colliding refs
70 """
70 """
71 repo_scm = repo.scm_instance()
71 repo_scm = repo.scm_instance()
72 ref_type_mapping = {
72 ref_type_mapping = {
73 'book': repo_scm.bookmarks,
73 'book': repo_scm.bookmarks,
74 'bookmark': repo_scm.bookmarks,
74 'bookmark': repo_scm.bookmarks,
75 'tag': repo_scm.tags,
75 'tag': repo_scm.tags,
76 'branch': repo_scm.branches,
76 'branch': repo_scm.branches,
77 }
77 }
78
78
79 commit_id = ref_name
79 commit_id = ref_name
80 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
80 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
81 # the branch issue with svn is fixed
81 # the branch issue with svn is fixed
82 if ref_type and ref_type in ref_type_mapping:
82 if ref_type and ref_type in ref_type_mapping:
83 try:
83 try:
84 commit_id = ref_type_mapping[ref_type][ref_name]
84 commit_id = ref_type_mapping[ref_type][ref_name]
85 except KeyError:
85 except KeyError:
86 raise RepositoryError(
86 raise RepositoryError(
87 '%s "%s" does not exist' % (ref_type, ref_name))
87 '%s "%s" does not exist' % (ref_type, ref_name))
88
88
89 return repo_scm.get_commit(commit_id)
89 return repo_scm.get_commit(commit_id)
90
91
92 def reviewer_as_json(user, reasons, mandatory):
93 """
94 Returns json struct of a reviewer for frontend
95
96 :param user: the reviewer
97 :param reasons: list of strings of why they are reviewers
98 :param mandatory: bool, to set user as mandatory
99 """
100
101 return {
102 'user_id': user.user_id,
103 'reasons': reasons,
104 'mandatory': mandatory,
105 'username': user.username,
106 'firstname': user.firstname,
107 'lastname': user.lastname,
108 'gravatar_link': h.gravatar_url(user.email, 14),
109 }
@@ -1,268 +1,269 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Changeset status conttroller
22 Changeset status conttroller
23 """
23 """
24
24
25 import itertools
25 import itertools
26 import logging
26 import logging
27 from collections import defaultdict
27 from collections import defaultdict
28
28
29 from rhodecode.model import BaseModel
29 from rhodecode.model import BaseModel
30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32 from rhodecode.lib.markup_renderer import (
32 from rhodecode.lib.markup_renderer import (
33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class ChangesetStatusModel(BaseModel):
38 class ChangesetStatusModel(BaseModel):
39
39
40 cls = ChangesetStatus
40 cls = ChangesetStatus
41
41
42 def __get_changeset_status(self, changeset_status):
42 def __get_changeset_status(self, changeset_status):
43 return self._get_instance(ChangesetStatus, changeset_status)
43 return self._get_instance(ChangesetStatus, changeset_status)
44
44
45 def __get_pull_request(self, pull_request):
45 def __get_pull_request(self, pull_request):
46 return self._get_instance(PullRequest, pull_request)
46 return self._get_instance(PullRequest, pull_request)
47
47
48 def _get_status_query(self, repo, revision, pull_request,
48 def _get_status_query(self, repo, revision, pull_request,
49 with_revisions=False):
49 with_revisions=False):
50 repo = self._get_repo(repo)
50 repo = self._get_repo(repo)
51
51
52 q = ChangesetStatus.query()\
52 q = ChangesetStatus.query()\
53 .filter(ChangesetStatus.repo == repo)
53 .filter(ChangesetStatus.repo == repo)
54 if not with_revisions:
54 if not with_revisions:
55 q = q.filter(ChangesetStatus.version == 0)
55 q = q.filter(ChangesetStatus.version == 0)
56
56
57 if revision:
57 if revision:
58 q = q.filter(ChangesetStatus.revision == revision)
58 q = q.filter(ChangesetStatus.revision == revision)
59 elif pull_request:
59 elif pull_request:
60 pull_request = self.__get_pull_request(pull_request)
60 pull_request = self.__get_pull_request(pull_request)
61 # TODO: johbo: Think about the impact of this join, there must
61 # TODO: johbo: Think about the impact of this join, there must
62 # be a reason why ChangesetStatus and ChanagesetComment is linked
62 # be a reason why ChangesetStatus and ChanagesetComment is linked
63 # to the pull request. Might be that we want to do the same for
63 # to the pull request. Might be that we want to do the same for
64 # the pull_request_version_id.
64 # the pull_request_version_id.
65 q = q.join(ChangesetComment).filter(
65 q = q.join(ChangesetComment).filter(
66 ChangesetStatus.pull_request == pull_request,
66 ChangesetStatus.pull_request == pull_request,
67 ChangesetComment.pull_request_version_id == None)
67 ChangesetComment.pull_request_version_id == None)
68 else:
68 else:
69 raise Exception('Please specify revision or pull_request')
69 raise Exception('Please specify revision or pull_request')
70 q = q.order_by(ChangesetStatus.version.asc())
70 q = q.order_by(ChangesetStatus.version.asc())
71 return q
71 return q
72
72
73 def calculate_status(self, statuses_by_reviewers):
73 def calculate_status(self, statuses_by_reviewers):
74 """
74 """
75 Given the approval statuses from reviewers, calculates final approval
75 Given the approval statuses from reviewers, calculates final approval
76 status. There can only be 3 results, all approved, all rejected. If
76 status. There can only be 3 results, all approved, all rejected. If
77 there is no consensus the PR is under review.
77 there is no consensus the PR is under review.
78
78
79 :param statuses_by_reviewers:
79 :param statuses_by_reviewers:
80 """
80 """
81 votes = defaultdict(int)
81 votes = defaultdict(int)
82 reviewers_number = len(statuses_by_reviewers)
82 reviewers_number = len(statuses_by_reviewers)
83 for user, reasons, statuses in statuses_by_reviewers:
83 for user, reasons, mandatory, statuses in statuses_by_reviewers:
84 if statuses:
84 if statuses:
85 ver, latest = statuses[0]
85 ver, latest = statuses[0]
86 votes[latest.status] += 1
86 votes[latest.status] += 1
87 else:
87 else:
88 votes[ChangesetStatus.DEFAULT] += 1
88 votes[ChangesetStatus.DEFAULT] += 1
89
89
90 # all approved
90 # all approved
91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
92 return ChangesetStatus.STATUS_APPROVED
92 return ChangesetStatus.STATUS_APPROVED
93
93
94 # all rejected
94 # all rejected
95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
96 return ChangesetStatus.STATUS_REJECTED
96 return ChangesetStatus.STATUS_REJECTED
97
97
98 return ChangesetStatus.STATUS_UNDER_REVIEW
98 return ChangesetStatus.STATUS_UNDER_REVIEW
99
99
100 def get_statuses(self, repo, revision=None, pull_request=None,
100 def get_statuses(self, repo, revision=None, pull_request=None,
101 with_revisions=False):
101 with_revisions=False):
102 q = self._get_status_query(repo, revision, pull_request,
102 q = self._get_status_query(repo, revision, pull_request,
103 with_revisions)
103 with_revisions)
104 return q.all()
104 return q.all()
105
105
106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
107 """
107 """
108 Returns latest status of changeset for given revision or for given
108 Returns latest status of changeset for given revision or for given
109 pull request. Statuses are versioned inside a table itself and
109 pull request. Statuses are versioned inside a table itself and
110 version == 0 is always the current one
110 version == 0 is always the current one
111
111
112 :param repo:
112 :param repo:
113 :param revision: 40char hash or None
113 :param revision: 40char hash or None
114 :param pull_request: pull_request reference
114 :param pull_request: pull_request reference
115 :param as_str: return status as string not object
115 :param as_str: return status as string not object
116 """
116 """
117 q = self._get_status_query(repo, revision, pull_request)
117 q = self._get_status_query(repo, revision, pull_request)
118
118
119 # need to use first here since there can be multiple statuses
119 # need to use first here since there can be multiple statuses
120 # returned from pull_request
120 # returned from pull_request
121 status = q.first()
121 status = q.first()
122 if as_str:
122 if as_str:
123 status = status.status if status else status
123 status = status.status if status else status
124 st = status or ChangesetStatus.DEFAULT
124 st = status or ChangesetStatus.DEFAULT
125 return str(st)
125 return str(st)
126 return status
126 return status
127
127
128 def _render_auto_status_message(
128 def _render_auto_status_message(
129 self, status, commit_id=None, pull_request=None):
129 self, status, commit_id=None, pull_request=None):
130 """
130 """
131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
132 so it's always looking the same disregarding on which default
132 so it's always looking the same disregarding on which default
133 renderer system is using.
133 renderer system is using.
134
134
135 :param status: status text to change into
135 :param status: status text to change into
136 :param commit_id: the commit_id we change the status for
136 :param commit_id: the commit_id we change the status for
137 :param pull_request: the pull request we change the status for
137 :param pull_request: the pull request we change the status for
138 """
138 """
139
139
140 new_status = ChangesetStatus.get_status_lbl(status)
140 new_status = ChangesetStatus.get_status_lbl(status)
141
141
142 params = {
142 params = {
143 'new_status_label': new_status,
143 'new_status_label': new_status,
144 'pull_request': pull_request,
144 'pull_request': pull_request,
145 'commit_id': commit_id,
145 'commit_id': commit_id,
146 }
146 }
147 renderer = RstTemplateRenderer()
147 renderer = RstTemplateRenderer()
148 return renderer.render('auto_status_change.mako', **params)
148 return renderer.render('auto_status_change.mako', **params)
149
149
150 def set_status(self, repo, status, user, comment=None, revision=None,
150 def set_status(self, repo, status, user, comment=None, revision=None,
151 pull_request=None, dont_allow_on_closed_pull_request=False):
151 pull_request=None, dont_allow_on_closed_pull_request=False):
152 """
152 """
153 Creates new status for changeset or updates the old ones bumping their
153 Creates new status for changeset or updates the old ones bumping their
154 version, leaving the current status at
154 version, leaving the current status at
155
155
156 :param repo:
156 :param repo:
157 :param revision:
157 :param revision:
158 :param status:
158 :param status:
159 :param user:
159 :param user:
160 :param comment:
160 :param comment:
161 :param dont_allow_on_closed_pull_request: don't allow a status change
161 :param dont_allow_on_closed_pull_request: don't allow a status change
162 if last status was for pull request and it's closed. We shouldn't
162 if last status was for pull request and it's closed. We shouldn't
163 mess around this manually
163 mess around this manually
164 """
164 """
165 repo = self._get_repo(repo)
165 repo = self._get_repo(repo)
166
166
167 q = ChangesetStatus.query()
167 q = ChangesetStatus.query()
168
168
169 if revision:
169 if revision:
170 q = q.filter(ChangesetStatus.repo == repo)
170 q = q.filter(ChangesetStatus.repo == repo)
171 q = q.filter(ChangesetStatus.revision == revision)
171 q = q.filter(ChangesetStatus.revision == revision)
172 elif pull_request:
172 elif pull_request:
173 pull_request = self.__get_pull_request(pull_request)
173 pull_request = self.__get_pull_request(pull_request)
174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
176 cur_statuses = q.all()
176 cur_statuses = q.all()
177
177
178 # if statuses exists and last is associated with a closed pull request
178 # if statuses exists and last is associated with a closed pull request
179 # we need to check if we can allow this status change
179 # we need to check if we can allow this status change
180 if (dont_allow_on_closed_pull_request and cur_statuses
180 if (dont_allow_on_closed_pull_request and cur_statuses
181 and getattr(cur_statuses[0].pull_request, 'status', '')
181 and getattr(cur_statuses[0].pull_request, 'status', '')
182 == PullRequest.STATUS_CLOSED):
182 == PullRequest.STATUS_CLOSED):
183 raise StatusChangeOnClosedPullRequestError(
183 raise StatusChangeOnClosedPullRequestError(
184 'Changing status on closed pull request is not allowed'
184 'Changing status on closed pull request is not allowed'
185 )
185 )
186
186
187 # update all current statuses with older version
187 # update all current statuses with older version
188 if cur_statuses:
188 if cur_statuses:
189 for st in cur_statuses:
189 for st in cur_statuses:
190 st.version += 1
190 st.version += 1
191 self.sa.add(st)
191 self.sa.add(st)
192
192
193 def _create_status(user, repo, status, comment, revision, pull_request):
193 def _create_status(user, repo, status, comment, revision, pull_request):
194 new_status = ChangesetStatus()
194 new_status = ChangesetStatus()
195 new_status.author = self._get_user(user)
195 new_status.author = self._get_user(user)
196 new_status.repo = self._get_repo(repo)
196 new_status.repo = self._get_repo(repo)
197 new_status.status = status
197 new_status.status = status
198 new_status.comment = comment
198 new_status.comment = comment
199 new_status.revision = revision
199 new_status.revision = revision
200 new_status.pull_request = pull_request
200 new_status.pull_request = pull_request
201 return new_status
201 return new_status
202
202
203 if not comment:
203 if not comment:
204 from rhodecode.model.comment import CommentsModel
204 from rhodecode.model.comment import CommentsModel
205 comment = CommentsModel().create(
205 comment = CommentsModel().create(
206 text=self._render_auto_status_message(
206 text=self._render_auto_status_message(
207 status, commit_id=revision, pull_request=pull_request),
207 status, commit_id=revision, pull_request=pull_request),
208 repo=repo,
208 repo=repo,
209 user=user,
209 user=user,
210 pull_request=pull_request,
210 pull_request=pull_request,
211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
212 )
212 )
213
213
214 if revision:
214 if revision:
215 new_status = _create_status(
215 new_status = _create_status(
216 user=user, repo=repo, status=status, comment=comment,
216 user=user, repo=repo, status=status, comment=comment,
217 revision=revision, pull_request=pull_request)
217 revision=revision, pull_request=pull_request)
218 self.sa.add(new_status)
218 self.sa.add(new_status)
219 return new_status
219 return new_status
220 elif pull_request:
220 elif pull_request:
221 # pull request can have more than one revision associated to it
221 # pull request can have more than one revision associated to it
222 # we need to create new version for each one
222 # we need to create new version for each one
223 new_statuses = []
223 new_statuses = []
224 repo = pull_request.source_repo
224 repo = pull_request.source_repo
225 for rev in pull_request.revisions:
225 for rev in pull_request.revisions:
226 new_status = _create_status(
226 new_status = _create_status(
227 user=user, repo=repo, status=status, comment=comment,
227 user=user, repo=repo, status=status, comment=comment,
228 revision=rev, pull_request=pull_request)
228 revision=rev, pull_request=pull_request)
229 new_statuses.append(new_status)
229 new_statuses.append(new_status)
230 self.sa.add(new_status)
230 self.sa.add(new_status)
231 return new_statuses
231 return new_statuses
232
232
233 def reviewers_statuses(self, pull_request):
233 def reviewers_statuses(self, pull_request):
234 _commit_statuses = self.get_statuses(
234 _commit_statuses = self.get_statuses(
235 pull_request.source_repo,
235 pull_request.source_repo,
236 pull_request=pull_request,
236 pull_request=pull_request,
237 with_revisions=True)
237 with_revisions=True)
238
238
239 commit_statuses = defaultdict(list)
239 commit_statuses = defaultdict(list)
240 for st in _commit_statuses:
240 for st in _commit_statuses:
241 commit_statuses[st.author.username] += [st]
241 commit_statuses[st.author.username] += [st]
242
242
243 pull_request_reviewers = []
243 pull_request_reviewers = []
244
244
245 def version(commit_status):
245 def version(commit_status):
246 return commit_status.version
246 return commit_status.version
247
247
248 for o in pull_request.reviewers:
248 for o in pull_request.reviewers:
249 if not o.user:
249 if not o.user:
250 continue
250 continue
251 st = commit_statuses.get(o.user.username, None)
251 statuses = commit_statuses.get(o.user.username, None)
252 if st:
252 if statuses:
253 st = [(x, list(y)[0])
253 statuses = [(x, list(y)[0])
254 for x, y in (itertools.groupby(sorted(st, key=version),
254 for x, y in (itertools.groupby(
255 version))]
255 sorted(statuses, key=version),version))]
256
256
257 pull_request_reviewers.append((o.user, o.reasons, st))
257 pull_request_reviewers.append(
258 (o.user, o.reasons, o.mandatory, statuses))
258 return pull_request_reviewers
259 return pull_request_reviewers
259
260
260 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 """
262 """
262 calculate pull request status based on reviewers, it should be a list
263 calculate pull request status based on reviewers, it should be a list
263 of two element lists.
264 of two element lists.
264
265
265 :param reviewers_statuses:
266 :param reviewers_statuses:
266 """
267 """
267 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 return self.calculate_status(reviewers)
269 return self.calculate_status(reviewers)
@@ -1,3993 +1,4015 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from zope.cachedescriptors.property import Lazy as LazyProperty
45 from zope.cachedescriptors.property import Lazy as LazyProperty
46
46
47 from pylons import url
47 from pylons import url
48 from pylons.i18n.translation import lazy_ugettext as _
48 from pylons.i18n.translation import lazy_ugettext as _
49
49
50 from rhodecode.lib.vcs import get_vcs_instance
50 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.utils2 import (
52 from rhodecode.lib.utils2 import (
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 glob2re, StrictAttributeDict, cleaned_uri)
55 glob2re, StrictAttributeDict, cleaned_uri)
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.ext_json import json
57 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.caching_query import FromCache
58 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.encrypt import AESCipher
59 from rhodecode.lib.encrypt import AESCipher
60
60
61 from rhodecode.model.meta import Base, Session
61 from rhodecode.model.meta import Base, Session
62
62
63 URL_SEP = '/'
63 URL_SEP = '/'
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66 # =============================================================================
66 # =============================================================================
67 # BASE CLASSES
67 # BASE CLASSES
68 # =============================================================================
68 # =============================================================================
69
69
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # beaker.session.secret if first is not set.
71 # beaker.session.secret if first is not set.
72 # and initialized at environment.py
72 # and initialized at environment.py
73 ENCRYPTION_KEY = None
73 ENCRYPTION_KEY = None
74
74
75 # used to sort permissions by types, '#' used here is not allowed to be in
75 # used to sort permissions by types, '#' used here is not allowed to be in
76 # usernames, and it's very early in sorted string.printable table.
76 # usernames, and it's very early in sorted string.printable table.
77 PERMISSION_TYPE_SORT = {
77 PERMISSION_TYPE_SORT = {
78 'admin': '####',
78 'admin': '####',
79 'write': '###',
79 'write': '###',
80 'read': '##',
80 'read': '##',
81 'none': '#',
81 'none': '#',
82 }
82 }
83
83
84
84
85 def display_sort(obj):
85 def display_sort(obj):
86 """
86 """
87 Sort function used to sort permissions in .permissions() function of
87 Sort function used to sort permissions in .permissions() function of
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 of all other resources
89 of all other resources
90 """
90 """
91
91
92 if obj.username == User.DEFAULT_USER:
92 if obj.username == User.DEFAULT_USER:
93 return '#####'
93 return '#####'
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 return prefix + obj.username
95 return prefix + obj.username
96
96
97
97
98 def _hash_key(k):
98 def _hash_key(k):
99 return md5_safe(k)
99 return md5_safe(k)
100
100
101
101
102 class EncryptedTextValue(TypeDecorator):
102 class EncryptedTextValue(TypeDecorator):
103 """
103 """
104 Special column for encrypted long text data, use like::
104 Special column for encrypted long text data, use like::
105
105
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107
107
108 This column is intelligent so if value is in unencrypted form it return
108 This column is intelligent so if value is in unencrypted form it return
109 unencrypted form, but on save it always encrypts
109 unencrypted form, but on save it always encrypts
110 """
110 """
111 impl = Text
111 impl = Text
112
112
113 def process_bind_param(self, value, dialect):
113 def process_bind_param(self, value, dialect):
114 if not value:
114 if not value:
115 return value
115 return value
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 # protect against double encrypting if someone manually starts
117 # protect against double encrypting if someone manually starts
118 # doing
118 # doing
119 raise ValueError('value needs to be in unencrypted format, ie. '
119 raise ValueError('value needs to be in unencrypted format, ie. '
120 'not starting with enc$aes')
120 'not starting with enc$aes')
121 return 'enc$aes_hmac$%s' % AESCipher(
121 return 'enc$aes_hmac$%s' % AESCipher(
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123
123
124 def process_result_value(self, value, dialect):
124 def process_result_value(self, value, dialect):
125 import rhodecode
125 import rhodecode
126
126
127 if not value:
127 if not value:
128 return value
128 return value
129
129
130 parts = value.split('$', 3)
130 parts = value.split('$', 3)
131 if not len(parts) == 3:
131 if not len(parts) == 3:
132 # probably not encrypted values
132 # probably not encrypted values
133 return value
133 return value
134 else:
134 else:
135 if parts[0] != 'enc':
135 if parts[0] != 'enc':
136 # parts ok but without our header ?
136 # parts ok but without our header ?
137 return value
137 return value
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 'rhodecode.encrypted_values.strict') or True)
139 'rhodecode.encrypted_values.strict') or True)
140 # at that stage we know it's our encryption
140 # at that stage we know it's our encryption
141 if parts[1] == 'aes':
141 if parts[1] == 'aes':
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 elif parts[1] == 'aes_hmac':
143 elif parts[1] == 'aes_hmac':
144 decrypted_data = AESCipher(
144 decrypted_data = AESCipher(
145 ENCRYPTION_KEY, hmac=True,
145 ENCRYPTION_KEY, hmac=True,
146 strict_verification=enc_strict_mode).decrypt(parts[2])
146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 else:
147 else:
148 raise ValueError(
148 raise ValueError(
149 'Encryption type part is wrong, must be `aes` '
149 'Encryption type part is wrong, must be `aes` '
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 return decrypted_data
151 return decrypted_data
152
152
153
153
154 class BaseModel(object):
154 class BaseModel(object):
155 """
155 """
156 Base Model for all classes
156 Base Model for all classes
157 """
157 """
158
158
159 @classmethod
159 @classmethod
160 def _get_keys(cls):
160 def _get_keys(cls):
161 """return column names for this model """
161 """return column names for this model """
162 return class_mapper(cls).c.keys()
162 return class_mapper(cls).c.keys()
163
163
164 def get_dict(self):
164 def get_dict(self):
165 """
165 """
166 return dict with keys and values corresponding
166 return dict with keys and values corresponding
167 to this model data """
167 to this model data """
168
168
169 d = {}
169 d = {}
170 for k in self._get_keys():
170 for k in self._get_keys():
171 d[k] = getattr(self, k)
171 d[k] = getattr(self, k)
172
172
173 # also use __json__() if present to get additional fields
173 # also use __json__() if present to get additional fields
174 _json_attr = getattr(self, '__json__', None)
174 _json_attr = getattr(self, '__json__', None)
175 if _json_attr:
175 if _json_attr:
176 # update with attributes from __json__
176 # update with attributes from __json__
177 if callable(_json_attr):
177 if callable(_json_attr):
178 _json_attr = _json_attr()
178 _json_attr = _json_attr()
179 for k, val in _json_attr.iteritems():
179 for k, val in _json_attr.iteritems():
180 d[k] = val
180 d[k] = val
181 return d
181 return d
182
182
183 def get_appstruct(self):
183 def get_appstruct(self):
184 """return list with keys and values tuples corresponding
184 """return list with keys and values tuples corresponding
185 to this model data """
185 to this model data """
186
186
187 l = []
187 l = []
188 for k in self._get_keys():
188 for k in self._get_keys():
189 l.append((k, getattr(self, k),))
189 l.append((k, getattr(self, k),))
190 return l
190 return l
191
191
192 def populate_obj(self, populate_dict):
192 def populate_obj(self, populate_dict):
193 """populate model with data from given populate_dict"""
193 """populate model with data from given populate_dict"""
194
194
195 for k in self._get_keys():
195 for k in self._get_keys():
196 if k in populate_dict:
196 if k in populate_dict:
197 setattr(self, k, populate_dict[k])
197 setattr(self, k, populate_dict[k])
198
198
199 @classmethod
199 @classmethod
200 def query(cls):
200 def query(cls):
201 return Session().query(cls)
201 return Session().query(cls)
202
202
203 @classmethod
203 @classmethod
204 def get(cls, id_):
204 def get(cls, id_):
205 if id_:
205 if id_:
206 return cls.query().get(id_)
206 return cls.query().get(id_)
207
207
208 @classmethod
208 @classmethod
209 def get_or_404(cls, id_, pyramid_exc=False):
209 def get_or_404(cls, id_, pyramid_exc=False):
210 if pyramid_exc:
210 if pyramid_exc:
211 # NOTE(marcink): backward compat, once migration to pyramid
211 # NOTE(marcink): backward compat, once migration to pyramid
212 # this should only use pyramid exceptions
212 # this should only use pyramid exceptions
213 from pyramid.httpexceptions import HTTPNotFound
213 from pyramid.httpexceptions import HTTPNotFound
214 else:
214 else:
215 from webob.exc import HTTPNotFound
215 from webob.exc import HTTPNotFound
216
216
217 try:
217 try:
218 id_ = int(id_)
218 id_ = int(id_)
219 except (TypeError, ValueError):
219 except (TypeError, ValueError):
220 raise HTTPNotFound
220 raise HTTPNotFound
221
221
222 res = cls.query().get(id_)
222 res = cls.query().get(id_)
223 if not res:
223 if not res:
224 raise HTTPNotFound
224 raise HTTPNotFound
225 return res
225 return res
226
226
227 @classmethod
227 @classmethod
228 def getAll(cls):
228 def getAll(cls):
229 # deprecated and left for backward compatibility
229 # deprecated and left for backward compatibility
230 return cls.get_all()
230 return cls.get_all()
231
231
232 @classmethod
232 @classmethod
233 def get_all(cls):
233 def get_all(cls):
234 return cls.query().all()
234 return cls.query().all()
235
235
236 @classmethod
236 @classmethod
237 def delete(cls, id_):
237 def delete(cls, id_):
238 obj = cls.query().get(id_)
238 obj = cls.query().get(id_)
239 Session().delete(obj)
239 Session().delete(obj)
240
240
241 @classmethod
241 @classmethod
242 def identity_cache(cls, session, attr_name, value):
242 def identity_cache(cls, session, attr_name, value):
243 exist_in_session = []
243 exist_in_session = []
244 for (item_cls, pkey), instance in session.identity_map.items():
244 for (item_cls, pkey), instance in session.identity_map.items():
245 if cls == item_cls and getattr(instance, attr_name) == value:
245 if cls == item_cls and getattr(instance, attr_name) == value:
246 exist_in_session.append(instance)
246 exist_in_session.append(instance)
247 if exist_in_session:
247 if exist_in_session:
248 if len(exist_in_session) == 1:
248 if len(exist_in_session) == 1:
249 return exist_in_session[0]
249 return exist_in_session[0]
250 log.exception(
250 log.exception(
251 'multiple objects with attr %s and '
251 'multiple objects with attr %s and '
252 'value %s found with same name: %r',
252 'value %s found with same name: %r',
253 attr_name, value, exist_in_session)
253 attr_name, value, exist_in_session)
254
254
255 def __repr__(self):
255 def __repr__(self):
256 if hasattr(self, '__unicode__'):
256 if hasattr(self, '__unicode__'):
257 # python repr needs to return str
257 # python repr needs to return str
258 try:
258 try:
259 return safe_str(self.__unicode__())
259 return safe_str(self.__unicode__())
260 except UnicodeDecodeError:
260 except UnicodeDecodeError:
261 pass
261 pass
262 return '<DB:%s>' % (self.__class__.__name__)
262 return '<DB:%s>' % (self.__class__.__name__)
263
263
264
264
265 class RhodeCodeSetting(Base, BaseModel):
265 class RhodeCodeSetting(Base, BaseModel):
266 __tablename__ = 'rhodecode_settings'
266 __tablename__ = 'rhodecode_settings'
267 __table_args__ = (
267 __table_args__ = (
268 UniqueConstraint('app_settings_name'),
268 UniqueConstraint('app_settings_name'),
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
271 )
271 )
272
272
273 SETTINGS_TYPES = {
273 SETTINGS_TYPES = {
274 'str': safe_str,
274 'str': safe_str,
275 'int': safe_int,
275 'int': safe_int,
276 'unicode': safe_unicode,
276 'unicode': safe_unicode,
277 'bool': str2bool,
277 'bool': str2bool,
278 'list': functools.partial(aslist, sep=',')
278 'list': functools.partial(aslist, sep=',')
279 }
279 }
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
281 GLOBAL_CONF_KEY = 'app_settings'
281 GLOBAL_CONF_KEY = 'app_settings'
282
282
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
287
287
288 def __init__(self, key='', val='', type='unicode'):
288 def __init__(self, key='', val='', type='unicode'):
289 self.app_settings_name = key
289 self.app_settings_name = key
290 self.app_settings_type = type
290 self.app_settings_type = type
291 self.app_settings_value = val
291 self.app_settings_value = val
292
292
293 @validates('_app_settings_value')
293 @validates('_app_settings_value')
294 def validate_settings_value(self, key, val):
294 def validate_settings_value(self, key, val):
295 assert type(val) == unicode
295 assert type(val) == unicode
296 return val
296 return val
297
297
298 @hybrid_property
298 @hybrid_property
299 def app_settings_value(self):
299 def app_settings_value(self):
300 v = self._app_settings_value
300 v = self._app_settings_value
301 _type = self.app_settings_type
301 _type = self.app_settings_type
302 if _type:
302 if _type:
303 _type = self.app_settings_type.split('.')[0]
303 _type = self.app_settings_type.split('.')[0]
304 # decode the encrypted value
304 # decode the encrypted value
305 if 'encrypted' in self.app_settings_type:
305 if 'encrypted' in self.app_settings_type:
306 cipher = EncryptedTextValue()
306 cipher = EncryptedTextValue()
307 v = safe_unicode(cipher.process_result_value(v, None))
307 v = safe_unicode(cipher.process_result_value(v, None))
308
308
309 converter = self.SETTINGS_TYPES.get(_type) or \
309 converter = self.SETTINGS_TYPES.get(_type) or \
310 self.SETTINGS_TYPES['unicode']
310 self.SETTINGS_TYPES['unicode']
311 return converter(v)
311 return converter(v)
312
312
313 @app_settings_value.setter
313 @app_settings_value.setter
314 def app_settings_value(self, val):
314 def app_settings_value(self, val):
315 """
315 """
316 Setter that will always make sure we use unicode in app_settings_value
316 Setter that will always make sure we use unicode in app_settings_value
317
317
318 :param val:
318 :param val:
319 """
319 """
320 val = safe_unicode(val)
320 val = safe_unicode(val)
321 # encode the encrypted value
321 # encode the encrypted value
322 if 'encrypted' in self.app_settings_type:
322 if 'encrypted' in self.app_settings_type:
323 cipher = EncryptedTextValue()
323 cipher = EncryptedTextValue()
324 val = safe_unicode(cipher.process_bind_param(val, None))
324 val = safe_unicode(cipher.process_bind_param(val, None))
325 self._app_settings_value = val
325 self._app_settings_value = val
326
326
327 @hybrid_property
327 @hybrid_property
328 def app_settings_type(self):
328 def app_settings_type(self):
329 return self._app_settings_type
329 return self._app_settings_type
330
330
331 @app_settings_type.setter
331 @app_settings_type.setter
332 def app_settings_type(self, val):
332 def app_settings_type(self, val):
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 if val.split('.')[0] not in self.SETTINGS_TYPES:
334 raise Exception('type must be one of %s got %s'
334 raise Exception('type must be one of %s got %s'
335 % (self.SETTINGS_TYPES.keys(), val))
335 % (self.SETTINGS_TYPES.keys(), val))
336 self._app_settings_type = val
336 self._app_settings_type = val
337
337
338 def __unicode__(self):
338 def __unicode__(self):
339 return u"<%s('%s:%s[%s]')>" % (
339 return u"<%s('%s:%s[%s]')>" % (
340 self.__class__.__name__,
340 self.__class__.__name__,
341 self.app_settings_name, self.app_settings_value,
341 self.app_settings_name, self.app_settings_value,
342 self.app_settings_type
342 self.app_settings_type
343 )
343 )
344
344
345
345
346 class RhodeCodeUi(Base, BaseModel):
346 class RhodeCodeUi(Base, BaseModel):
347 __tablename__ = 'rhodecode_ui'
347 __tablename__ = 'rhodecode_ui'
348 __table_args__ = (
348 __table_args__ = (
349 UniqueConstraint('ui_key'),
349 UniqueConstraint('ui_key'),
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
352 )
352 )
353
353
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 HOOK_REPO_SIZE = 'changegroup.repo_size'
355 # HG
355 # HG
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
357 HOOK_PULL = 'outgoing.pull_logger'
357 HOOK_PULL = 'outgoing.pull_logger'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
360 HOOK_PUSH = 'changegroup.push_logger'
360 HOOK_PUSH = 'changegroup.push_logger'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
361 HOOK_PUSH_KEY = 'pushkey.key_push'
362
362
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
363 # TODO: johbo: Unify way how hooks are configured for git and hg,
364 # git part is currently hardcoded.
364 # git part is currently hardcoded.
365
365
366 # SVN PATTERNS
366 # SVN PATTERNS
367 SVN_BRANCH_ID = 'vcs_svn_branch'
367 SVN_BRANCH_ID = 'vcs_svn_branch'
368 SVN_TAG_ID = 'vcs_svn_tag'
368 SVN_TAG_ID = 'vcs_svn_tag'
369
369
370 ui_id = Column(
370 ui_id = Column(
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
371 "ui_id", Integer(), nullable=False, unique=True, default=None,
372 primary_key=True)
372 primary_key=True)
373 ui_section = Column(
373 ui_section = Column(
374 "ui_section", String(255), nullable=True, unique=None, default=None)
374 "ui_section", String(255), nullable=True, unique=None, default=None)
375 ui_key = Column(
375 ui_key = Column(
376 "ui_key", String(255), nullable=True, unique=None, default=None)
376 "ui_key", String(255), nullable=True, unique=None, default=None)
377 ui_value = Column(
377 ui_value = Column(
378 "ui_value", String(255), nullable=True, unique=None, default=None)
378 "ui_value", String(255), nullable=True, unique=None, default=None)
379 ui_active = Column(
379 ui_active = Column(
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
381
381
382 def __repr__(self):
382 def __repr__(self):
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
384 self.ui_key, self.ui_value)
384 self.ui_key, self.ui_value)
385
385
386
386
387 class RepoRhodeCodeSetting(Base, BaseModel):
387 class RepoRhodeCodeSetting(Base, BaseModel):
388 __tablename__ = 'repo_rhodecode_settings'
388 __tablename__ = 'repo_rhodecode_settings'
389 __table_args__ = (
389 __table_args__ = (
390 UniqueConstraint(
390 UniqueConstraint(
391 'app_settings_name', 'repository_id',
391 'app_settings_name', 'repository_id',
392 name='uq_repo_rhodecode_setting_name_repo_id'),
392 name='uq_repo_rhodecode_setting_name_repo_id'),
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
395 )
395 )
396
396
397 repository_id = Column(
397 repository_id = Column(
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
399 nullable=False)
399 nullable=False)
400 app_settings_id = Column(
400 app_settings_id = Column(
401 "app_settings_id", Integer(), nullable=False, unique=True,
401 "app_settings_id", Integer(), nullable=False, unique=True,
402 default=None, primary_key=True)
402 default=None, primary_key=True)
403 app_settings_name = Column(
403 app_settings_name = Column(
404 "app_settings_name", String(255), nullable=True, unique=None,
404 "app_settings_name", String(255), nullable=True, unique=None,
405 default=None)
405 default=None)
406 _app_settings_value = Column(
406 _app_settings_value = Column(
407 "app_settings_value", String(4096), nullable=True, unique=None,
407 "app_settings_value", String(4096), nullable=True, unique=None,
408 default=None)
408 default=None)
409 _app_settings_type = Column(
409 _app_settings_type = Column(
410 "app_settings_type", String(255), nullable=True, unique=None,
410 "app_settings_type", String(255), nullable=True, unique=None,
411 default=None)
411 default=None)
412
412
413 repository = relationship('Repository')
413 repository = relationship('Repository')
414
414
415 def __init__(self, repository_id, key='', val='', type='unicode'):
415 def __init__(self, repository_id, key='', val='', type='unicode'):
416 self.repository_id = repository_id
416 self.repository_id = repository_id
417 self.app_settings_name = key
417 self.app_settings_name = key
418 self.app_settings_type = type
418 self.app_settings_type = type
419 self.app_settings_value = val
419 self.app_settings_value = val
420
420
421 @validates('_app_settings_value')
421 @validates('_app_settings_value')
422 def validate_settings_value(self, key, val):
422 def validate_settings_value(self, key, val):
423 assert type(val) == unicode
423 assert type(val) == unicode
424 return val
424 return val
425
425
426 @hybrid_property
426 @hybrid_property
427 def app_settings_value(self):
427 def app_settings_value(self):
428 v = self._app_settings_value
428 v = self._app_settings_value
429 type_ = self.app_settings_type
429 type_ = self.app_settings_type
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
432 return converter(v)
432 return converter(v)
433
433
434 @app_settings_value.setter
434 @app_settings_value.setter
435 def app_settings_value(self, val):
435 def app_settings_value(self, val):
436 """
436 """
437 Setter that will always make sure we use unicode in app_settings_value
437 Setter that will always make sure we use unicode in app_settings_value
438
438
439 :param val:
439 :param val:
440 """
440 """
441 self._app_settings_value = safe_unicode(val)
441 self._app_settings_value = safe_unicode(val)
442
442
443 @hybrid_property
443 @hybrid_property
444 def app_settings_type(self):
444 def app_settings_type(self):
445 return self._app_settings_type
445 return self._app_settings_type
446
446
447 @app_settings_type.setter
447 @app_settings_type.setter
448 def app_settings_type(self, val):
448 def app_settings_type(self, val):
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
450 if val not in SETTINGS_TYPES:
450 if val not in SETTINGS_TYPES:
451 raise Exception('type must be one of %s got %s'
451 raise Exception('type must be one of %s got %s'
452 % (SETTINGS_TYPES.keys(), val))
452 % (SETTINGS_TYPES.keys(), val))
453 self._app_settings_type = val
453 self._app_settings_type = val
454
454
455 def __unicode__(self):
455 def __unicode__(self):
456 return u"<%s('%s:%s:%s[%s]')>" % (
456 return u"<%s('%s:%s:%s[%s]')>" % (
457 self.__class__.__name__, self.repository.repo_name,
457 self.__class__.__name__, self.repository.repo_name,
458 self.app_settings_name, self.app_settings_value,
458 self.app_settings_name, self.app_settings_value,
459 self.app_settings_type
459 self.app_settings_type
460 )
460 )
461
461
462
462
463 class RepoRhodeCodeUi(Base, BaseModel):
463 class RepoRhodeCodeUi(Base, BaseModel):
464 __tablename__ = 'repo_rhodecode_ui'
464 __tablename__ = 'repo_rhodecode_ui'
465 __table_args__ = (
465 __table_args__ = (
466 UniqueConstraint(
466 UniqueConstraint(
467 'repository_id', 'ui_section', 'ui_key',
467 'repository_id', 'ui_section', 'ui_key',
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
471 )
471 )
472
472
473 repository_id = Column(
473 repository_id = Column(
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
475 nullable=False)
475 nullable=False)
476 ui_id = Column(
476 ui_id = Column(
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
477 "ui_id", Integer(), nullable=False, unique=True, default=None,
478 primary_key=True)
478 primary_key=True)
479 ui_section = Column(
479 ui_section = Column(
480 "ui_section", String(255), nullable=True, unique=None, default=None)
480 "ui_section", String(255), nullable=True, unique=None, default=None)
481 ui_key = Column(
481 ui_key = Column(
482 "ui_key", String(255), nullable=True, unique=None, default=None)
482 "ui_key", String(255), nullable=True, unique=None, default=None)
483 ui_value = Column(
483 ui_value = Column(
484 "ui_value", String(255), nullable=True, unique=None, default=None)
484 "ui_value", String(255), nullable=True, unique=None, default=None)
485 ui_active = Column(
485 ui_active = Column(
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
487
487
488 repository = relationship('Repository')
488 repository = relationship('Repository')
489
489
490 def __repr__(self):
490 def __repr__(self):
491 return '<%s[%s:%s]%s=>%s]>' % (
491 return '<%s[%s:%s]%s=>%s]>' % (
492 self.__class__.__name__, self.repository.repo_name,
492 self.__class__.__name__, self.repository.repo_name,
493 self.ui_section, self.ui_key, self.ui_value)
493 self.ui_section, self.ui_key, self.ui_value)
494
494
495
495
496 class User(Base, BaseModel):
496 class User(Base, BaseModel):
497 __tablename__ = 'users'
497 __tablename__ = 'users'
498 __table_args__ = (
498 __table_args__ = (
499 UniqueConstraint('username'), UniqueConstraint('email'),
499 UniqueConstraint('username'), UniqueConstraint('email'),
500 Index('u_username_idx', 'username'),
500 Index('u_username_idx', 'username'),
501 Index('u_email_idx', 'email'),
501 Index('u_email_idx', 'email'),
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
504 )
504 )
505 DEFAULT_USER = 'default'
505 DEFAULT_USER = 'default'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
508
508
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
510 username = Column("username", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
511 password = Column("password", String(255), nullable=True, unique=None, default=None)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
519
519
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
526
526
527 user_log = relationship('UserLog')
527 user_log = relationship('UserLog')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
529
529
530 repositories = relationship('Repository')
530 repositories = relationship('Repository')
531 repository_groups = relationship('RepoGroup')
531 repository_groups = relationship('RepoGroup')
532 user_groups = relationship('UserGroup')
532 user_groups = relationship('UserGroup')
533
533
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
536
536
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
540
540
541 group_member = relationship('UserGroupMember', cascade='all')
541 group_member = relationship('UserGroupMember', cascade='all')
542
542
543 notifications = relationship('UserNotification', cascade='all')
543 notifications = relationship('UserNotification', cascade='all')
544 # notifications assigned to this user
544 # notifications assigned to this user
545 user_created_notifications = relationship('Notification', cascade='all')
545 user_created_notifications = relationship('Notification', cascade='all')
546 # comments created by this user
546 # comments created by this user
547 user_comments = relationship('ChangesetComment', cascade='all')
547 user_comments = relationship('ChangesetComment', cascade='all')
548 # user profile extra info
548 # user profile extra info
549 user_emails = relationship('UserEmailMap', cascade='all')
549 user_emails = relationship('UserEmailMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
550 user_ip_map = relationship('UserIpMap', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
552 # gists
552 # gists
553 user_gists = relationship('Gist', cascade='all')
553 user_gists = relationship('Gist', cascade='all')
554 # user pull requests
554 # user pull requests
555 user_pull_requests = relationship('PullRequest', cascade='all')
555 user_pull_requests = relationship('PullRequest', cascade='all')
556 # external identities
556 # external identities
557 extenal_identities = relationship(
557 extenal_identities = relationship(
558 'ExternalIdentity',
558 'ExternalIdentity',
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
560 cascade='all')
560 cascade='all')
561
561
562 def __unicode__(self):
562 def __unicode__(self):
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
564 self.user_id, self.username)
564 self.user_id, self.username)
565
565
566 @hybrid_property
566 @hybrid_property
567 def email(self):
567 def email(self):
568 return self._email
568 return self._email
569
569
570 @email.setter
570 @email.setter
571 def email(self, val):
571 def email(self, val):
572 self._email = val.lower() if val else None
572 self._email = val.lower() if val else None
573
573
574 @hybrid_property
574 @hybrid_property
575 def api_key(self):
575 def api_key(self):
576 """
576 """
577 Fetch if exist an auth-token with role ALL connected to this user
577 Fetch if exist an auth-token with role ALL connected to this user
578 """
578 """
579 user_auth_token = UserApiKeys.query()\
579 user_auth_token = UserApiKeys.query()\
580 .filter(UserApiKeys.user_id == self.user_id)\
580 .filter(UserApiKeys.user_id == self.user_id)\
581 .filter(or_(UserApiKeys.expires == -1,
581 .filter(or_(UserApiKeys.expires == -1,
582 UserApiKeys.expires >= time.time()))\
582 UserApiKeys.expires >= time.time()))\
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
583 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
584 if user_auth_token:
584 if user_auth_token:
585 user_auth_token = user_auth_token.api_key
585 user_auth_token = user_auth_token.api_key
586
586
587 return user_auth_token
587 return user_auth_token
588
588
589 @api_key.setter
589 @api_key.setter
590 def api_key(self, val):
590 def api_key(self, val):
591 # don't allow to set API key this is deprecated for now
591 # don't allow to set API key this is deprecated for now
592 self._api_key = None
592 self._api_key = None
593
593
594 @property
594 @property
595 def firstname(self):
595 def firstname(self):
596 # alias for future
596 # alias for future
597 return self.name
597 return self.name
598
598
599 @property
599 @property
600 def emails(self):
600 def emails(self):
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
601 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
602 return [self.email] + [x.email for x in other]
602 return [self.email] + [x.email for x in other]
603
603
604 @property
604 @property
605 def auth_tokens(self):
605 def auth_tokens(self):
606 return [x.api_key for x in self.extra_auth_tokens]
606 return [x.api_key for x in self.extra_auth_tokens]
607
607
608 @property
608 @property
609 def extra_auth_tokens(self):
609 def extra_auth_tokens(self):
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
610 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
611
611
612 @property
612 @property
613 def feed_token(self):
613 def feed_token(self):
614 return self.get_feed_token()
614 return self.get_feed_token()
615
615
616 def get_feed_token(self):
616 def get_feed_token(self):
617 feed_tokens = UserApiKeys.query()\
617 feed_tokens = UserApiKeys.query()\
618 .filter(UserApiKeys.user == self)\
618 .filter(UserApiKeys.user == self)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
619 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
620 .all()
620 .all()
621 if feed_tokens:
621 if feed_tokens:
622 return feed_tokens[0].api_key
622 return feed_tokens[0].api_key
623 return 'NO_FEED_TOKEN_AVAILABLE'
623 return 'NO_FEED_TOKEN_AVAILABLE'
624
624
625 @classmethod
625 @classmethod
626 def extra_valid_auth_tokens(cls, user, role=None):
626 def extra_valid_auth_tokens(cls, user, role=None):
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
627 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
628 .filter(or_(UserApiKeys.expires == -1,
628 .filter(or_(UserApiKeys.expires == -1,
629 UserApiKeys.expires >= time.time()))
629 UserApiKeys.expires >= time.time()))
630 if role:
630 if role:
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
631 tokens = tokens.filter(or_(UserApiKeys.role == role,
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
632 UserApiKeys.role == UserApiKeys.ROLE_ALL))
633 return tokens.all()
633 return tokens.all()
634
634
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
635 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
636 from rhodecode.lib import auth
636 from rhodecode.lib import auth
637
637
638 log.debug('Trying to authenticate user: %s via auth-token, '
638 log.debug('Trying to authenticate user: %s via auth-token, '
639 'and roles: %s', self, roles)
639 'and roles: %s', self, roles)
640
640
641 if not auth_token:
641 if not auth_token:
642 return False
642 return False
643
643
644 crypto_backend = auth.crypto_backend()
644 crypto_backend = auth.crypto_backend()
645
645
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
646 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
647 tokens_q = UserApiKeys.query()\
647 tokens_q = UserApiKeys.query()\
648 .filter(UserApiKeys.user_id == self.user_id)\
648 .filter(UserApiKeys.user_id == self.user_id)\
649 .filter(or_(UserApiKeys.expires == -1,
649 .filter(or_(UserApiKeys.expires == -1,
650 UserApiKeys.expires >= time.time()))
650 UserApiKeys.expires >= time.time()))
651
651
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
652 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
653
653
654 plain_tokens = []
654 plain_tokens = []
655 hash_tokens = []
655 hash_tokens = []
656
656
657 for token in tokens_q.all():
657 for token in tokens_q.all():
658 # verify scope first
658 # verify scope first
659 if token.repo_id:
659 if token.repo_id:
660 # token has a scope, we need to verify it
660 # token has a scope, we need to verify it
661 if scope_repo_id != token.repo_id:
661 if scope_repo_id != token.repo_id:
662 log.debug(
662 log.debug(
663 'Scope mismatch: token has a set repo scope: %s, '
663 'Scope mismatch: token has a set repo scope: %s, '
664 'and calling scope is:%s, skipping further checks',
664 'and calling scope is:%s, skipping further checks',
665 token.repo, scope_repo_id)
665 token.repo, scope_repo_id)
666 # token has a scope, and it doesn't match, skip token
666 # token has a scope, and it doesn't match, skip token
667 continue
667 continue
668
668
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
669 if token.api_key.startswith(crypto_backend.ENC_PREF):
670 hash_tokens.append(token.api_key)
670 hash_tokens.append(token.api_key)
671 else:
671 else:
672 plain_tokens.append(token.api_key)
672 plain_tokens.append(token.api_key)
673
673
674 is_plain_match = auth_token in plain_tokens
674 is_plain_match = auth_token in plain_tokens
675 if is_plain_match:
675 if is_plain_match:
676 return True
676 return True
677
677
678 for hashed in hash_tokens:
678 for hashed in hash_tokens:
679 # TODO(marcink): this is expensive to calculate, but most secure
679 # TODO(marcink): this is expensive to calculate, but most secure
680 match = crypto_backend.hash_check(auth_token, hashed)
680 match = crypto_backend.hash_check(auth_token, hashed)
681 if match:
681 if match:
682 return True
682 return True
683
683
684 return False
684 return False
685
685
686 @property
686 @property
687 def ip_addresses(self):
687 def ip_addresses(self):
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
688 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
689 return [x.ip_addr for x in ret]
689 return [x.ip_addr for x in ret]
690
690
691 @property
691 @property
692 def username_and_name(self):
692 def username_and_name(self):
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
693 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
694
694
695 @property
695 @property
696 def username_or_name_or_email(self):
696 def username_or_name_or_email(self):
697 full_name = self.full_name if self.full_name is not ' ' else None
697 full_name = self.full_name if self.full_name is not ' ' else None
698 return self.username or full_name or self.email
698 return self.username or full_name or self.email
699
699
700 @property
700 @property
701 def full_name(self):
701 def full_name(self):
702 return '%s %s' % (self.firstname, self.lastname)
702 return '%s %s' % (self.firstname, self.lastname)
703
703
704 @property
704 @property
705 def full_name_or_username(self):
705 def full_name_or_username(self):
706 return ('%s %s' % (self.firstname, self.lastname)
706 return ('%s %s' % (self.firstname, self.lastname)
707 if (self.firstname and self.lastname) else self.username)
707 if (self.firstname and self.lastname) else self.username)
708
708
709 @property
709 @property
710 def full_contact(self):
710 def full_contact(self):
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
711 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
712
712
713 @property
713 @property
714 def short_contact(self):
714 def short_contact(self):
715 return '%s %s' % (self.firstname, self.lastname)
715 return '%s %s' % (self.firstname, self.lastname)
716
716
717 @property
717 @property
718 def is_admin(self):
718 def is_admin(self):
719 return self.admin
719 return self.admin
720
720
721 @property
721 @property
722 def AuthUser(self):
722 def AuthUser(self):
723 """
723 """
724 Returns instance of AuthUser for this user
724 Returns instance of AuthUser for this user
725 """
725 """
726 from rhodecode.lib.auth import AuthUser
726 from rhodecode.lib.auth import AuthUser
727 return AuthUser(user_id=self.user_id, username=self.username)
727 return AuthUser(user_id=self.user_id, username=self.username)
728
728
729 @hybrid_property
729 @hybrid_property
730 def user_data(self):
730 def user_data(self):
731 if not self._user_data:
731 if not self._user_data:
732 return {}
732 return {}
733
733
734 try:
734 try:
735 return json.loads(self._user_data)
735 return json.loads(self._user_data)
736 except TypeError:
736 except TypeError:
737 return {}
737 return {}
738
738
739 @user_data.setter
739 @user_data.setter
740 def user_data(self, val):
740 def user_data(self, val):
741 if not isinstance(val, dict):
741 if not isinstance(val, dict):
742 raise Exception('user_data must be dict, got %s' % type(val))
742 raise Exception('user_data must be dict, got %s' % type(val))
743 try:
743 try:
744 self._user_data = json.dumps(val)
744 self._user_data = json.dumps(val)
745 except Exception:
745 except Exception:
746 log.error(traceback.format_exc())
746 log.error(traceback.format_exc())
747
747
748 @classmethod
748 @classmethod
749 def get_by_username(cls, username, case_insensitive=False,
749 def get_by_username(cls, username, case_insensitive=False,
750 cache=False, identity_cache=False):
750 cache=False, identity_cache=False):
751 session = Session()
751 session = Session()
752
752
753 if case_insensitive:
753 if case_insensitive:
754 q = cls.query().filter(
754 q = cls.query().filter(
755 func.lower(cls.username) == func.lower(username))
755 func.lower(cls.username) == func.lower(username))
756 else:
756 else:
757 q = cls.query().filter(cls.username == username)
757 q = cls.query().filter(cls.username == username)
758
758
759 if cache:
759 if cache:
760 if identity_cache:
760 if identity_cache:
761 val = cls.identity_cache(session, 'username', username)
761 val = cls.identity_cache(session, 'username', username)
762 if val:
762 if val:
763 return val
763 return val
764 else:
764 else:
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
765 cache_key = "get_user_by_name_%s" % _hash_key(username)
766 q = q.options(
766 q = q.options(
767 FromCache("sql_cache_short", cache_key))
767 FromCache("sql_cache_short", cache_key))
768
768
769 return q.scalar()
769 return q.scalar()
770
770
771 @classmethod
771 @classmethod
772 def get_by_auth_token(cls, auth_token, cache=False):
772 def get_by_auth_token(cls, auth_token, cache=False):
773 q = UserApiKeys.query()\
773 q = UserApiKeys.query()\
774 .filter(UserApiKeys.api_key == auth_token)\
774 .filter(UserApiKeys.api_key == auth_token)\
775 .filter(or_(UserApiKeys.expires == -1,
775 .filter(or_(UserApiKeys.expires == -1,
776 UserApiKeys.expires >= time.time()))
776 UserApiKeys.expires >= time.time()))
777 if cache:
777 if cache:
778 q = q.options(
778 q = q.options(
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
779 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
780
780
781 match = q.first()
781 match = q.first()
782 if match:
782 if match:
783 return match.user
783 return match.user
784
784
785 @classmethod
785 @classmethod
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
786 def get_by_email(cls, email, case_insensitive=False, cache=False):
787
787
788 if case_insensitive:
788 if case_insensitive:
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
789 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
790
790
791 else:
791 else:
792 q = cls.query().filter(cls.email == email)
792 q = cls.query().filter(cls.email == email)
793
793
794 email_key = _hash_key(email)
794 email_key = _hash_key(email)
795 if cache:
795 if cache:
796 q = q.options(
796 q = q.options(
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
797 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
798
798
799 ret = q.scalar()
799 ret = q.scalar()
800 if ret is None:
800 if ret is None:
801 q = UserEmailMap.query()
801 q = UserEmailMap.query()
802 # try fetching in alternate email map
802 # try fetching in alternate email map
803 if case_insensitive:
803 if case_insensitive:
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
804 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
805 else:
805 else:
806 q = q.filter(UserEmailMap.email == email)
806 q = q.filter(UserEmailMap.email == email)
807 q = q.options(joinedload(UserEmailMap.user))
807 q = q.options(joinedload(UserEmailMap.user))
808 if cache:
808 if cache:
809 q = q.options(
809 q = q.options(
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
810 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
811 ret = getattr(q.scalar(), 'user', None)
811 ret = getattr(q.scalar(), 'user', None)
812
812
813 return ret
813 return ret
814
814
815 @classmethod
815 @classmethod
816 def get_from_cs_author(cls, author):
816 def get_from_cs_author(cls, author):
817 """
817 """
818 Tries to get User objects out of commit author string
818 Tries to get User objects out of commit author string
819
819
820 :param author:
820 :param author:
821 """
821 """
822 from rhodecode.lib.helpers import email, author_name
822 from rhodecode.lib.helpers import email, author_name
823 # Valid email in the attribute passed, see if they're in the system
823 # Valid email in the attribute passed, see if they're in the system
824 _email = email(author)
824 _email = email(author)
825 if _email:
825 if _email:
826 user = cls.get_by_email(_email, case_insensitive=True)
826 user = cls.get_by_email(_email, case_insensitive=True)
827 if user:
827 if user:
828 return user
828 return user
829 # Maybe we can match by username?
829 # Maybe we can match by username?
830 _author = author_name(author)
830 _author = author_name(author)
831 user = cls.get_by_username(_author, case_insensitive=True)
831 user = cls.get_by_username(_author, case_insensitive=True)
832 if user:
832 if user:
833 return user
833 return user
834
834
835 def update_userdata(self, **kwargs):
835 def update_userdata(self, **kwargs):
836 usr = self
836 usr = self
837 old = usr.user_data
837 old = usr.user_data
838 old.update(**kwargs)
838 old.update(**kwargs)
839 usr.user_data = old
839 usr.user_data = old
840 Session().add(usr)
840 Session().add(usr)
841 log.debug('updated userdata with ', kwargs)
841 log.debug('updated userdata with ', kwargs)
842
842
843 def update_lastlogin(self):
843 def update_lastlogin(self):
844 """Update user lastlogin"""
844 """Update user lastlogin"""
845 self.last_login = datetime.datetime.now()
845 self.last_login = datetime.datetime.now()
846 Session().add(self)
846 Session().add(self)
847 log.debug('updated user %s lastlogin', self.username)
847 log.debug('updated user %s lastlogin', self.username)
848
848
849 def update_lastactivity(self):
849 def update_lastactivity(self):
850 """Update user lastactivity"""
850 """Update user lastactivity"""
851 self.last_activity = datetime.datetime.now()
851 self.last_activity = datetime.datetime.now()
852 Session().add(self)
852 Session().add(self)
853 log.debug('updated user %s lastactivity', self.username)
853 log.debug('updated user %s lastactivity', self.username)
854
854
855 def update_password(self, new_password):
855 def update_password(self, new_password):
856 from rhodecode.lib.auth import get_crypt_password
856 from rhodecode.lib.auth import get_crypt_password
857
857
858 self.password = get_crypt_password(new_password)
858 self.password = get_crypt_password(new_password)
859 Session().add(self)
859 Session().add(self)
860
860
861 @classmethod
861 @classmethod
862 def get_first_super_admin(cls):
862 def get_first_super_admin(cls):
863 user = User.query().filter(User.admin == true()).first()
863 user = User.query().filter(User.admin == true()).first()
864 if user is None:
864 if user is None:
865 raise Exception('FATAL: Missing administrative account!')
865 raise Exception('FATAL: Missing administrative account!')
866 return user
866 return user
867
867
868 @classmethod
868 @classmethod
869 def get_all_super_admins(cls):
869 def get_all_super_admins(cls):
870 """
870 """
871 Returns all admin accounts sorted by username
871 Returns all admin accounts sorted by username
872 """
872 """
873 return User.query().filter(User.admin == true())\
873 return User.query().filter(User.admin == true())\
874 .order_by(User.username.asc()).all()
874 .order_by(User.username.asc()).all()
875
875
876 @classmethod
876 @classmethod
877 def get_default_user(cls, cache=False, refresh=False):
877 def get_default_user(cls, cache=False, refresh=False):
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
878 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
879 if user is None:
879 if user is None:
880 raise Exception('FATAL: Missing default account!')
880 raise Exception('FATAL: Missing default account!')
881 if refresh:
881 if refresh:
882 # The default user might be based on outdated state which
882 # The default user might be based on outdated state which
883 # has been loaded from the cache.
883 # has been loaded from the cache.
884 # A call to refresh() ensures that the
884 # A call to refresh() ensures that the
885 # latest state from the database is used.
885 # latest state from the database is used.
886 Session().refresh(user)
886 Session().refresh(user)
887 return user
887 return user
888
888
889 def _get_default_perms(self, user, suffix=''):
889 def _get_default_perms(self, user, suffix=''):
890 from rhodecode.model.permission import PermissionModel
890 from rhodecode.model.permission import PermissionModel
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
891 return PermissionModel().get_default_perms(user.user_perms, suffix)
892
892
893 def get_default_perms(self, suffix=''):
893 def get_default_perms(self, suffix=''):
894 return self._get_default_perms(self, suffix)
894 return self._get_default_perms(self, suffix)
895
895
896 def get_api_data(self, include_secrets=False, details='full'):
896 def get_api_data(self, include_secrets=False, details='full'):
897 """
897 """
898 Common function for generating user related data for API
898 Common function for generating user related data for API
899
899
900 :param include_secrets: By default secrets in the API data will be replaced
900 :param include_secrets: By default secrets in the API data will be replaced
901 by a placeholder value to prevent exposing this data by accident. In case
901 by a placeholder value to prevent exposing this data by accident. In case
902 this data shall be exposed, set this flag to ``True``.
902 this data shall be exposed, set this flag to ``True``.
903
903
904 :param details: details can be 'basic|full' basic gives only a subset of
904 :param details: details can be 'basic|full' basic gives only a subset of
905 the available user information that includes user_id, name and emails.
905 the available user information that includes user_id, name and emails.
906 """
906 """
907 user = self
907 user = self
908 user_data = self.user_data
908 user_data = self.user_data
909 data = {
909 data = {
910 'user_id': user.user_id,
910 'user_id': user.user_id,
911 'username': user.username,
911 'username': user.username,
912 'firstname': user.name,
912 'firstname': user.name,
913 'lastname': user.lastname,
913 'lastname': user.lastname,
914 'email': user.email,
914 'email': user.email,
915 'emails': user.emails,
915 'emails': user.emails,
916 }
916 }
917 if details == 'basic':
917 if details == 'basic':
918 return data
918 return data
919
919
920 api_key_length = 40
920 api_key_length = 40
921 api_key_replacement = '*' * api_key_length
921 api_key_replacement = '*' * api_key_length
922
922
923 extras = {
923 extras = {
924 'api_keys': [api_key_replacement],
924 'api_keys': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
925 'auth_tokens': [api_key_replacement],
926 'active': user.active,
926 'active': user.active,
927 'admin': user.admin,
927 'admin': user.admin,
928 'extern_type': user.extern_type,
928 'extern_type': user.extern_type,
929 'extern_name': user.extern_name,
929 'extern_name': user.extern_name,
930 'last_login': user.last_login,
930 'last_login': user.last_login,
931 'last_activity': user.last_activity,
931 'last_activity': user.last_activity,
932 'ip_addresses': user.ip_addresses,
932 'ip_addresses': user.ip_addresses,
933 'language': user_data.get('language')
933 'language': user_data.get('language')
934 }
934 }
935 data.update(extras)
935 data.update(extras)
936
936
937 if include_secrets:
937 if include_secrets:
938 data['api_keys'] = user.auth_tokens
938 data['api_keys'] = user.auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
939 data['auth_tokens'] = user.extra_auth_tokens
940 return data
940 return data
941
941
942 def __json__(self):
942 def __json__(self):
943 data = {
943 data = {
944 'full_name': self.full_name,
944 'full_name': self.full_name,
945 'full_name_or_username': self.full_name_or_username,
945 'full_name_or_username': self.full_name_or_username,
946 'short_contact': self.short_contact,
946 'short_contact': self.short_contact,
947 'full_contact': self.full_contact,
947 'full_contact': self.full_contact,
948 }
948 }
949 data.update(self.get_api_data())
949 data.update(self.get_api_data())
950 return data
950 return data
951
951
952
952
953 class UserApiKeys(Base, BaseModel):
953 class UserApiKeys(Base, BaseModel):
954 __tablename__ = 'user_api_keys'
954 __tablename__ = 'user_api_keys'
955 __table_args__ = (
955 __table_args__ = (
956 Index('uak_api_key_idx', 'api_key'),
956 Index('uak_api_key_idx', 'api_key'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
957 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
958 UniqueConstraint('api_key'),
958 UniqueConstraint('api_key'),
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
959 {'extend_existing': True, 'mysql_engine': 'InnoDB',
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
960 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
961 )
961 )
962 __mapper_args__ = {}
962 __mapper_args__ = {}
963
963
964 # ApiKey role
964 # ApiKey role
965 ROLE_ALL = 'token_role_all'
965 ROLE_ALL = 'token_role_all'
966 ROLE_HTTP = 'token_role_http'
966 ROLE_HTTP = 'token_role_http'
967 ROLE_VCS = 'token_role_vcs'
967 ROLE_VCS = 'token_role_vcs'
968 ROLE_API = 'token_role_api'
968 ROLE_API = 'token_role_api'
969 ROLE_FEED = 'token_role_feed'
969 ROLE_FEED = 'token_role_feed'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
970 ROLE_PASSWORD_RESET = 'token_password_reset'
971
971
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
972 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
973
973
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
974 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
975 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
976 api_key = Column("api_key", String(255), nullable=False, unique=True)
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
977 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
978 expires = Column('expires', Float(53), nullable=False)
978 expires = Column('expires', Float(53), nullable=False)
979 role = Column('role', String(255), nullable=True)
979 role = Column('role', String(255), nullable=True)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
980 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
981
981
982 # scope columns
982 # scope columns
983 repo_id = Column(
983 repo_id = Column(
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
984 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
985 nullable=True, unique=None, default=None)
985 nullable=True, unique=None, default=None)
986 repo = relationship('Repository', lazy='joined')
986 repo = relationship('Repository', lazy='joined')
987
987
988 repo_group_id = Column(
988 repo_group_id = Column(
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
989 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
990 nullable=True, unique=None, default=None)
990 nullable=True, unique=None, default=None)
991 repo_group = relationship('RepoGroup', lazy='joined')
991 repo_group = relationship('RepoGroup', lazy='joined')
992
992
993 user = relationship('User', lazy='joined')
993 user = relationship('User', lazy='joined')
994
994
995 def __unicode__(self):
995 def __unicode__(self):
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
996 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
997
997
998 def __json__(self):
998 def __json__(self):
999 data = {
999 data = {
1000 'auth_token': self.api_key,
1000 'auth_token': self.api_key,
1001 'role': self.role,
1001 'role': self.role,
1002 'scope': self.scope_humanized,
1002 'scope': self.scope_humanized,
1003 'expired': self.expired
1003 'expired': self.expired
1004 }
1004 }
1005 return data
1005 return data
1006
1006
1007 @property
1007 @property
1008 def expired(self):
1008 def expired(self):
1009 if self.expires == -1:
1009 if self.expires == -1:
1010 return False
1010 return False
1011 return time.time() > self.expires
1011 return time.time() > self.expires
1012
1012
1013 @classmethod
1013 @classmethod
1014 def _get_role_name(cls, role):
1014 def _get_role_name(cls, role):
1015 return {
1015 return {
1016 cls.ROLE_ALL: _('all'),
1016 cls.ROLE_ALL: _('all'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1017 cls.ROLE_HTTP: _('http/web interface'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1018 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1019 cls.ROLE_API: _('api calls'),
1019 cls.ROLE_API: _('api calls'),
1020 cls.ROLE_FEED: _('feed access'),
1020 cls.ROLE_FEED: _('feed access'),
1021 }.get(role, role)
1021 }.get(role, role)
1022
1022
1023 @property
1023 @property
1024 def role_humanized(self):
1024 def role_humanized(self):
1025 return self._get_role_name(self.role)
1025 return self._get_role_name(self.role)
1026
1026
1027 def _get_scope(self):
1027 def _get_scope(self):
1028 if self.repo:
1028 if self.repo:
1029 return repr(self.repo)
1029 return repr(self.repo)
1030 if self.repo_group:
1030 if self.repo_group:
1031 return repr(self.repo_group) + ' (recursive)'
1031 return repr(self.repo_group) + ' (recursive)'
1032 return 'global'
1032 return 'global'
1033
1033
1034 @property
1034 @property
1035 def scope_humanized(self):
1035 def scope_humanized(self):
1036 return self._get_scope()
1036 return self._get_scope()
1037
1037
1038
1038
1039 class UserEmailMap(Base, BaseModel):
1039 class UserEmailMap(Base, BaseModel):
1040 __tablename__ = 'user_email_map'
1040 __tablename__ = 'user_email_map'
1041 __table_args__ = (
1041 __table_args__ = (
1042 Index('uem_email_idx', 'email'),
1042 Index('uem_email_idx', 'email'),
1043 UniqueConstraint('email'),
1043 UniqueConstraint('email'),
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1046 )
1046 )
1047 __mapper_args__ = {}
1047 __mapper_args__ = {}
1048
1048
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1051 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1052 user = relationship('User', lazy='joined')
1052 user = relationship('User', lazy='joined')
1053
1053
1054 @validates('_email')
1054 @validates('_email')
1055 def validate_email(self, key, email):
1055 def validate_email(self, key, email):
1056 # check if this email is not main one
1056 # check if this email is not main one
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1057 main_email = Session().query(User).filter(User.email == email).scalar()
1058 if main_email is not None:
1058 if main_email is not None:
1059 raise AttributeError('email %s is present is user table' % email)
1059 raise AttributeError('email %s is present is user table' % email)
1060 return email
1060 return email
1061
1061
1062 @hybrid_property
1062 @hybrid_property
1063 def email(self):
1063 def email(self):
1064 return self._email
1064 return self._email
1065
1065
1066 @email.setter
1066 @email.setter
1067 def email(self, val):
1067 def email(self, val):
1068 self._email = val.lower() if val else None
1068 self._email = val.lower() if val else None
1069
1069
1070
1070
1071 class UserIpMap(Base, BaseModel):
1071 class UserIpMap(Base, BaseModel):
1072 __tablename__ = 'user_ip_map'
1072 __tablename__ = 'user_ip_map'
1073 __table_args__ = (
1073 __table_args__ = (
1074 UniqueConstraint('user_id', 'ip_addr'),
1074 UniqueConstraint('user_id', 'ip_addr'),
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1075 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1076 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1077 )
1077 )
1078 __mapper_args__ = {}
1078 __mapper_args__ = {}
1079
1079
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1080 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1082 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1083 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1084 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1085 user = relationship('User', lazy='joined')
1085 user = relationship('User', lazy='joined')
1086
1086
1087 @classmethod
1087 @classmethod
1088 def _get_ip_range(cls, ip_addr):
1088 def _get_ip_range(cls, ip_addr):
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1089 net = ipaddress.ip_network(ip_addr, strict=False)
1090 return [str(net.network_address), str(net.broadcast_address)]
1090 return [str(net.network_address), str(net.broadcast_address)]
1091
1091
1092 def __json__(self):
1092 def __json__(self):
1093 return {
1093 return {
1094 'ip_addr': self.ip_addr,
1094 'ip_addr': self.ip_addr,
1095 'ip_range': self._get_ip_range(self.ip_addr),
1095 'ip_range': self._get_ip_range(self.ip_addr),
1096 }
1096 }
1097
1097
1098 def __unicode__(self):
1098 def __unicode__(self):
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1099 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1100 self.user_id, self.ip_addr)
1100 self.user_id, self.ip_addr)
1101
1101
1102
1102
1103 class UserLog(Base, BaseModel):
1103 class UserLog(Base, BaseModel):
1104 __tablename__ = 'user_logs'
1104 __tablename__ = 'user_logs'
1105 __table_args__ = (
1105 __table_args__ = (
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1106 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1107 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1108 )
1108 )
1109 VERSION_1 = 'v1'
1109 VERSION_1 = 'v1'
1110 VERSION_2 = 'v2'
1110 VERSION_2 = 'v2'
1111 VERSIONS = [VERSION_1, VERSION_2]
1111 VERSIONS = [VERSION_1, VERSION_2]
1112
1112
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1113 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1114 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1115 username = Column("username", String(255), nullable=True, unique=None, default=None)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1116 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1117 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1118 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1119 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1120 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1121
1121
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1122 version = Column("version", String(255), nullable=True, default=VERSION_1)
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1123 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1124 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1125
1125
1126 def __unicode__(self):
1126 def __unicode__(self):
1127 return u"<%s('id:%s:%s')>" % (
1127 return u"<%s('id:%s:%s')>" % (
1128 self.__class__.__name__, self.repository_name, self.action)
1128 self.__class__.__name__, self.repository_name, self.action)
1129
1129
1130 def __json__(self):
1130 def __json__(self):
1131 return {
1131 return {
1132 'user_id': self.user_id,
1132 'user_id': self.user_id,
1133 'username': self.username,
1133 'username': self.username,
1134 'repository_id': self.repository_id,
1134 'repository_id': self.repository_id,
1135 'repository_name': self.repository_name,
1135 'repository_name': self.repository_name,
1136 'user_ip': self.user_ip,
1136 'user_ip': self.user_ip,
1137 'action_date': self.action_date,
1137 'action_date': self.action_date,
1138 'action': self.action,
1138 'action': self.action,
1139 }
1139 }
1140
1140
1141 @property
1141 @property
1142 def action_as_day(self):
1142 def action_as_day(self):
1143 return datetime.date(*self.action_date.timetuple()[:3])
1143 return datetime.date(*self.action_date.timetuple()[:3])
1144
1144
1145 user = relationship('User')
1145 user = relationship('User')
1146 repository = relationship('Repository', cascade='')
1146 repository = relationship('Repository', cascade='')
1147
1147
1148
1148
1149 class UserGroup(Base, BaseModel):
1149 class UserGroup(Base, BaseModel):
1150 __tablename__ = 'users_groups'
1150 __tablename__ = 'users_groups'
1151 __table_args__ = (
1151 __table_args__ = (
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1153 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1154 )
1154 )
1155
1155
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1156 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1157 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1158 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1159 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1160 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1161 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1162 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1163 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1164
1164
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1165 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1166 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1167 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1168 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1169 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1170 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1171
1171
1172 user = relationship('User')
1172 user = relationship('User')
1173
1173
1174 @hybrid_property
1174 @hybrid_property
1175 def group_data(self):
1175 def group_data(self):
1176 if not self._group_data:
1176 if not self._group_data:
1177 return {}
1177 return {}
1178
1178
1179 try:
1179 try:
1180 return json.loads(self._group_data)
1180 return json.loads(self._group_data)
1181 except TypeError:
1181 except TypeError:
1182 return {}
1182 return {}
1183
1183
1184 @group_data.setter
1184 @group_data.setter
1185 def group_data(self, val):
1185 def group_data(self, val):
1186 try:
1186 try:
1187 self._group_data = json.dumps(val)
1187 self._group_data = json.dumps(val)
1188 except Exception:
1188 except Exception:
1189 log.error(traceback.format_exc())
1189 log.error(traceback.format_exc())
1190
1190
1191 def __unicode__(self):
1191 def __unicode__(self):
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1192 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1193 self.users_group_id,
1193 self.users_group_id,
1194 self.users_group_name)
1194 self.users_group_name)
1195
1195
1196 @classmethod
1196 @classmethod
1197 def get_by_group_name(cls, group_name, cache=False,
1197 def get_by_group_name(cls, group_name, cache=False,
1198 case_insensitive=False):
1198 case_insensitive=False):
1199 if case_insensitive:
1199 if case_insensitive:
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1200 q = cls.query().filter(func.lower(cls.users_group_name) ==
1201 func.lower(group_name))
1201 func.lower(group_name))
1202
1202
1203 else:
1203 else:
1204 q = cls.query().filter(cls.users_group_name == group_name)
1204 q = cls.query().filter(cls.users_group_name == group_name)
1205 if cache:
1205 if cache:
1206 q = q.options(
1206 q = q.options(
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1207 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1208 return q.scalar()
1208 return q.scalar()
1209
1209
1210 @classmethod
1210 @classmethod
1211 def get(cls, user_group_id, cache=False):
1211 def get(cls, user_group_id, cache=False):
1212 user_group = cls.query()
1212 user_group = cls.query()
1213 if cache:
1213 if cache:
1214 user_group = user_group.options(
1214 user_group = user_group.options(
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1215 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1216 return user_group.get(user_group_id)
1216 return user_group.get(user_group_id)
1217
1217
1218 def permissions(self, with_admins=True, with_owner=True):
1218 def permissions(self, with_admins=True, with_owner=True):
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1219 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1220 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1221 joinedload(UserUserGroupToPerm.user),
1221 joinedload(UserUserGroupToPerm.user),
1222 joinedload(UserUserGroupToPerm.permission),)
1222 joinedload(UserUserGroupToPerm.permission),)
1223
1223
1224 # get owners and admins and permissions. We do a trick of re-writing
1224 # get owners and admins and permissions. We do a trick of re-writing
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1225 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1226 # has a global reference and changing one object propagates to all
1226 # has a global reference and changing one object propagates to all
1227 # others. This means if admin is also an owner admin_row that change
1227 # others. This means if admin is also an owner admin_row that change
1228 # would propagate to both objects
1228 # would propagate to both objects
1229 perm_rows = []
1229 perm_rows = []
1230 for _usr in q.all():
1230 for _usr in q.all():
1231 usr = AttributeDict(_usr.user.get_dict())
1231 usr = AttributeDict(_usr.user.get_dict())
1232 usr.permission = _usr.permission.permission_name
1232 usr.permission = _usr.permission.permission_name
1233 perm_rows.append(usr)
1233 perm_rows.append(usr)
1234
1234
1235 # filter the perm rows by 'default' first and then sort them by
1235 # filter the perm rows by 'default' first and then sort them by
1236 # admin,write,read,none permissions sorted again alphabetically in
1236 # admin,write,read,none permissions sorted again alphabetically in
1237 # each group
1237 # each group
1238 perm_rows = sorted(perm_rows, key=display_sort)
1238 perm_rows = sorted(perm_rows, key=display_sort)
1239
1239
1240 _admin_perm = 'usergroup.admin'
1240 _admin_perm = 'usergroup.admin'
1241 owner_row = []
1241 owner_row = []
1242 if with_owner:
1242 if with_owner:
1243 usr = AttributeDict(self.user.get_dict())
1243 usr = AttributeDict(self.user.get_dict())
1244 usr.owner_row = True
1244 usr.owner_row = True
1245 usr.permission = _admin_perm
1245 usr.permission = _admin_perm
1246 owner_row.append(usr)
1246 owner_row.append(usr)
1247
1247
1248 super_admin_rows = []
1248 super_admin_rows = []
1249 if with_admins:
1249 if with_admins:
1250 for usr in User.get_all_super_admins():
1250 for usr in User.get_all_super_admins():
1251 # if this admin is also owner, don't double the record
1251 # if this admin is also owner, don't double the record
1252 if usr.user_id == owner_row[0].user_id:
1252 if usr.user_id == owner_row[0].user_id:
1253 owner_row[0].admin_row = True
1253 owner_row[0].admin_row = True
1254 else:
1254 else:
1255 usr = AttributeDict(usr.get_dict())
1255 usr = AttributeDict(usr.get_dict())
1256 usr.admin_row = True
1256 usr.admin_row = True
1257 usr.permission = _admin_perm
1257 usr.permission = _admin_perm
1258 super_admin_rows.append(usr)
1258 super_admin_rows.append(usr)
1259
1259
1260 return super_admin_rows + owner_row + perm_rows
1260 return super_admin_rows + owner_row + perm_rows
1261
1261
1262 def permission_user_groups(self):
1262 def permission_user_groups(self):
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1263 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1264 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1265 joinedload(UserGroupUserGroupToPerm.target_user_group),
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1266 joinedload(UserGroupUserGroupToPerm.permission),)
1267
1267
1268 perm_rows = []
1268 perm_rows = []
1269 for _user_group in q.all():
1269 for _user_group in q.all():
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1270 usr = AttributeDict(_user_group.user_group.get_dict())
1271 usr.permission = _user_group.permission.permission_name
1271 usr.permission = _user_group.permission.permission_name
1272 perm_rows.append(usr)
1272 perm_rows.append(usr)
1273
1273
1274 return perm_rows
1274 return perm_rows
1275
1275
1276 def _get_default_perms(self, user_group, suffix=''):
1276 def _get_default_perms(self, user_group, suffix=''):
1277 from rhodecode.model.permission import PermissionModel
1277 from rhodecode.model.permission import PermissionModel
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1278 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1279
1279
1280 def get_default_perms(self, suffix=''):
1280 def get_default_perms(self, suffix=''):
1281 return self._get_default_perms(self, suffix)
1281 return self._get_default_perms(self, suffix)
1282
1282
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1283 def get_api_data(self, with_group_members=True, include_secrets=False):
1284 """
1284 """
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1285 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1286 basically forwarded.
1286 basically forwarded.
1287
1287
1288 """
1288 """
1289 user_group = self
1289 user_group = self
1290 data = {
1290 data = {
1291 'users_group_id': user_group.users_group_id,
1291 'users_group_id': user_group.users_group_id,
1292 'group_name': user_group.users_group_name,
1292 'group_name': user_group.users_group_name,
1293 'group_description': user_group.user_group_description,
1293 'group_description': user_group.user_group_description,
1294 'active': user_group.users_group_active,
1294 'active': user_group.users_group_active,
1295 'owner': user_group.user.username,
1295 'owner': user_group.user.username,
1296 'owner_email': user_group.user.email,
1296 'owner_email': user_group.user.email,
1297 }
1297 }
1298
1298
1299 if with_group_members:
1299 if with_group_members:
1300 users = []
1300 users = []
1301 for user in user_group.members:
1301 for user in user_group.members:
1302 user = user.user
1302 user = user.user
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1303 users.append(user.get_api_data(include_secrets=include_secrets))
1304 data['users'] = users
1304 data['users'] = users
1305
1305
1306 return data
1306 return data
1307
1307
1308
1308
1309 class UserGroupMember(Base, BaseModel):
1309 class UserGroupMember(Base, BaseModel):
1310 __tablename__ = 'users_groups_members'
1310 __tablename__ = 'users_groups_members'
1311 __table_args__ = (
1311 __table_args__ = (
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1314 )
1314 )
1315
1315
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1316 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1317 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1318 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1319
1319
1320 user = relationship('User', lazy='joined')
1320 user = relationship('User', lazy='joined')
1321 users_group = relationship('UserGroup')
1321 users_group = relationship('UserGroup')
1322
1322
1323 def __init__(self, gr_id='', u_id=''):
1323 def __init__(self, gr_id='', u_id=''):
1324 self.users_group_id = gr_id
1324 self.users_group_id = gr_id
1325 self.user_id = u_id
1325 self.user_id = u_id
1326
1326
1327
1327
1328 class RepositoryField(Base, BaseModel):
1328 class RepositoryField(Base, BaseModel):
1329 __tablename__ = 'repositories_fields'
1329 __tablename__ = 'repositories_fields'
1330 __table_args__ = (
1330 __table_args__ = (
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1331 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1334 )
1334 )
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1335 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1336
1336
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1337 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1338 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1339 field_key = Column("field_key", String(250))
1339 field_key = Column("field_key", String(250))
1340 field_label = Column("field_label", String(1024), nullable=False)
1340 field_label = Column("field_label", String(1024), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1341 field_value = Column("field_value", String(10000), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1342 field_desc = Column("field_desc", String(1024), nullable=False)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1343 field_type = Column("field_type", String(255), nullable=False, unique=None)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1344 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1345
1345
1346 repository = relationship('Repository')
1346 repository = relationship('Repository')
1347
1347
1348 @property
1348 @property
1349 def field_key_prefixed(self):
1349 def field_key_prefixed(self):
1350 return 'ex_%s' % self.field_key
1350 return 'ex_%s' % self.field_key
1351
1351
1352 @classmethod
1352 @classmethod
1353 def un_prefix_key(cls, key):
1353 def un_prefix_key(cls, key):
1354 if key.startswith(cls.PREFIX):
1354 if key.startswith(cls.PREFIX):
1355 return key[len(cls.PREFIX):]
1355 return key[len(cls.PREFIX):]
1356 return key
1356 return key
1357
1357
1358 @classmethod
1358 @classmethod
1359 def get_by_key_name(cls, key, repo):
1359 def get_by_key_name(cls, key, repo):
1360 row = cls.query()\
1360 row = cls.query()\
1361 .filter(cls.repository == repo)\
1361 .filter(cls.repository == repo)\
1362 .filter(cls.field_key == key).scalar()
1362 .filter(cls.field_key == key).scalar()
1363 return row
1363 return row
1364
1364
1365
1365
1366 class Repository(Base, BaseModel):
1366 class Repository(Base, BaseModel):
1367 __tablename__ = 'repositories'
1367 __tablename__ = 'repositories'
1368 __table_args__ = (
1368 __table_args__ = (
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1369 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1372 )
1372 )
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1373 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1374 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1375
1375
1376 STATE_CREATED = 'repo_state_created'
1376 STATE_CREATED = 'repo_state_created'
1377 STATE_PENDING = 'repo_state_pending'
1377 STATE_PENDING = 'repo_state_pending'
1378 STATE_ERROR = 'repo_state_error'
1378 STATE_ERROR = 'repo_state_error'
1379
1379
1380 LOCK_AUTOMATIC = 'lock_auto'
1380 LOCK_AUTOMATIC = 'lock_auto'
1381 LOCK_API = 'lock_api'
1381 LOCK_API = 'lock_api'
1382 LOCK_WEB = 'lock_web'
1382 LOCK_WEB = 'lock_web'
1383 LOCK_PULL = 'lock_pull'
1383 LOCK_PULL = 'lock_pull'
1384
1384
1385 NAME_SEP = URL_SEP
1385 NAME_SEP = URL_SEP
1386
1386
1387 repo_id = Column(
1387 repo_id = Column(
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1388 "repo_id", Integer(), nullable=False, unique=True, default=None,
1389 primary_key=True)
1389 primary_key=True)
1390 _repo_name = Column(
1390 _repo_name = Column(
1391 "repo_name", Text(), nullable=False, default=None)
1391 "repo_name", Text(), nullable=False, default=None)
1392 _repo_name_hash = Column(
1392 _repo_name_hash = Column(
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1393 "repo_name_hash", String(255), nullable=False, unique=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1394 repo_state = Column("repo_state", String(255), nullable=True)
1395
1395
1396 clone_uri = Column(
1396 clone_uri = Column(
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1397 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1398 default=None)
1398 default=None)
1399 repo_type = Column(
1399 repo_type = Column(
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1400 "repo_type", String(255), nullable=False, unique=False, default=None)
1401 user_id = Column(
1401 user_id = Column(
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1402 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1403 unique=False, default=None)
1403 unique=False, default=None)
1404 private = Column(
1404 private = Column(
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1405 "private", Boolean(), nullable=True, unique=None, default=None)
1406 enable_statistics = Column(
1406 enable_statistics = Column(
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1407 "statistics", Boolean(), nullable=True, unique=None, default=True)
1408 enable_downloads = Column(
1408 enable_downloads = Column(
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1409 "downloads", Boolean(), nullable=True, unique=None, default=True)
1410 description = Column(
1410 description = Column(
1411 "description", String(10000), nullable=True, unique=None, default=None)
1411 "description", String(10000), nullable=True, unique=None, default=None)
1412 created_on = Column(
1412 created_on = Column(
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1413 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1414 default=datetime.datetime.now)
1414 default=datetime.datetime.now)
1415 updated_on = Column(
1415 updated_on = Column(
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1416 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1417 default=datetime.datetime.now)
1417 default=datetime.datetime.now)
1418 _landing_revision = Column(
1418 _landing_revision = Column(
1419 "landing_revision", String(255), nullable=False, unique=False,
1419 "landing_revision", String(255), nullable=False, unique=False,
1420 default=None)
1420 default=None)
1421 enable_locking = Column(
1421 enable_locking = Column(
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1422 "enable_locking", Boolean(), nullable=False, unique=None,
1423 default=False)
1423 default=False)
1424 _locked = Column(
1424 _locked = Column(
1425 "locked", String(255), nullable=True, unique=False, default=None)
1425 "locked", String(255), nullable=True, unique=False, default=None)
1426 _changeset_cache = Column(
1426 _changeset_cache = Column(
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1427 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1428
1428
1429 fork_id = Column(
1429 fork_id = Column(
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1430 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1431 nullable=True, unique=False, default=None)
1431 nullable=True, unique=False, default=None)
1432 group_id = Column(
1432 group_id = Column(
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1433 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1434 unique=False, default=None)
1434 unique=False, default=None)
1435
1435
1436 user = relationship('User', lazy='joined')
1436 user = relationship('User', lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1437 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1438 group = relationship('RepoGroup', lazy='joined')
1439 repo_to_perm = relationship(
1439 repo_to_perm = relationship(
1440 'UserRepoToPerm', cascade='all',
1440 'UserRepoToPerm', cascade='all',
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1441 order_by='UserRepoToPerm.repo_to_perm_id')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1442 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1443 stats = relationship('Statistics', cascade='all', uselist=False)
1444
1444
1445 followers = relationship(
1445 followers = relationship(
1446 'UserFollowing',
1446 'UserFollowing',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1447 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1448 cascade='all')
1448 cascade='all')
1449 extra_fields = relationship(
1449 extra_fields = relationship(
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1450 'RepositoryField', cascade="all, delete, delete-orphan")
1451 logs = relationship('UserLog')
1451 logs = relationship('UserLog')
1452 comments = relationship(
1452 comments = relationship(
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1453 'ChangesetComment', cascade="all, delete, delete-orphan")
1454 pull_requests_source = relationship(
1454 pull_requests_source = relationship(
1455 'PullRequest',
1455 'PullRequest',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1456 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1457 cascade="all, delete, delete-orphan")
1457 cascade="all, delete, delete-orphan")
1458 pull_requests_target = relationship(
1458 pull_requests_target = relationship(
1459 'PullRequest',
1459 'PullRequest',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1460 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1461 cascade="all, delete, delete-orphan")
1461 cascade="all, delete, delete-orphan")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1462 ui = relationship('RepoRhodeCodeUi', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1463 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1464 integrations = relationship('Integration',
1464 integrations = relationship('Integration',
1465 cascade="all, delete, delete-orphan")
1465 cascade="all, delete, delete-orphan")
1466
1466
1467 def __unicode__(self):
1467 def __unicode__(self):
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1468 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1469 safe_unicode(self.repo_name))
1469 safe_unicode(self.repo_name))
1470
1470
1471 @hybrid_property
1471 @hybrid_property
1472 def landing_rev(self):
1472 def landing_rev(self):
1473 # always should return [rev_type, rev]
1473 # always should return [rev_type, rev]
1474 if self._landing_revision:
1474 if self._landing_revision:
1475 _rev_info = self._landing_revision.split(':')
1475 _rev_info = self._landing_revision.split(':')
1476 if len(_rev_info) < 2:
1476 if len(_rev_info) < 2:
1477 _rev_info.insert(0, 'rev')
1477 _rev_info.insert(0, 'rev')
1478 return [_rev_info[0], _rev_info[1]]
1478 return [_rev_info[0], _rev_info[1]]
1479 return [None, None]
1479 return [None, None]
1480
1480
1481 @landing_rev.setter
1481 @landing_rev.setter
1482 def landing_rev(self, val):
1482 def landing_rev(self, val):
1483 if ':' not in val:
1483 if ':' not in val:
1484 raise ValueError('value must be delimited with `:` and consist '
1484 raise ValueError('value must be delimited with `:` and consist '
1485 'of <rev_type>:<rev>, got %s instead' % val)
1485 'of <rev_type>:<rev>, got %s instead' % val)
1486 self._landing_revision = val
1486 self._landing_revision = val
1487
1487
1488 @hybrid_property
1488 @hybrid_property
1489 def locked(self):
1489 def locked(self):
1490 if self._locked:
1490 if self._locked:
1491 user_id, timelocked, reason = self._locked.split(':')
1491 user_id, timelocked, reason = self._locked.split(':')
1492 lock_values = int(user_id), timelocked, reason
1492 lock_values = int(user_id), timelocked, reason
1493 else:
1493 else:
1494 lock_values = [None, None, None]
1494 lock_values = [None, None, None]
1495 return lock_values
1495 return lock_values
1496
1496
1497 @locked.setter
1497 @locked.setter
1498 def locked(self, val):
1498 def locked(self, val):
1499 if val and isinstance(val, (list, tuple)):
1499 if val and isinstance(val, (list, tuple)):
1500 self._locked = ':'.join(map(str, val))
1500 self._locked = ':'.join(map(str, val))
1501 else:
1501 else:
1502 self._locked = None
1502 self._locked = None
1503
1503
1504 @hybrid_property
1504 @hybrid_property
1505 def changeset_cache(self):
1505 def changeset_cache(self):
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1506 from rhodecode.lib.vcs.backends.base import EmptyCommit
1507 dummy = EmptyCommit().__json__()
1507 dummy = EmptyCommit().__json__()
1508 if not self._changeset_cache:
1508 if not self._changeset_cache:
1509 return dummy
1509 return dummy
1510 try:
1510 try:
1511 return json.loads(self._changeset_cache)
1511 return json.loads(self._changeset_cache)
1512 except TypeError:
1512 except TypeError:
1513 return dummy
1513 return dummy
1514 except Exception:
1514 except Exception:
1515 log.error(traceback.format_exc())
1515 log.error(traceback.format_exc())
1516 return dummy
1516 return dummy
1517
1517
1518 @changeset_cache.setter
1518 @changeset_cache.setter
1519 def changeset_cache(self, val):
1519 def changeset_cache(self, val):
1520 try:
1520 try:
1521 self._changeset_cache = json.dumps(val)
1521 self._changeset_cache = json.dumps(val)
1522 except Exception:
1522 except Exception:
1523 log.error(traceback.format_exc())
1523 log.error(traceback.format_exc())
1524
1524
1525 @hybrid_property
1525 @hybrid_property
1526 def repo_name(self):
1526 def repo_name(self):
1527 return self._repo_name
1527 return self._repo_name
1528
1528
1529 @repo_name.setter
1529 @repo_name.setter
1530 def repo_name(self, value):
1530 def repo_name(self, value):
1531 self._repo_name = value
1531 self._repo_name = value
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1532 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1533
1533
1534 @classmethod
1534 @classmethod
1535 def normalize_repo_name(cls, repo_name):
1535 def normalize_repo_name(cls, repo_name):
1536 """
1536 """
1537 Normalizes os specific repo_name to the format internally stored inside
1537 Normalizes os specific repo_name to the format internally stored inside
1538 database using URL_SEP
1538 database using URL_SEP
1539
1539
1540 :param cls:
1540 :param cls:
1541 :param repo_name:
1541 :param repo_name:
1542 """
1542 """
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1543 return cls.NAME_SEP.join(repo_name.split(os.sep))
1544
1544
1545 @classmethod
1545 @classmethod
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1546 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1547 session = Session()
1547 session = Session()
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1548 q = session.query(cls).filter(cls.repo_name == repo_name)
1549
1549
1550 if cache:
1550 if cache:
1551 if identity_cache:
1551 if identity_cache:
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1552 val = cls.identity_cache(session, 'repo_name', repo_name)
1553 if val:
1553 if val:
1554 return val
1554 return val
1555 else:
1555 else:
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1556 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1557 q = q.options(
1557 q = q.options(
1558 FromCache("sql_cache_short", cache_key))
1558 FromCache("sql_cache_short", cache_key))
1559
1559
1560 return q.scalar()
1560 return q.scalar()
1561
1561
1562 @classmethod
1562 @classmethod
1563 def get_by_full_path(cls, repo_full_path):
1563 def get_by_full_path(cls, repo_full_path):
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1564 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1565 repo_name = cls.normalize_repo_name(repo_name)
1565 repo_name = cls.normalize_repo_name(repo_name)
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1566 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1567
1567
1568 @classmethod
1568 @classmethod
1569 def get_repo_forks(cls, repo_id):
1569 def get_repo_forks(cls, repo_id):
1570 return cls.query().filter(Repository.fork_id == repo_id)
1570 return cls.query().filter(Repository.fork_id == repo_id)
1571
1571
1572 @classmethod
1572 @classmethod
1573 def base_path(cls):
1573 def base_path(cls):
1574 """
1574 """
1575 Returns base path when all repos are stored
1575 Returns base path when all repos are stored
1576
1576
1577 :param cls:
1577 :param cls:
1578 """
1578 """
1579 q = Session().query(RhodeCodeUi)\
1579 q = Session().query(RhodeCodeUi)\
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1580 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1581 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1582 return q.one().ui_value
1582 return q.one().ui_value
1583
1583
1584 @classmethod
1584 @classmethod
1585 def is_valid(cls, repo_name):
1585 def is_valid(cls, repo_name):
1586 """
1586 """
1587 returns True if given repo name is a valid filesystem repository
1587 returns True if given repo name is a valid filesystem repository
1588
1588
1589 :param cls:
1589 :param cls:
1590 :param repo_name:
1590 :param repo_name:
1591 """
1591 """
1592 from rhodecode.lib.utils import is_valid_repo
1592 from rhodecode.lib.utils import is_valid_repo
1593
1593
1594 return is_valid_repo(repo_name, cls.base_path())
1594 return is_valid_repo(repo_name, cls.base_path())
1595
1595
1596 @classmethod
1596 @classmethod
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1597 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1598 case_insensitive=True):
1598 case_insensitive=True):
1599 q = Repository.query()
1599 q = Repository.query()
1600
1600
1601 if not isinstance(user_id, Optional):
1601 if not isinstance(user_id, Optional):
1602 q = q.filter(Repository.user_id == user_id)
1602 q = q.filter(Repository.user_id == user_id)
1603
1603
1604 if not isinstance(group_id, Optional):
1604 if not isinstance(group_id, Optional):
1605 q = q.filter(Repository.group_id == group_id)
1605 q = q.filter(Repository.group_id == group_id)
1606
1606
1607 if case_insensitive:
1607 if case_insensitive:
1608 q = q.order_by(func.lower(Repository.repo_name))
1608 q = q.order_by(func.lower(Repository.repo_name))
1609 else:
1609 else:
1610 q = q.order_by(Repository.repo_name)
1610 q = q.order_by(Repository.repo_name)
1611 return q.all()
1611 return q.all()
1612
1612
1613 @property
1613 @property
1614 def forks(self):
1614 def forks(self):
1615 """
1615 """
1616 Return forks of this repo
1616 Return forks of this repo
1617 """
1617 """
1618 return Repository.get_repo_forks(self.repo_id)
1618 return Repository.get_repo_forks(self.repo_id)
1619
1619
1620 @property
1620 @property
1621 def parent(self):
1621 def parent(self):
1622 """
1622 """
1623 Returns fork parent
1623 Returns fork parent
1624 """
1624 """
1625 return self.fork
1625 return self.fork
1626
1626
1627 @property
1627 @property
1628 def just_name(self):
1628 def just_name(self):
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1629 return self.repo_name.split(self.NAME_SEP)[-1]
1630
1630
1631 @property
1631 @property
1632 def groups_with_parents(self):
1632 def groups_with_parents(self):
1633 groups = []
1633 groups = []
1634 if self.group is None:
1634 if self.group is None:
1635 return groups
1635 return groups
1636
1636
1637 cur_gr = self.group
1637 cur_gr = self.group
1638 groups.insert(0, cur_gr)
1638 groups.insert(0, cur_gr)
1639 while 1:
1639 while 1:
1640 gr = getattr(cur_gr, 'parent_group', None)
1640 gr = getattr(cur_gr, 'parent_group', None)
1641 cur_gr = cur_gr.parent_group
1641 cur_gr = cur_gr.parent_group
1642 if gr is None:
1642 if gr is None:
1643 break
1643 break
1644 groups.insert(0, gr)
1644 groups.insert(0, gr)
1645
1645
1646 return groups
1646 return groups
1647
1647
1648 @property
1648 @property
1649 def groups_and_repo(self):
1649 def groups_and_repo(self):
1650 return self.groups_with_parents, self
1650 return self.groups_with_parents, self
1651
1651
1652 @LazyProperty
1652 @LazyProperty
1653 def repo_path(self):
1653 def repo_path(self):
1654 """
1654 """
1655 Returns base full path for that repository means where it actually
1655 Returns base full path for that repository means where it actually
1656 exists on a filesystem
1656 exists on a filesystem
1657 """
1657 """
1658 q = Session().query(RhodeCodeUi).filter(
1658 q = Session().query(RhodeCodeUi).filter(
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1659 RhodeCodeUi.ui_key == self.NAME_SEP)
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1660 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1661 return q.one().ui_value
1661 return q.one().ui_value
1662
1662
1663 @property
1663 @property
1664 def repo_full_path(self):
1664 def repo_full_path(self):
1665 p = [self.repo_path]
1665 p = [self.repo_path]
1666 # we need to split the name by / since this is how we store the
1666 # we need to split the name by / since this is how we store the
1667 # names in the database, but that eventually needs to be converted
1667 # names in the database, but that eventually needs to be converted
1668 # into a valid system path
1668 # into a valid system path
1669 p += self.repo_name.split(self.NAME_SEP)
1669 p += self.repo_name.split(self.NAME_SEP)
1670 return os.path.join(*map(safe_unicode, p))
1670 return os.path.join(*map(safe_unicode, p))
1671
1671
1672 @property
1672 @property
1673 def cache_keys(self):
1673 def cache_keys(self):
1674 """
1674 """
1675 Returns associated cache keys for that repo
1675 Returns associated cache keys for that repo
1676 """
1676 """
1677 return CacheKey.query()\
1677 return CacheKey.query()\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1678 .filter(CacheKey.cache_args == self.repo_name)\
1679 .order_by(CacheKey.cache_key)\
1679 .order_by(CacheKey.cache_key)\
1680 .all()
1680 .all()
1681
1681
1682 def get_new_name(self, repo_name):
1682 def get_new_name(self, repo_name):
1683 """
1683 """
1684 returns new full repository name based on assigned group and new new
1684 returns new full repository name based on assigned group and new new
1685
1685
1686 :param group_name:
1686 :param group_name:
1687 """
1687 """
1688 path_prefix = self.group.full_path_splitted if self.group else []
1688 path_prefix = self.group.full_path_splitted if self.group else []
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1689 return self.NAME_SEP.join(path_prefix + [repo_name])
1690
1690
1691 @property
1691 @property
1692 def _config(self):
1692 def _config(self):
1693 """
1693 """
1694 Returns db based config object.
1694 Returns db based config object.
1695 """
1695 """
1696 from rhodecode.lib.utils import make_db_config
1696 from rhodecode.lib.utils import make_db_config
1697 return make_db_config(clear_session=False, repo=self)
1697 return make_db_config(clear_session=False, repo=self)
1698
1698
1699 def permissions(self, with_admins=True, with_owner=True):
1699 def permissions(self, with_admins=True, with_owner=True):
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1700 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1701 q = q.options(joinedload(UserRepoToPerm.repository),
1702 joinedload(UserRepoToPerm.user),
1702 joinedload(UserRepoToPerm.user),
1703 joinedload(UserRepoToPerm.permission),)
1703 joinedload(UserRepoToPerm.permission),)
1704
1704
1705 # get owners and admins and permissions. We do a trick of re-writing
1705 # get owners and admins and permissions. We do a trick of re-writing
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1706 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1707 # has a global reference and changing one object propagates to all
1707 # has a global reference and changing one object propagates to all
1708 # others. This means if admin is also an owner admin_row that change
1708 # others. This means if admin is also an owner admin_row that change
1709 # would propagate to both objects
1709 # would propagate to both objects
1710 perm_rows = []
1710 perm_rows = []
1711 for _usr in q.all():
1711 for _usr in q.all():
1712 usr = AttributeDict(_usr.user.get_dict())
1712 usr = AttributeDict(_usr.user.get_dict())
1713 usr.permission = _usr.permission.permission_name
1713 usr.permission = _usr.permission.permission_name
1714 perm_rows.append(usr)
1714 perm_rows.append(usr)
1715
1715
1716 # filter the perm rows by 'default' first and then sort them by
1716 # filter the perm rows by 'default' first and then sort them by
1717 # admin,write,read,none permissions sorted again alphabetically in
1717 # admin,write,read,none permissions sorted again alphabetically in
1718 # each group
1718 # each group
1719 perm_rows = sorted(perm_rows, key=display_sort)
1719 perm_rows = sorted(perm_rows, key=display_sort)
1720
1720
1721 _admin_perm = 'repository.admin'
1721 _admin_perm = 'repository.admin'
1722 owner_row = []
1722 owner_row = []
1723 if with_owner:
1723 if with_owner:
1724 usr = AttributeDict(self.user.get_dict())
1724 usr = AttributeDict(self.user.get_dict())
1725 usr.owner_row = True
1725 usr.owner_row = True
1726 usr.permission = _admin_perm
1726 usr.permission = _admin_perm
1727 owner_row.append(usr)
1727 owner_row.append(usr)
1728
1728
1729 super_admin_rows = []
1729 super_admin_rows = []
1730 if with_admins:
1730 if with_admins:
1731 for usr in User.get_all_super_admins():
1731 for usr in User.get_all_super_admins():
1732 # if this admin is also owner, don't double the record
1732 # if this admin is also owner, don't double the record
1733 if usr.user_id == owner_row[0].user_id:
1733 if usr.user_id == owner_row[0].user_id:
1734 owner_row[0].admin_row = True
1734 owner_row[0].admin_row = True
1735 else:
1735 else:
1736 usr = AttributeDict(usr.get_dict())
1736 usr = AttributeDict(usr.get_dict())
1737 usr.admin_row = True
1737 usr.admin_row = True
1738 usr.permission = _admin_perm
1738 usr.permission = _admin_perm
1739 super_admin_rows.append(usr)
1739 super_admin_rows.append(usr)
1740
1740
1741 return super_admin_rows + owner_row + perm_rows
1741 return super_admin_rows + owner_row + perm_rows
1742
1742
1743 def permission_user_groups(self):
1743 def permission_user_groups(self):
1744 q = UserGroupRepoToPerm.query().filter(
1744 q = UserGroupRepoToPerm.query().filter(
1745 UserGroupRepoToPerm.repository == self)
1745 UserGroupRepoToPerm.repository == self)
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1746 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1747 joinedload(UserGroupRepoToPerm.users_group),
1747 joinedload(UserGroupRepoToPerm.users_group),
1748 joinedload(UserGroupRepoToPerm.permission),)
1748 joinedload(UserGroupRepoToPerm.permission),)
1749
1749
1750 perm_rows = []
1750 perm_rows = []
1751 for _user_group in q.all():
1751 for _user_group in q.all():
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1752 usr = AttributeDict(_user_group.users_group.get_dict())
1753 usr.permission = _user_group.permission.permission_name
1753 usr.permission = _user_group.permission.permission_name
1754 perm_rows.append(usr)
1754 perm_rows.append(usr)
1755
1755
1756 return perm_rows
1756 return perm_rows
1757
1757
1758 def get_api_data(self, include_secrets=False):
1758 def get_api_data(self, include_secrets=False):
1759 """
1759 """
1760 Common function for generating repo api data
1760 Common function for generating repo api data
1761
1761
1762 :param include_secrets: See :meth:`User.get_api_data`.
1762 :param include_secrets: See :meth:`User.get_api_data`.
1763
1763
1764 """
1764 """
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1765 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1766 # move this methods on models level.
1766 # move this methods on models level.
1767 from rhodecode.model.settings import SettingsModel
1767 from rhodecode.model.settings import SettingsModel
1768
1768
1769 repo = self
1769 repo = self
1770 _user_id, _time, _reason = self.locked
1770 _user_id, _time, _reason = self.locked
1771
1771
1772 data = {
1772 data = {
1773 'repo_id': repo.repo_id,
1773 'repo_id': repo.repo_id,
1774 'repo_name': repo.repo_name,
1774 'repo_name': repo.repo_name,
1775 'repo_type': repo.repo_type,
1775 'repo_type': repo.repo_type,
1776 'clone_uri': repo.clone_uri or '',
1776 'clone_uri': repo.clone_uri or '',
1777 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1777 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1778 'private': repo.private,
1778 'private': repo.private,
1779 'created_on': repo.created_on,
1779 'created_on': repo.created_on,
1780 'description': repo.description,
1780 'description': repo.description,
1781 'landing_rev': repo.landing_rev,
1781 'landing_rev': repo.landing_rev,
1782 'owner': repo.user.username,
1782 'owner': repo.user.username,
1783 'fork_of': repo.fork.repo_name if repo.fork else None,
1783 'fork_of': repo.fork.repo_name if repo.fork else None,
1784 'enable_statistics': repo.enable_statistics,
1784 'enable_statistics': repo.enable_statistics,
1785 'enable_locking': repo.enable_locking,
1785 'enable_locking': repo.enable_locking,
1786 'enable_downloads': repo.enable_downloads,
1786 'enable_downloads': repo.enable_downloads,
1787 'last_changeset': repo.changeset_cache,
1787 'last_changeset': repo.changeset_cache,
1788 'locked_by': User.get(_user_id).get_api_data(
1788 'locked_by': User.get(_user_id).get_api_data(
1789 include_secrets=include_secrets) if _user_id else None,
1789 include_secrets=include_secrets) if _user_id else None,
1790 'locked_date': time_to_datetime(_time) if _time else None,
1790 'locked_date': time_to_datetime(_time) if _time else None,
1791 'lock_reason': _reason if _reason else None,
1791 'lock_reason': _reason if _reason else None,
1792 }
1792 }
1793
1793
1794 # TODO: mikhail: should be per-repo settings here
1794 # TODO: mikhail: should be per-repo settings here
1795 rc_config = SettingsModel().get_all_settings()
1795 rc_config = SettingsModel().get_all_settings()
1796 repository_fields = str2bool(
1796 repository_fields = str2bool(
1797 rc_config.get('rhodecode_repository_fields'))
1797 rc_config.get('rhodecode_repository_fields'))
1798 if repository_fields:
1798 if repository_fields:
1799 for f in self.extra_fields:
1799 for f in self.extra_fields:
1800 data[f.field_key_prefixed] = f.field_value
1800 data[f.field_key_prefixed] = f.field_value
1801
1801
1802 return data
1802 return data
1803
1803
1804 @classmethod
1804 @classmethod
1805 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1805 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1806 if not lock_time:
1806 if not lock_time:
1807 lock_time = time.time()
1807 lock_time = time.time()
1808 if not lock_reason:
1808 if not lock_reason:
1809 lock_reason = cls.LOCK_AUTOMATIC
1809 lock_reason = cls.LOCK_AUTOMATIC
1810 repo.locked = [user_id, lock_time, lock_reason]
1810 repo.locked = [user_id, lock_time, lock_reason]
1811 Session().add(repo)
1811 Session().add(repo)
1812 Session().commit()
1812 Session().commit()
1813
1813
1814 @classmethod
1814 @classmethod
1815 def unlock(cls, repo):
1815 def unlock(cls, repo):
1816 repo.locked = None
1816 repo.locked = None
1817 Session().add(repo)
1817 Session().add(repo)
1818 Session().commit()
1818 Session().commit()
1819
1819
1820 @classmethod
1820 @classmethod
1821 def getlock(cls, repo):
1821 def getlock(cls, repo):
1822 return repo.locked
1822 return repo.locked
1823
1823
1824 def is_user_lock(self, user_id):
1824 def is_user_lock(self, user_id):
1825 if self.lock[0]:
1825 if self.lock[0]:
1826 lock_user_id = safe_int(self.lock[0])
1826 lock_user_id = safe_int(self.lock[0])
1827 user_id = safe_int(user_id)
1827 user_id = safe_int(user_id)
1828 # both are ints, and they are equal
1828 # both are ints, and they are equal
1829 return all([lock_user_id, user_id]) and lock_user_id == user_id
1829 return all([lock_user_id, user_id]) and lock_user_id == user_id
1830
1830
1831 return False
1831 return False
1832
1832
1833 def get_locking_state(self, action, user_id, only_when_enabled=True):
1833 def get_locking_state(self, action, user_id, only_when_enabled=True):
1834 """
1834 """
1835 Checks locking on this repository, if locking is enabled and lock is
1835 Checks locking on this repository, if locking is enabled and lock is
1836 present returns a tuple of make_lock, locked, locked_by.
1836 present returns a tuple of make_lock, locked, locked_by.
1837 make_lock can have 3 states None (do nothing) True, make lock
1837 make_lock can have 3 states None (do nothing) True, make lock
1838 False release lock, This value is later propagated to hooks, which
1838 False release lock, This value is later propagated to hooks, which
1839 do the locking. Think about this as signals passed to hooks what to do.
1839 do the locking. Think about this as signals passed to hooks what to do.
1840
1840
1841 """
1841 """
1842 # TODO: johbo: This is part of the business logic and should be moved
1842 # TODO: johbo: This is part of the business logic and should be moved
1843 # into the RepositoryModel.
1843 # into the RepositoryModel.
1844
1844
1845 if action not in ('push', 'pull'):
1845 if action not in ('push', 'pull'):
1846 raise ValueError("Invalid action value: %s" % repr(action))
1846 raise ValueError("Invalid action value: %s" % repr(action))
1847
1847
1848 # defines if locked error should be thrown to user
1848 # defines if locked error should be thrown to user
1849 currently_locked = False
1849 currently_locked = False
1850 # defines if new lock should be made, tri-state
1850 # defines if new lock should be made, tri-state
1851 make_lock = None
1851 make_lock = None
1852 repo = self
1852 repo = self
1853 user = User.get(user_id)
1853 user = User.get(user_id)
1854
1854
1855 lock_info = repo.locked
1855 lock_info = repo.locked
1856
1856
1857 if repo and (repo.enable_locking or not only_when_enabled):
1857 if repo and (repo.enable_locking or not only_when_enabled):
1858 if action == 'push':
1858 if action == 'push':
1859 # check if it's already locked !, if it is compare users
1859 # check if it's already locked !, if it is compare users
1860 locked_by_user_id = lock_info[0]
1860 locked_by_user_id = lock_info[0]
1861 if user.user_id == locked_by_user_id:
1861 if user.user_id == locked_by_user_id:
1862 log.debug(
1862 log.debug(
1863 'Got `push` action from user %s, now unlocking', user)
1863 'Got `push` action from user %s, now unlocking', user)
1864 # unlock if we have push from user who locked
1864 # unlock if we have push from user who locked
1865 make_lock = False
1865 make_lock = False
1866 else:
1866 else:
1867 # we're not the same user who locked, ban with
1867 # we're not the same user who locked, ban with
1868 # code defined in settings (default is 423 HTTP Locked) !
1868 # code defined in settings (default is 423 HTTP Locked) !
1869 log.debug('Repo %s is currently locked by %s', repo, user)
1869 log.debug('Repo %s is currently locked by %s', repo, user)
1870 currently_locked = True
1870 currently_locked = True
1871 elif action == 'pull':
1871 elif action == 'pull':
1872 # [0] user [1] date
1872 # [0] user [1] date
1873 if lock_info[0] and lock_info[1]:
1873 if lock_info[0] and lock_info[1]:
1874 log.debug('Repo %s is currently locked by %s', repo, user)
1874 log.debug('Repo %s is currently locked by %s', repo, user)
1875 currently_locked = True
1875 currently_locked = True
1876 else:
1876 else:
1877 log.debug('Setting lock on repo %s by %s', repo, user)
1877 log.debug('Setting lock on repo %s by %s', repo, user)
1878 make_lock = True
1878 make_lock = True
1879
1879
1880 else:
1880 else:
1881 log.debug('Repository %s do not have locking enabled', repo)
1881 log.debug('Repository %s do not have locking enabled', repo)
1882
1882
1883 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1883 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1884 make_lock, currently_locked, lock_info)
1884 make_lock, currently_locked, lock_info)
1885
1885
1886 from rhodecode.lib.auth import HasRepoPermissionAny
1886 from rhodecode.lib.auth import HasRepoPermissionAny
1887 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1887 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1888 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1888 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1889 # if we don't have at least write permission we cannot make a lock
1889 # if we don't have at least write permission we cannot make a lock
1890 log.debug('lock state reset back to FALSE due to lack '
1890 log.debug('lock state reset back to FALSE due to lack '
1891 'of at least read permission')
1891 'of at least read permission')
1892 make_lock = False
1892 make_lock = False
1893
1893
1894 return make_lock, currently_locked, lock_info
1894 return make_lock, currently_locked, lock_info
1895
1895
1896 @property
1896 @property
1897 def last_db_change(self):
1897 def last_db_change(self):
1898 return self.updated_on
1898 return self.updated_on
1899
1899
1900 @property
1900 @property
1901 def clone_uri_hidden(self):
1901 def clone_uri_hidden(self):
1902 clone_uri = self.clone_uri
1902 clone_uri = self.clone_uri
1903 if clone_uri:
1903 if clone_uri:
1904 import urlobject
1904 import urlobject
1905 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1905 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1906 if url_obj.password:
1906 if url_obj.password:
1907 clone_uri = url_obj.with_password('*****')
1907 clone_uri = url_obj.with_password('*****')
1908 return clone_uri
1908 return clone_uri
1909
1909
1910 def clone_url(self, **override):
1910 def clone_url(self, **override):
1911 qualified_home_url = url('home', qualified=True)
1911 qualified_home_url = url('home', qualified=True)
1912
1912
1913 uri_tmpl = None
1913 uri_tmpl = None
1914 if 'with_id' in override:
1914 if 'with_id' in override:
1915 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1915 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1916 del override['with_id']
1916 del override['with_id']
1917
1917
1918 if 'uri_tmpl' in override:
1918 if 'uri_tmpl' in override:
1919 uri_tmpl = override['uri_tmpl']
1919 uri_tmpl = override['uri_tmpl']
1920 del override['uri_tmpl']
1920 del override['uri_tmpl']
1921
1921
1922 # we didn't override our tmpl from **overrides
1922 # we didn't override our tmpl from **overrides
1923 if not uri_tmpl:
1923 if not uri_tmpl:
1924 uri_tmpl = self.DEFAULT_CLONE_URI
1924 uri_tmpl = self.DEFAULT_CLONE_URI
1925 try:
1925 try:
1926 from pylons import tmpl_context as c
1926 from pylons import tmpl_context as c
1927 uri_tmpl = c.clone_uri_tmpl
1927 uri_tmpl = c.clone_uri_tmpl
1928 except Exception:
1928 except Exception:
1929 # in any case if we call this outside of request context,
1929 # in any case if we call this outside of request context,
1930 # ie, not having tmpl_context set up
1930 # ie, not having tmpl_context set up
1931 pass
1931 pass
1932
1932
1933 return get_clone_url(uri_tmpl=uri_tmpl,
1933 return get_clone_url(uri_tmpl=uri_tmpl,
1934 qualifed_home_url=qualified_home_url,
1934 qualifed_home_url=qualified_home_url,
1935 repo_name=self.repo_name,
1935 repo_name=self.repo_name,
1936 repo_id=self.repo_id, **override)
1936 repo_id=self.repo_id, **override)
1937
1937
1938 def set_state(self, state):
1938 def set_state(self, state):
1939 self.repo_state = state
1939 self.repo_state = state
1940 Session().add(self)
1940 Session().add(self)
1941 #==========================================================================
1941 #==========================================================================
1942 # SCM PROPERTIES
1942 # SCM PROPERTIES
1943 #==========================================================================
1943 #==========================================================================
1944
1944
1945 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1945 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1946 return get_commit_safe(
1946 return get_commit_safe(
1947 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1947 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1948
1948
1949 def get_changeset(self, rev=None, pre_load=None):
1949 def get_changeset(self, rev=None, pre_load=None):
1950 warnings.warn("Use get_commit", DeprecationWarning)
1950 warnings.warn("Use get_commit", DeprecationWarning)
1951 commit_id = None
1951 commit_id = None
1952 commit_idx = None
1952 commit_idx = None
1953 if isinstance(rev, basestring):
1953 if isinstance(rev, basestring):
1954 commit_id = rev
1954 commit_id = rev
1955 else:
1955 else:
1956 commit_idx = rev
1956 commit_idx = rev
1957 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1957 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1958 pre_load=pre_load)
1958 pre_load=pre_load)
1959
1959
1960 def get_landing_commit(self):
1960 def get_landing_commit(self):
1961 """
1961 """
1962 Returns landing commit, or if that doesn't exist returns the tip
1962 Returns landing commit, or if that doesn't exist returns the tip
1963 """
1963 """
1964 _rev_type, _rev = self.landing_rev
1964 _rev_type, _rev = self.landing_rev
1965 commit = self.get_commit(_rev)
1965 commit = self.get_commit(_rev)
1966 if isinstance(commit, EmptyCommit):
1966 if isinstance(commit, EmptyCommit):
1967 return self.get_commit()
1967 return self.get_commit()
1968 return commit
1968 return commit
1969
1969
1970 def update_commit_cache(self, cs_cache=None, config=None):
1970 def update_commit_cache(self, cs_cache=None, config=None):
1971 """
1971 """
1972 Update cache of last changeset for repository, keys should be::
1972 Update cache of last changeset for repository, keys should be::
1973
1973
1974 short_id
1974 short_id
1975 raw_id
1975 raw_id
1976 revision
1976 revision
1977 parents
1977 parents
1978 message
1978 message
1979 date
1979 date
1980 author
1980 author
1981
1981
1982 :param cs_cache:
1982 :param cs_cache:
1983 """
1983 """
1984 from rhodecode.lib.vcs.backends.base import BaseChangeset
1984 from rhodecode.lib.vcs.backends.base import BaseChangeset
1985 if cs_cache is None:
1985 if cs_cache is None:
1986 # use no-cache version here
1986 # use no-cache version here
1987 scm_repo = self.scm_instance(cache=False, config=config)
1987 scm_repo = self.scm_instance(cache=False, config=config)
1988 if scm_repo:
1988 if scm_repo:
1989 cs_cache = scm_repo.get_commit(
1989 cs_cache = scm_repo.get_commit(
1990 pre_load=["author", "date", "message", "parents"])
1990 pre_load=["author", "date", "message", "parents"])
1991 else:
1991 else:
1992 cs_cache = EmptyCommit()
1992 cs_cache = EmptyCommit()
1993
1993
1994 if isinstance(cs_cache, BaseChangeset):
1994 if isinstance(cs_cache, BaseChangeset):
1995 cs_cache = cs_cache.__json__()
1995 cs_cache = cs_cache.__json__()
1996
1996
1997 def is_outdated(new_cs_cache):
1997 def is_outdated(new_cs_cache):
1998 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1998 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1999 new_cs_cache['revision'] != self.changeset_cache['revision']):
1999 new_cs_cache['revision'] != self.changeset_cache['revision']):
2000 return True
2000 return True
2001 return False
2001 return False
2002
2002
2003 # check if we have maybe already latest cached revision
2003 # check if we have maybe already latest cached revision
2004 if is_outdated(cs_cache) or not self.changeset_cache:
2004 if is_outdated(cs_cache) or not self.changeset_cache:
2005 _default = datetime.datetime.fromtimestamp(0)
2005 _default = datetime.datetime.fromtimestamp(0)
2006 last_change = cs_cache.get('date') or _default
2006 last_change = cs_cache.get('date') or _default
2007 log.debug('updated repo %s with new cs cache %s',
2007 log.debug('updated repo %s with new cs cache %s',
2008 self.repo_name, cs_cache)
2008 self.repo_name, cs_cache)
2009 self.updated_on = last_change
2009 self.updated_on = last_change
2010 self.changeset_cache = cs_cache
2010 self.changeset_cache = cs_cache
2011 Session().add(self)
2011 Session().add(self)
2012 Session().commit()
2012 Session().commit()
2013 else:
2013 else:
2014 log.debug('Skipping update_commit_cache for repo:`%s` '
2014 log.debug('Skipping update_commit_cache for repo:`%s` '
2015 'commit already with latest changes', self.repo_name)
2015 'commit already with latest changes', self.repo_name)
2016
2016
2017 @property
2017 @property
2018 def tip(self):
2018 def tip(self):
2019 return self.get_commit('tip')
2019 return self.get_commit('tip')
2020
2020
2021 @property
2021 @property
2022 def author(self):
2022 def author(self):
2023 return self.tip.author
2023 return self.tip.author
2024
2024
2025 @property
2025 @property
2026 def last_change(self):
2026 def last_change(self):
2027 return self.scm_instance().last_change
2027 return self.scm_instance().last_change
2028
2028
2029 def get_comments(self, revisions=None):
2029 def get_comments(self, revisions=None):
2030 """
2030 """
2031 Returns comments for this repository grouped by revisions
2031 Returns comments for this repository grouped by revisions
2032
2032
2033 :param revisions: filter query by revisions only
2033 :param revisions: filter query by revisions only
2034 """
2034 """
2035 cmts = ChangesetComment.query()\
2035 cmts = ChangesetComment.query()\
2036 .filter(ChangesetComment.repo == self)
2036 .filter(ChangesetComment.repo == self)
2037 if revisions:
2037 if revisions:
2038 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2038 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2039 grouped = collections.defaultdict(list)
2039 grouped = collections.defaultdict(list)
2040 for cmt in cmts.all():
2040 for cmt in cmts.all():
2041 grouped[cmt.revision].append(cmt)
2041 grouped[cmt.revision].append(cmt)
2042 return grouped
2042 return grouped
2043
2043
2044 def statuses(self, revisions=None):
2044 def statuses(self, revisions=None):
2045 """
2045 """
2046 Returns statuses for this repository
2046 Returns statuses for this repository
2047
2047
2048 :param revisions: list of revisions to get statuses for
2048 :param revisions: list of revisions to get statuses for
2049 """
2049 """
2050 statuses = ChangesetStatus.query()\
2050 statuses = ChangesetStatus.query()\
2051 .filter(ChangesetStatus.repo == self)\
2051 .filter(ChangesetStatus.repo == self)\
2052 .filter(ChangesetStatus.version == 0)
2052 .filter(ChangesetStatus.version == 0)
2053
2053
2054 if revisions:
2054 if revisions:
2055 # Try doing the filtering in chunks to avoid hitting limits
2055 # Try doing the filtering in chunks to avoid hitting limits
2056 size = 500
2056 size = 500
2057 status_results = []
2057 status_results = []
2058 for chunk in xrange(0, len(revisions), size):
2058 for chunk in xrange(0, len(revisions), size):
2059 status_results += statuses.filter(
2059 status_results += statuses.filter(
2060 ChangesetStatus.revision.in_(
2060 ChangesetStatus.revision.in_(
2061 revisions[chunk: chunk+size])
2061 revisions[chunk: chunk+size])
2062 ).all()
2062 ).all()
2063 else:
2063 else:
2064 status_results = statuses.all()
2064 status_results = statuses.all()
2065
2065
2066 grouped = {}
2066 grouped = {}
2067
2067
2068 # maybe we have open new pullrequest without a status?
2068 # maybe we have open new pullrequest without a status?
2069 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2069 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2070 status_lbl = ChangesetStatus.get_status_lbl(stat)
2070 status_lbl = ChangesetStatus.get_status_lbl(stat)
2071 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2071 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2072 for rev in pr.revisions:
2072 for rev in pr.revisions:
2073 pr_id = pr.pull_request_id
2073 pr_id = pr.pull_request_id
2074 pr_repo = pr.target_repo.repo_name
2074 pr_repo = pr.target_repo.repo_name
2075 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2075 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2076
2076
2077 for stat in status_results:
2077 for stat in status_results:
2078 pr_id = pr_repo = None
2078 pr_id = pr_repo = None
2079 if stat.pull_request:
2079 if stat.pull_request:
2080 pr_id = stat.pull_request.pull_request_id
2080 pr_id = stat.pull_request.pull_request_id
2081 pr_repo = stat.pull_request.target_repo.repo_name
2081 pr_repo = stat.pull_request.target_repo.repo_name
2082 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2082 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2083 pr_id, pr_repo]
2083 pr_id, pr_repo]
2084 return grouped
2084 return grouped
2085
2085
2086 # ==========================================================================
2086 # ==========================================================================
2087 # SCM CACHE INSTANCE
2087 # SCM CACHE INSTANCE
2088 # ==========================================================================
2088 # ==========================================================================
2089
2089
2090 def scm_instance(self, **kwargs):
2090 def scm_instance(self, **kwargs):
2091 import rhodecode
2091 import rhodecode
2092
2092
2093 # Passing a config will not hit the cache currently only used
2093 # Passing a config will not hit the cache currently only used
2094 # for repo2dbmapper
2094 # for repo2dbmapper
2095 config = kwargs.pop('config', None)
2095 config = kwargs.pop('config', None)
2096 cache = kwargs.pop('cache', None)
2096 cache = kwargs.pop('cache', None)
2097 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2097 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2098 # if cache is NOT defined use default global, else we have a full
2098 # if cache is NOT defined use default global, else we have a full
2099 # control over cache behaviour
2099 # control over cache behaviour
2100 if cache is None and full_cache and not config:
2100 if cache is None and full_cache and not config:
2101 return self._get_instance_cached()
2101 return self._get_instance_cached()
2102 return self._get_instance(cache=bool(cache), config=config)
2102 return self._get_instance(cache=bool(cache), config=config)
2103
2103
2104 def _get_instance_cached(self):
2104 def _get_instance_cached(self):
2105 @cache_region('long_term')
2105 @cache_region('long_term')
2106 def _get_repo(cache_key):
2106 def _get_repo(cache_key):
2107 return self._get_instance()
2107 return self._get_instance()
2108
2108
2109 invalidator_context = CacheKey.repo_context_cache(
2109 invalidator_context = CacheKey.repo_context_cache(
2110 _get_repo, self.repo_name, None, thread_scoped=True)
2110 _get_repo, self.repo_name, None, thread_scoped=True)
2111
2111
2112 with invalidator_context as context:
2112 with invalidator_context as context:
2113 context.invalidate()
2113 context.invalidate()
2114 repo = context.compute()
2114 repo = context.compute()
2115
2115
2116 return repo
2116 return repo
2117
2117
2118 def _get_instance(self, cache=True, config=None):
2118 def _get_instance(self, cache=True, config=None):
2119 config = config or self._config
2119 config = config or self._config
2120 custom_wire = {
2120 custom_wire = {
2121 'cache': cache # controls the vcs.remote cache
2121 'cache': cache # controls the vcs.remote cache
2122 }
2122 }
2123 repo = get_vcs_instance(
2123 repo = get_vcs_instance(
2124 repo_path=safe_str(self.repo_full_path),
2124 repo_path=safe_str(self.repo_full_path),
2125 config=config,
2125 config=config,
2126 with_wire=custom_wire,
2126 with_wire=custom_wire,
2127 create=False,
2127 create=False,
2128 _vcs_alias=self.repo_type)
2128 _vcs_alias=self.repo_type)
2129
2129
2130 return repo
2130 return repo
2131
2131
2132 def __json__(self):
2132 def __json__(self):
2133 return {'landing_rev': self.landing_rev}
2133 return {'landing_rev': self.landing_rev}
2134
2134
2135 def get_dict(self):
2135 def get_dict(self):
2136
2136
2137 # Since we transformed `repo_name` to a hybrid property, we need to
2137 # Since we transformed `repo_name` to a hybrid property, we need to
2138 # keep compatibility with the code which uses `repo_name` field.
2138 # keep compatibility with the code which uses `repo_name` field.
2139
2139
2140 result = super(Repository, self).get_dict()
2140 result = super(Repository, self).get_dict()
2141 result['repo_name'] = result.pop('_repo_name', None)
2141 result['repo_name'] = result.pop('_repo_name', None)
2142 return result
2142 return result
2143
2143
2144
2144
2145 class RepoGroup(Base, BaseModel):
2145 class RepoGroup(Base, BaseModel):
2146 __tablename__ = 'groups'
2146 __tablename__ = 'groups'
2147 __table_args__ = (
2147 __table_args__ = (
2148 UniqueConstraint('group_name', 'group_parent_id'),
2148 UniqueConstraint('group_name', 'group_parent_id'),
2149 CheckConstraint('group_id != group_parent_id'),
2149 CheckConstraint('group_id != group_parent_id'),
2150 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2150 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2151 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2151 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2152 )
2152 )
2153 __mapper_args__ = {'order_by': 'group_name'}
2153 __mapper_args__ = {'order_by': 'group_name'}
2154
2154
2155 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2155 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2156
2156
2157 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2157 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2158 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2158 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2159 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2159 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2160 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2160 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2161 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2161 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2162 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2162 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2163 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2163 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2164 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2164 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2165
2165
2166 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2166 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2167 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2167 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2168 parent_group = relationship('RepoGroup', remote_side=group_id)
2168 parent_group = relationship('RepoGroup', remote_side=group_id)
2169 user = relationship('User')
2169 user = relationship('User')
2170 integrations = relationship('Integration',
2170 integrations = relationship('Integration',
2171 cascade="all, delete, delete-orphan")
2171 cascade="all, delete, delete-orphan")
2172
2172
2173 def __init__(self, group_name='', parent_group=None):
2173 def __init__(self, group_name='', parent_group=None):
2174 self.group_name = group_name
2174 self.group_name = group_name
2175 self.parent_group = parent_group
2175 self.parent_group = parent_group
2176
2176
2177 def __unicode__(self):
2177 def __unicode__(self):
2178 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2178 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2179 self.group_name)
2179 self.group_name)
2180
2180
2181 @classmethod
2181 @classmethod
2182 def _generate_choice(cls, repo_group):
2182 def _generate_choice(cls, repo_group):
2183 from webhelpers.html import literal as _literal
2183 from webhelpers.html import literal as _literal
2184 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2184 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2185 return repo_group.group_id, _name(repo_group.full_path_splitted)
2185 return repo_group.group_id, _name(repo_group.full_path_splitted)
2186
2186
2187 @classmethod
2187 @classmethod
2188 def groups_choices(cls, groups=None, show_empty_group=True):
2188 def groups_choices(cls, groups=None, show_empty_group=True):
2189 if not groups:
2189 if not groups:
2190 groups = cls.query().all()
2190 groups = cls.query().all()
2191
2191
2192 repo_groups = []
2192 repo_groups = []
2193 if show_empty_group:
2193 if show_empty_group:
2194 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2194 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2195
2195
2196 repo_groups.extend([cls._generate_choice(x) for x in groups])
2196 repo_groups.extend([cls._generate_choice(x) for x in groups])
2197
2197
2198 repo_groups = sorted(
2198 repo_groups = sorted(
2199 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2199 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2200 return repo_groups
2200 return repo_groups
2201
2201
2202 @classmethod
2202 @classmethod
2203 def url_sep(cls):
2203 def url_sep(cls):
2204 return URL_SEP
2204 return URL_SEP
2205
2205
2206 @classmethod
2206 @classmethod
2207 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2207 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2208 if case_insensitive:
2208 if case_insensitive:
2209 gr = cls.query().filter(func.lower(cls.group_name)
2209 gr = cls.query().filter(func.lower(cls.group_name)
2210 == func.lower(group_name))
2210 == func.lower(group_name))
2211 else:
2211 else:
2212 gr = cls.query().filter(cls.group_name == group_name)
2212 gr = cls.query().filter(cls.group_name == group_name)
2213 if cache:
2213 if cache:
2214 name_key = _hash_key(group_name)
2214 name_key = _hash_key(group_name)
2215 gr = gr.options(
2215 gr = gr.options(
2216 FromCache("sql_cache_short", "get_group_%s" % name_key))
2216 FromCache("sql_cache_short", "get_group_%s" % name_key))
2217 return gr.scalar()
2217 return gr.scalar()
2218
2218
2219 @classmethod
2219 @classmethod
2220 def get_user_personal_repo_group(cls, user_id):
2220 def get_user_personal_repo_group(cls, user_id):
2221 user = User.get(user_id)
2221 user = User.get(user_id)
2222 if user.username == User.DEFAULT_USER:
2222 if user.username == User.DEFAULT_USER:
2223 return None
2223 return None
2224
2224
2225 return cls.query()\
2225 return cls.query()\
2226 .filter(cls.personal == true()) \
2226 .filter(cls.personal == true()) \
2227 .filter(cls.user == user).scalar()
2227 .filter(cls.user == user).scalar()
2228
2228
2229 @classmethod
2229 @classmethod
2230 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2230 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2231 case_insensitive=True):
2231 case_insensitive=True):
2232 q = RepoGroup.query()
2232 q = RepoGroup.query()
2233
2233
2234 if not isinstance(user_id, Optional):
2234 if not isinstance(user_id, Optional):
2235 q = q.filter(RepoGroup.user_id == user_id)
2235 q = q.filter(RepoGroup.user_id == user_id)
2236
2236
2237 if not isinstance(group_id, Optional):
2237 if not isinstance(group_id, Optional):
2238 q = q.filter(RepoGroup.group_parent_id == group_id)
2238 q = q.filter(RepoGroup.group_parent_id == group_id)
2239
2239
2240 if case_insensitive:
2240 if case_insensitive:
2241 q = q.order_by(func.lower(RepoGroup.group_name))
2241 q = q.order_by(func.lower(RepoGroup.group_name))
2242 else:
2242 else:
2243 q = q.order_by(RepoGroup.group_name)
2243 q = q.order_by(RepoGroup.group_name)
2244 return q.all()
2244 return q.all()
2245
2245
2246 @property
2246 @property
2247 def parents(self):
2247 def parents(self):
2248 parents_recursion_limit = 10
2248 parents_recursion_limit = 10
2249 groups = []
2249 groups = []
2250 if self.parent_group is None:
2250 if self.parent_group is None:
2251 return groups
2251 return groups
2252 cur_gr = self.parent_group
2252 cur_gr = self.parent_group
2253 groups.insert(0, cur_gr)
2253 groups.insert(0, cur_gr)
2254 cnt = 0
2254 cnt = 0
2255 while 1:
2255 while 1:
2256 cnt += 1
2256 cnt += 1
2257 gr = getattr(cur_gr, 'parent_group', None)
2257 gr = getattr(cur_gr, 'parent_group', None)
2258 cur_gr = cur_gr.parent_group
2258 cur_gr = cur_gr.parent_group
2259 if gr is None:
2259 if gr is None:
2260 break
2260 break
2261 if cnt == parents_recursion_limit:
2261 if cnt == parents_recursion_limit:
2262 # this will prevent accidental infinit loops
2262 # this will prevent accidental infinit loops
2263 log.error(('more than %s parents found for group %s, stopping '
2263 log.error(('more than %s parents found for group %s, stopping '
2264 'recursive parent fetching' % (parents_recursion_limit, self)))
2264 'recursive parent fetching' % (parents_recursion_limit, self)))
2265 break
2265 break
2266
2266
2267 groups.insert(0, gr)
2267 groups.insert(0, gr)
2268 return groups
2268 return groups
2269
2269
2270 @property
2270 @property
2271 def children(self):
2271 def children(self):
2272 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2272 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2273
2273
2274 @property
2274 @property
2275 def name(self):
2275 def name(self):
2276 return self.group_name.split(RepoGroup.url_sep())[-1]
2276 return self.group_name.split(RepoGroup.url_sep())[-1]
2277
2277
2278 @property
2278 @property
2279 def full_path(self):
2279 def full_path(self):
2280 return self.group_name
2280 return self.group_name
2281
2281
2282 @property
2282 @property
2283 def full_path_splitted(self):
2283 def full_path_splitted(self):
2284 return self.group_name.split(RepoGroup.url_sep())
2284 return self.group_name.split(RepoGroup.url_sep())
2285
2285
2286 @property
2286 @property
2287 def repositories(self):
2287 def repositories(self):
2288 return Repository.query()\
2288 return Repository.query()\
2289 .filter(Repository.group == self)\
2289 .filter(Repository.group == self)\
2290 .order_by(Repository.repo_name)
2290 .order_by(Repository.repo_name)
2291
2291
2292 @property
2292 @property
2293 def repositories_recursive_count(self):
2293 def repositories_recursive_count(self):
2294 cnt = self.repositories.count()
2294 cnt = self.repositories.count()
2295
2295
2296 def children_count(group):
2296 def children_count(group):
2297 cnt = 0
2297 cnt = 0
2298 for child in group.children:
2298 for child in group.children:
2299 cnt += child.repositories.count()
2299 cnt += child.repositories.count()
2300 cnt += children_count(child)
2300 cnt += children_count(child)
2301 return cnt
2301 return cnt
2302
2302
2303 return cnt + children_count(self)
2303 return cnt + children_count(self)
2304
2304
2305 def _recursive_objects(self, include_repos=True):
2305 def _recursive_objects(self, include_repos=True):
2306 all_ = []
2306 all_ = []
2307
2307
2308 def _get_members(root_gr):
2308 def _get_members(root_gr):
2309 if include_repos:
2309 if include_repos:
2310 for r in root_gr.repositories:
2310 for r in root_gr.repositories:
2311 all_.append(r)
2311 all_.append(r)
2312 childs = root_gr.children.all()
2312 childs = root_gr.children.all()
2313 if childs:
2313 if childs:
2314 for gr in childs:
2314 for gr in childs:
2315 all_.append(gr)
2315 all_.append(gr)
2316 _get_members(gr)
2316 _get_members(gr)
2317
2317
2318 _get_members(self)
2318 _get_members(self)
2319 return [self] + all_
2319 return [self] + all_
2320
2320
2321 def recursive_groups_and_repos(self):
2321 def recursive_groups_and_repos(self):
2322 """
2322 """
2323 Recursive return all groups, with repositories in those groups
2323 Recursive return all groups, with repositories in those groups
2324 """
2324 """
2325 return self._recursive_objects()
2325 return self._recursive_objects()
2326
2326
2327 def recursive_groups(self):
2327 def recursive_groups(self):
2328 """
2328 """
2329 Returns all children groups for this group including children of children
2329 Returns all children groups for this group including children of children
2330 """
2330 """
2331 return self._recursive_objects(include_repos=False)
2331 return self._recursive_objects(include_repos=False)
2332
2332
2333 def get_new_name(self, group_name):
2333 def get_new_name(self, group_name):
2334 """
2334 """
2335 returns new full group name based on parent and new name
2335 returns new full group name based on parent and new name
2336
2336
2337 :param group_name:
2337 :param group_name:
2338 """
2338 """
2339 path_prefix = (self.parent_group.full_path_splitted if
2339 path_prefix = (self.parent_group.full_path_splitted if
2340 self.parent_group else [])
2340 self.parent_group else [])
2341 return RepoGroup.url_sep().join(path_prefix + [group_name])
2341 return RepoGroup.url_sep().join(path_prefix + [group_name])
2342
2342
2343 def permissions(self, with_admins=True, with_owner=True):
2343 def permissions(self, with_admins=True, with_owner=True):
2344 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2344 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2345 q = q.options(joinedload(UserRepoGroupToPerm.group),
2345 q = q.options(joinedload(UserRepoGroupToPerm.group),
2346 joinedload(UserRepoGroupToPerm.user),
2346 joinedload(UserRepoGroupToPerm.user),
2347 joinedload(UserRepoGroupToPerm.permission),)
2347 joinedload(UserRepoGroupToPerm.permission),)
2348
2348
2349 # get owners and admins and permissions. We do a trick of re-writing
2349 # get owners and admins and permissions. We do a trick of re-writing
2350 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2350 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2351 # has a global reference and changing one object propagates to all
2351 # has a global reference and changing one object propagates to all
2352 # others. This means if admin is also an owner admin_row that change
2352 # others. This means if admin is also an owner admin_row that change
2353 # would propagate to both objects
2353 # would propagate to both objects
2354 perm_rows = []
2354 perm_rows = []
2355 for _usr in q.all():
2355 for _usr in q.all():
2356 usr = AttributeDict(_usr.user.get_dict())
2356 usr = AttributeDict(_usr.user.get_dict())
2357 usr.permission = _usr.permission.permission_name
2357 usr.permission = _usr.permission.permission_name
2358 perm_rows.append(usr)
2358 perm_rows.append(usr)
2359
2359
2360 # filter the perm rows by 'default' first and then sort them by
2360 # filter the perm rows by 'default' first and then sort them by
2361 # admin,write,read,none permissions sorted again alphabetically in
2361 # admin,write,read,none permissions sorted again alphabetically in
2362 # each group
2362 # each group
2363 perm_rows = sorted(perm_rows, key=display_sort)
2363 perm_rows = sorted(perm_rows, key=display_sort)
2364
2364
2365 _admin_perm = 'group.admin'
2365 _admin_perm = 'group.admin'
2366 owner_row = []
2366 owner_row = []
2367 if with_owner:
2367 if with_owner:
2368 usr = AttributeDict(self.user.get_dict())
2368 usr = AttributeDict(self.user.get_dict())
2369 usr.owner_row = True
2369 usr.owner_row = True
2370 usr.permission = _admin_perm
2370 usr.permission = _admin_perm
2371 owner_row.append(usr)
2371 owner_row.append(usr)
2372
2372
2373 super_admin_rows = []
2373 super_admin_rows = []
2374 if with_admins:
2374 if with_admins:
2375 for usr in User.get_all_super_admins():
2375 for usr in User.get_all_super_admins():
2376 # if this admin is also owner, don't double the record
2376 # if this admin is also owner, don't double the record
2377 if usr.user_id == owner_row[0].user_id:
2377 if usr.user_id == owner_row[0].user_id:
2378 owner_row[0].admin_row = True
2378 owner_row[0].admin_row = True
2379 else:
2379 else:
2380 usr = AttributeDict(usr.get_dict())
2380 usr = AttributeDict(usr.get_dict())
2381 usr.admin_row = True
2381 usr.admin_row = True
2382 usr.permission = _admin_perm
2382 usr.permission = _admin_perm
2383 super_admin_rows.append(usr)
2383 super_admin_rows.append(usr)
2384
2384
2385 return super_admin_rows + owner_row + perm_rows
2385 return super_admin_rows + owner_row + perm_rows
2386
2386
2387 def permission_user_groups(self):
2387 def permission_user_groups(self):
2388 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2388 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2389 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2389 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2390 joinedload(UserGroupRepoGroupToPerm.users_group),
2390 joinedload(UserGroupRepoGroupToPerm.users_group),
2391 joinedload(UserGroupRepoGroupToPerm.permission),)
2391 joinedload(UserGroupRepoGroupToPerm.permission),)
2392
2392
2393 perm_rows = []
2393 perm_rows = []
2394 for _user_group in q.all():
2394 for _user_group in q.all():
2395 usr = AttributeDict(_user_group.users_group.get_dict())
2395 usr = AttributeDict(_user_group.users_group.get_dict())
2396 usr.permission = _user_group.permission.permission_name
2396 usr.permission = _user_group.permission.permission_name
2397 perm_rows.append(usr)
2397 perm_rows.append(usr)
2398
2398
2399 return perm_rows
2399 return perm_rows
2400
2400
2401 def get_api_data(self):
2401 def get_api_data(self):
2402 """
2402 """
2403 Common function for generating api data
2403 Common function for generating api data
2404
2404
2405 """
2405 """
2406 group = self
2406 group = self
2407 data = {
2407 data = {
2408 'group_id': group.group_id,
2408 'group_id': group.group_id,
2409 'group_name': group.group_name,
2409 'group_name': group.group_name,
2410 'group_description': group.group_description,
2410 'group_description': group.group_description,
2411 'parent_group': group.parent_group.group_name if group.parent_group else None,
2411 'parent_group': group.parent_group.group_name if group.parent_group else None,
2412 'repositories': [x.repo_name for x in group.repositories],
2412 'repositories': [x.repo_name for x in group.repositories],
2413 'owner': group.user.username,
2413 'owner': group.user.username,
2414 }
2414 }
2415 return data
2415 return data
2416
2416
2417
2417
2418 class Permission(Base, BaseModel):
2418 class Permission(Base, BaseModel):
2419 __tablename__ = 'permissions'
2419 __tablename__ = 'permissions'
2420 __table_args__ = (
2420 __table_args__ = (
2421 Index('p_perm_name_idx', 'permission_name'),
2421 Index('p_perm_name_idx', 'permission_name'),
2422 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2422 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2423 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2423 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2424 )
2424 )
2425 PERMS = [
2425 PERMS = [
2426 ('hg.admin', _('RhodeCode Super Administrator')),
2426 ('hg.admin', _('RhodeCode Super Administrator')),
2427
2427
2428 ('repository.none', _('Repository no access')),
2428 ('repository.none', _('Repository no access')),
2429 ('repository.read', _('Repository read access')),
2429 ('repository.read', _('Repository read access')),
2430 ('repository.write', _('Repository write access')),
2430 ('repository.write', _('Repository write access')),
2431 ('repository.admin', _('Repository admin access')),
2431 ('repository.admin', _('Repository admin access')),
2432
2432
2433 ('group.none', _('Repository group no access')),
2433 ('group.none', _('Repository group no access')),
2434 ('group.read', _('Repository group read access')),
2434 ('group.read', _('Repository group read access')),
2435 ('group.write', _('Repository group write access')),
2435 ('group.write', _('Repository group write access')),
2436 ('group.admin', _('Repository group admin access')),
2436 ('group.admin', _('Repository group admin access')),
2437
2437
2438 ('usergroup.none', _('User group no access')),
2438 ('usergroup.none', _('User group no access')),
2439 ('usergroup.read', _('User group read access')),
2439 ('usergroup.read', _('User group read access')),
2440 ('usergroup.write', _('User group write access')),
2440 ('usergroup.write', _('User group write access')),
2441 ('usergroup.admin', _('User group admin access')),
2441 ('usergroup.admin', _('User group admin access')),
2442
2442
2443 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2443 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2444 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2444 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2445
2445
2446 ('hg.usergroup.create.false', _('User Group creation disabled')),
2446 ('hg.usergroup.create.false', _('User Group creation disabled')),
2447 ('hg.usergroup.create.true', _('User Group creation enabled')),
2447 ('hg.usergroup.create.true', _('User Group creation enabled')),
2448
2448
2449 ('hg.create.none', _('Repository creation disabled')),
2449 ('hg.create.none', _('Repository creation disabled')),
2450 ('hg.create.repository', _('Repository creation enabled')),
2450 ('hg.create.repository', _('Repository creation enabled')),
2451 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2451 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2452 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2452 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2453
2453
2454 ('hg.fork.none', _('Repository forking disabled')),
2454 ('hg.fork.none', _('Repository forking disabled')),
2455 ('hg.fork.repository', _('Repository forking enabled')),
2455 ('hg.fork.repository', _('Repository forking enabled')),
2456
2456
2457 ('hg.register.none', _('Registration disabled')),
2457 ('hg.register.none', _('Registration disabled')),
2458 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2458 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2459 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2459 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2460
2460
2461 ('hg.password_reset.enabled', _('Password reset enabled')),
2461 ('hg.password_reset.enabled', _('Password reset enabled')),
2462 ('hg.password_reset.hidden', _('Password reset hidden')),
2462 ('hg.password_reset.hidden', _('Password reset hidden')),
2463 ('hg.password_reset.disabled', _('Password reset disabled')),
2463 ('hg.password_reset.disabled', _('Password reset disabled')),
2464
2464
2465 ('hg.extern_activate.manual', _('Manual activation of external account')),
2465 ('hg.extern_activate.manual', _('Manual activation of external account')),
2466 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2466 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2467
2467
2468 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2468 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2469 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2469 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2470 ]
2470 ]
2471
2471
2472 # definition of system default permissions for DEFAULT user
2472 # definition of system default permissions for DEFAULT user
2473 DEFAULT_USER_PERMISSIONS = [
2473 DEFAULT_USER_PERMISSIONS = [
2474 'repository.read',
2474 'repository.read',
2475 'group.read',
2475 'group.read',
2476 'usergroup.read',
2476 'usergroup.read',
2477 'hg.create.repository',
2477 'hg.create.repository',
2478 'hg.repogroup.create.false',
2478 'hg.repogroup.create.false',
2479 'hg.usergroup.create.false',
2479 'hg.usergroup.create.false',
2480 'hg.create.write_on_repogroup.true',
2480 'hg.create.write_on_repogroup.true',
2481 'hg.fork.repository',
2481 'hg.fork.repository',
2482 'hg.register.manual_activate',
2482 'hg.register.manual_activate',
2483 'hg.password_reset.enabled',
2483 'hg.password_reset.enabled',
2484 'hg.extern_activate.auto',
2484 'hg.extern_activate.auto',
2485 'hg.inherit_default_perms.true',
2485 'hg.inherit_default_perms.true',
2486 ]
2486 ]
2487
2487
2488 # defines which permissions are more important higher the more important
2488 # defines which permissions are more important higher the more important
2489 # Weight defines which permissions are more important.
2489 # Weight defines which permissions are more important.
2490 # The higher number the more important.
2490 # The higher number the more important.
2491 PERM_WEIGHTS = {
2491 PERM_WEIGHTS = {
2492 'repository.none': 0,
2492 'repository.none': 0,
2493 'repository.read': 1,
2493 'repository.read': 1,
2494 'repository.write': 3,
2494 'repository.write': 3,
2495 'repository.admin': 4,
2495 'repository.admin': 4,
2496
2496
2497 'group.none': 0,
2497 'group.none': 0,
2498 'group.read': 1,
2498 'group.read': 1,
2499 'group.write': 3,
2499 'group.write': 3,
2500 'group.admin': 4,
2500 'group.admin': 4,
2501
2501
2502 'usergroup.none': 0,
2502 'usergroup.none': 0,
2503 'usergroup.read': 1,
2503 'usergroup.read': 1,
2504 'usergroup.write': 3,
2504 'usergroup.write': 3,
2505 'usergroup.admin': 4,
2505 'usergroup.admin': 4,
2506
2506
2507 'hg.repogroup.create.false': 0,
2507 'hg.repogroup.create.false': 0,
2508 'hg.repogroup.create.true': 1,
2508 'hg.repogroup.create.true': 1,
2509
2509
2510 'hg.usergroup.create.false': 0,
2510 'hg.usergroup.create.false': 0,
2511 'hg.usergroup.create.true': 1,
2511 'hg.usergroup.create.true': 1,
2512
2512
2513 'hg.fork.none': 0,
2513 'hg.fork.none': 0,
2514 'hg.fork.repository': 1,
2514 'hg.fork.repository': 1,
2515 'hg.create.none': 0,
2515 'hg.create.none': 0,
2516 'hg.create.repository': 1
2516 'hg.create.repository': 1
2517 }
2517 }
2518
2518
2519 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2519 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2520 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2520 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2521 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2521 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2522
2522
2523 def __unicode__(self):
2523 def __unicode__(self):
2524 return u"<%s('%s:%s')>" % (
2524 return u"<%s('%s:%s')>" % (
2525 self.__class__.__name__, self.permission_id, self.permission_name
2525 self.__class__.__name__, self.permission_id, self.permission_name
2526 )
2526 )
2527
2527
2528 @classmethod
2528 @classmethod
2529 def get_by_key(cls, key):
2529 def get_by_key(cls, key):
2530 return cls.query().filter(cls.permission_name == key).scalar()
2530 return cls.query().filter(cls.permission_name == key).scalar()
2531
2531
2532 @classmethod
2532 @classmethod
2533 def get_default_repo_perms(cls, user_id, repo_id=None):
2533 def get_default_repo_perms(cls, user_id, repo_id=None):
2534 q = Session().query(UserRepoToPerm, Repository, Permission)\
2534 q = Session().query(UserRepoToPerm, Repository, Permission)\
2535 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2535 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2536 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2536 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2537 .filter(UserRepoToPerm.user_id == user_id)
2537 .filter(UserRepoToPerm.user_id == user_id)
2538 if repo_id:
2538 if repo_id:
2539 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2539 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2540 return q.all()
2540 return q.all()
2541
2541
2542 @classmethod
2542 @classmethod
2543 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2543 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2544 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2544 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2545 .join(
2545 .join(
2546 Permission,
2546 Permission,
2547 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2547 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2548 .join(
2548 .join(
2549 Repository,
2549 Repository,
2550 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2550 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2551 .join(
2551 .join(
2552 UserGroup,
2552 UserGroup,
2553 UserGroupRepoToPerm.users_group_id ==
2553 UserGroupRepoToPerm.users_group_id ==
2554 UserGroup.users_group_id)\
2554 UserGroup.users_group_id)\
2555 .join(
2555 .join(
2556 UserGroupMember,
2556 UserGroupMember,
2557 UserGroupRepoToPerm.users_group_id ==
2557 UserGroupRepoToPerm.users_group_id ==
2558 UserGroupMember.users_group_id)\
2558 UserGroupMember.users_group_id)\
2559 .filter(
2559 .filter(
2560 UserGroupMember.user_id == user_id,
2560 UserGroupMember.user_id == user_id,
2561 UserGroup.users_group_active == true())
2561 UserGroup.users_group_active == true())
2562 if repo_id:
2562 if repo_id:
2563 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2563 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2564 return q.all()
2564 return q.all()
2565
2565
2566 @classmethod
2566 @classmethod
2567 def get_default_group_perms(cls, user_id, repo_group_id=None):
2567 def get_default_group_perms(cls, user_id, repo_group_id=None):
2568 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2568 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2569 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2569 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2570 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2570 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2571 .filter(UserRepoGroupToPerm.user_id == user_id)
2571 .filter(UserRepoGroupToPerm.user_id == user_id)
2572 if repo_group_id:
2572 if repo_group_id:
2573 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2573 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2574 return q.all()
2574 return q.all()
2575
2575
2576 @classmethod
2576 @classmethod
2577 def get_default_group_perms_from_user_group(
2577 def get_default_group_perms_from_user_group(
2578 cls, user_id, repo_group_id=None):
2578 cls, user_id, repo_group_id=None):
2579 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2579 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2580 .join(
2580 .join(
2581 Permission,
2581 Permission,
2582 UserGroupRepoGroupToPerm.permission_id ==
2582 UserGroupRepoGroupToPerm.permission_id ==
2583 Permission.permission_id)\
2583 Permission.permission_id)\
2584 .join(
2584 .join(
2585 RepoGroup,
2585 RepoGroup,
2586 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2586 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2587 .join(
2587 .join(
2588 UserGroup,
2588 UserGroup,
2589 UserGroupRepoGroupToPerm.users_group_id ==
2589 UserGroupRepoGroupToPerm.users_group_id ==
2590 UserGroup.users_group_id)\
2590 UserGroup.users_group_id)\
2591 .join(
2591 .join(
2592 UserGroupMember,
2592 UserGroupMember,
2593 UserGroupRepoGroupToPerm.users_group_id ==
2593 UserGroupRepoGroupToPerm.users_group_id ==
2594 UserGroupMember.users_group_id)\
2594 UserGroupMember.users_group_id)\
2595 .filter(
2595 .filter(
2596 UserGroupMember.user_id == user_id,
2596 UserGroupMember.user_id == user_id,
2597 UserGroup.users_group_active == true())
2597 UserGroup.users_group_active == true())
2598 if repo_group_id:
2598 if repo_group_id:
2599 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2599 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2600 return q.all()
2600 return q.all()
2601
2601
2602 @classmethod
2602 @classmethod
2603 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2603 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2604 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2604 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2605 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2605 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2606 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2606 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2607 .filter(UserUserGroupToPerm.user_id == user_id)
2607 .filter(UserUserGroupToPerm.user_id == user_id)
2608 if user_group_id:
2608 if user_group_id:
2609 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2609 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2610 return q.all()
2610 return q.all()
2611
2611
2612 @classmethod
2612 @classmethod
2613 def get_default_user_group_perms_from_user_group(
2613 def get_default_user_group_perms_from_user_group(
2614 cls, user_id, user_group_id=None):
2614 cls, user_id, user_group_id=None):
2615 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2615 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2616 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2616 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2617 .join(
2617 .join(
2618 Permission,
2618 Permission,
2619 UserGroupUserGroupToPerm.permission_id ==
2619 UserGroupUserGroupToPerm.permission_id ==
2620 Permission.permission_id)\
2620 Permission.permission_id)\
2621 .join(
2621 .join(
2622 TargetUserGroup,
2622 TargetUserGroup,
2623 UserGroupUserGroupToPerm.target_user_group_id ==
2623 UserGroupUserGroupToPerm.target_user_group_id ==
2624 TargetUserGroup.users_group_id)\
2624 TargetUserGroup.users_group_id)\
2625 .join(
2625 .join(
2626 UserGroup,
2626 UserGroup,
2627 UserGroupUserGroupToPerm.user_group_id ==
2627 UserGroupUserGroupToPerm.user_group_id ==
2628 UserGroup.users_group_id)\
2628 UserGroup.users_group_id)\
2629 .join(
2629 .join(
2630 UserGroupMember,
2630 UserGroupMember,
2631 UserGroupUserGroupToPerm.user_group_id ==
2631 UserGroupUserGroupToPerm.user_group_id ==
2632 UserGroupMember.users_group_id)\
2632 UserGroupMember.users_group_id)\
2633 .filter(
2633 .filter(
2634 UserGroupMember.user_id == user_id,
2634 UserGroupMember.user_id == user_id,
2635 UserGroup.users_group_active == true())
2635 UserGroup.users_group_active == true())
2636 if user_group_id:
2636 if user_group_id:
2637 q = q.filter(
2637 q = q.filter(
2638 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2638 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2639
2639
2640 return q.all()
2640 return q.all()
2641
2641
2642
2642
2643 class UserRepoToPerm(Base, BaseModel):
2643 class UserRepoToPerm(Base, BaseModel):
2644 __tablename__ = 'repo_to_perm'
2644 __tablename__ = 'repo_to_perm'
2645 __table_args__ = (
2645 __table_args__ = (
2646 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2646 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2647 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2647 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2648 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2648 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2649 )
2649 )
2650 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2650 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2651 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2651 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2652 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2652 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2653 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2653 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2654
2654
2655 user = relationship('User')
2655 user = relationship('User')
2656 repository = relationship('Repository')
2656 repository = relationship('Repository')
2657 permission = relationship('Permission')
2657 permission = relationship('Permission')
2658
2658
2659 @classmethod
2659 @classmethod
2660 def create(cls, user, repository, permission):
2660 def create(cls, user, repository, permission):
2661 n = cls()
2661 n = cls()
2662 n.user = user
2662 n.user = user
2663 n.repository = repository
2663 n.repository = repository
2664 n.permission = permission
2664 n.permission = permission
2665 Session().add(n)
2665 Session().add(n)
2666 return n
2666 return n
2667
2667
2668 def __unicode__(self):
2668 def __unicode__(self):
2669 return u'<%s => %s >' % (self.user, self.repository)
2669 return u'<%s => %s >' % (self.user, self.repository)
2670
2670
2671
2671
2672 class UserUserGroupToPerm(Base, BaseModel):
2672 class UserUserGroupToPerm(Base, BaseModel):
2673 __tablename__ = 'user_user_group_to_perm'
2673 __tablename__ = 'user_user_group_to_perm'
2674 __table_args__ = (
2674 __table_args__ = (
2675 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2675 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2677 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2677 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2678 )
2678 )
2679 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2679 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2680 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2680 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2681 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2681 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2682 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2682 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2683
2683
2684 user = relationship('User')
2684 user = relationship('User')
2685 user_group = relationship('UserGroup')
2685 user_group = relationship('UserGroup')
2686 permission = relationship('Permission')
2686 permission = relationship('Permission')
2687
2687
2688 @classmethod
2688 @classmethod
2689 def create(cls, user, user_group, permission):
2689 def create(cls, user, user_group, permission):
2690 n = cls()
2690 n = cls()
2691 n.user = user
2691 n.user = user
2692 n.user_group = user_group
2692 n.user_group = user_group
2693 n.permission = permission
2693 n.permission = permission
2694 Session().add(n)
2694 Session().add(n)
2695 return n
2695 return n
2696
2696
2697 def __unicode__(self):
2697 def __unicode__(self):
2698 return u'<%s => %s >' % (self.user, self.user_group)
2698 return u'<%s => %s >' % (self.user, self.user_group)
2699
2699
2700
2700
2701 class UserToPerm(Base, BaseModel):
2701 class UserToPerm(Base, BaseModel):
2702 __tablename__ = 'user_to_perm'
2702 __tablename__ = 'user_to_perm'
2703 __table_args__ = (
2703 __table_args__ = (
2704 UniqueConstraint('user_id', 'permission_id'),
2704 UniqueConstraint('user_id', 'permission_id'),
2705 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2705 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2706 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2706 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2707 )
2707 )
2708 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2708 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2709 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2709 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2710 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2710 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2711
2711
2712 user = relationship('User')
2712 user = relationship('User')
2713 permission = relationship('Permission', lazy='joined')
2713 permission = relationship('Permission', lazy='joined')
2714
2714
2715 def __unicode__(self):
2715 def __unicode__(self):
2716 return u'<%s => %s >' % (self.user, self.permission)
2716 return u'<%s => %s >' % (self.user, self.permission)
2717
2717
2718
2718
2719 class UserGroupRepoToPerm(Base, BaseModel):
2719 class UserGroupRepoToPerm(Base, BaseModel):
2720 __tablename__ = 'users_group_repo_to_perm'
2720 __tablename__ = 'users_group_repo_to_perm'
2721 __table_args__ = (
2721 __table_args__ = (
2722 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2722 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2723 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2723 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2724 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2724 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2725 )
2725 )
2726 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2726 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2727 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2727 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2728 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2728 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2729 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2729 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2730
2730
2731 users_group = relationship('UserGroup')
2731 users_group = relationship('UserGroup')
2732 permission = relationship('Permission')
2732 permission = relationship('Permission')
2733 repository = relationship('Repository')
2733 repository = relationship('Repository')
2734
2734
2735 @classmethod
2735 @classmethod
2736 def create(cls, users_group, repository, permission):
2736 def create(cls, users_group, repository, permission):
2737 n = cls()
2737 n = cls()
2738 n.users_group = users_group
2738 n.users_group = users_group
2739 n.repository = repository
2739 n.repository = repository
2740 n.permission = permission
2740 n.permission = permission
2741 Session().add(n)
2741 Session().add(n)
2742 return n
2742 return n
2743
2743
2744 def __unicode__(self):
2744 def __unicode__(self):
2745 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2745 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2746
2746
2747
2747
2748 class UserGroupUserGroupToPerm(Base, BaseModel):
2748 class UserGroupUserGroupToPerm(Base, BaseModel):
2749 __tablename__ = 'user_group_user_group_to_perm'
2749 __tablename__ = 'user_group_user_group_to_perm'
2750 __table_args__ = (
2750 __table_args__ = (
2751 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2751 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2752 CheckConstraint('target_user_group_id != user_group_id'),
2752 CheckConstraint('target_user_group_id != user_group_id'),
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2755 )
2755 )
2756 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)
2756 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)
2757 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2757 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2758 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2758 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2759 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2759 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2760
2760
2761 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2761 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2762 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2762 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2763 permission = relationship('Permission')
2763 permission = relationship('Permission')
2764
2764
2765 @classmethod
2765 @classmethod
2766 def create(cls, target_user_group, user_group, permission):
2766 def create(cls, target_user_group, user_group, permission):
2767 n = cls()
2767 n = cls()
2768 n.target_user_group = target_user_group
2768 n.target_user_group = target_user_group
2769 n.user_group = user_group
2769 n.user_group = user_group
2770 n.permission = permission
2770 n.permission = permission
2771 Session().add(n)
2771 Session().add(n)
2772 return n
2772 return n
2773
2773
2774 def __unicode__(self):
2774 def __unicode__(self):
2775 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2775 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2776
2776
2777
2777
2778 class UserGroupToPerm(Base, BaseModel):
2778 class UserGroupToPerm(Base, BaseModel):
2779 __tablename__ = 'users_group_to_perm'
2779 __tablename__ = 'users_group_to_perm'
2780 __table_args__ = (
2780 __table_args__ = (
2781 UniqueConstraint('users_group_id', 'permission_id',),
2781 UniqueConstraint('users_group_id', 'permission_id',),
2782 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2782 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2783 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2783 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2784 )
2784 )
2785 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2785 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2786 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2786 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2787 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2787 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2788
2788
2789 users_group = relationship('UserGroup')
2789 users_group = relationship('UserGroup')
2790 permission = relationship('Permission')
2790 permission = relationship('Permission')
2791
2791
2792
2792
2793 class UserRepoGroupToPerm(Base, BaseModel):
2793 class UserRepoGroupToPerm(Base, BaseModel):
2794 __tablename__ = 'user_repo_group_to_perm'
2794 __tablename__ = 'user_repo_group_to_perm'
2795 __table_args__ = (
2795 __table_args__ = (
2796 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2796 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2798 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2798 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2799 )
2799 )
2800
2800
2801 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2801 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2802 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2802 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2803 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2803 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2804 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2804 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2805
2805
2806 user = relationship('User')
2806 user = relationship('User')
2807 group = relationship('RepoGroup')
2807 group = relationship('RepoGroup')
2808 permission = relationship('Permission')
2808 permission = relationship('Permission')
2809
2809
2810 @classmethod
2810 @classmethod
2811 def create(cls, user, repository_group, permission):
2811 def create(cls, user, repository_group, permission):
2812 n = cls()
2812 n = cls()
2813 n.user = user
2813 n.user = user
2814 n.group = repository_group
2814 n.group = repository_group
2815 n.permission = permission
2815 n.permission = permission
2816 Session().add(n)
2816 Session().add(n)
2817 return n
2817 return n
2818
2818
2819
2819
2820 class UserGroupRepoGroupToPerm(Base, BaseModel):
2820 class UserGroupRepoGroupToPerm(Base, BaseModel):
2821 __tablename__ = 'users_group_repo_group_to_perm'
2821 __tablename__ = 'users_group_repo_group_to_perm'
2822 __table_args__ = (
2822 __table_args__ = (
2823 UniqueConstraint('users_group_id', 'group_id'),
2823 UniqueConstraint('users_group_id', 'group_id'),
2824 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2824 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2825 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2825 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2826 )
2826 )
2827
2827
2828 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)
2828 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)
2829 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2829 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2830 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2830 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2831 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2831 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2832
2832
2833 users_group = relationship('UserGroup')
2833 users_group = relationship('UserGroup')
2834 permission = relationship('Permission')
2834 permission = relationship('Permission')
2835 group = relationship('RepoGroup')
2835 group = relationship('RepoGroup')
2836
2836
2837 @classmethod
2837 @classmethod
2838 def create(cls, user_group, repository_group, permission):
2838 def create(cls, user_group, repository_group, permission):
2839 n = cls()
2839 n = cls()
2840 n.users_group = user_group
2840 n.users_group = user_group
2841 n.group = repository_group
2841 n.group = repository_group
2842 n.permission = permission
2842 n.permission = permission
2843 Session().add(n)
2843 Session().add(n)
2844 return n
2844 return n
2845
2845
2846 def __unicode__(self):
2846 def __unicode__(self):
2847 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2847 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2848
2848
2849
2849
2850 class Statistics(Base, BaseModel):
2850 class Statistics(Base, BaseModel):
2851 __tablename__ = 'statistics'
2851 __tablename__ = 'statistics'
2852 __table_args__ = (
2852 __table_args__ = (
2853 UniqueConstraint('repository_id'),
2853 UniqueConstraint('repository_id'),
2854 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2854 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2855 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2855 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2856 )
2856 )
2857 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2857 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2858 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2858 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2859 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2859 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2860 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2860 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2861 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2861 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2862 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2862 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2863
2863
2864 repository = relationship('Repository', single_parent=True)
2864 repository = relationship('Repository', single_parent=True)
2865
2865
2866
2866
2867 class UserFollowing(Base, BaseModel):
2867 class UserFollowing(Base, BaseModel):
2868 __tablename__ = 'user_followings'
2868 __tablename__ = 'user_followings'
2869 __table_args__ = (
2869 __table_args__ = (
2870 UniqueConstraint('user_id', 'follows_repository_id'),
2870 UniqueConstraint('user_id', 'follows_repository_id'),
2871 UniqueConstraint('user_id', 'follows_user_id'),
2871 UniqueConstraint('user_id', 'follows_user_id'),
2872 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2872 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2873 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2873 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2874 )
2874 )
2875
2875
2876 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2876 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2877 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2877 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2878 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2878 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2879 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2879 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2880 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2880 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2881
2881
2882 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2882 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2883
2883
2884 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2884 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2885 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2885 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2886
2886
2887 @classmethod
2887 @classmethod
2888 def get_repo_followers(cls, repo_id):
2888 def get_repo_followers(cls, repo_id):
2889 return cls.query().filter(cls.follows_repo_id == repo_id)
2889 return cls.query().filter(cls.follows_repo_id == repo_id)
2890
2890
2891
2891
2892 class CacheKey(Base, BaseModel):
2892 class CacheKey(Base, BaseModel):
2893 __tablename__ = 'cache_invalidation'
2893 __tablename__ = 'cache_invalidation'
2894 __table_args__ = (
2894 __table_args__ = (
2895 UniqueConstraint('cache_key'),
2895 UniqueConstraint('cache_key'),
2896 Index('key_idx', 'cache_key'),
2896 Index('key_idx', 'cache_key'),
2897 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2897 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2898 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2898 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2899 )
2899 )
2900 CACHE_TYPE_ATOM = 'ATOM'
2900 CACHE_TYPE_ATOM = 'ATOM'
2901 CACHE_TYPE_RSS = 'RSS'
2901 CACHE_TYPE_RSS = 'RSS'
2902 CACHE_TYPE_README = 'README'
2902 CACHE_TYPE_README = 'README'
2903
2903
2904 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2904 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2905 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2905 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2906 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2906 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2907 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2907 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2908
2908
2909 def __init__(self, cache_key, cache_args=''):
2909 def __init__(self, cache_key, cache_args=''):
2910 self.cache_key = cache_key
2910 self.cache_key = cache_key
2911 self.cache_args = cache_args
2911 self.cache_args = cache_args
2912 self.cache_active = False
2912 self.cache_active = False
2913
2913
2914 def __unicode__(self):
2914 def __unicode__(self):
2915 return u"<%s('%s:%s[%s]')>" % (
2915 return u"<%s('%s:%s[%s]')>" % (
2916 self.__class__.__name__,
2916 self.__class__.__name__,
2917 self.cache_id, self.cache_key, self.cache_active)
2917 self.cache_id, self.cache_key, self.cache_active)
2918
2918
2919 def _cache_key_partition(self):
2919 def _cache_key_partition(self):
2920 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2920 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2921 return prefix, repo_name, suffix
2921 return prefix, repo_name, suffix
2922
2922
2923 def get_prefix(self):
2923 def get_prefix(self):
2924 """
2924 """
2925 Try to extract prefix from existing cache key. The key could consist
2925 Try to extract prefix from existing cache key. The key could consist
2926 of prefix, repo_name, suffix
2926 of prefix, repo_name, suffix
2927 """
2927 """
2928 # this returns prefix, repo_name, suffix
2928 # this returns prefix, repo_name, suffix
2929 return self._cache_key_partition()[0]
2929 return self._cache_key_partition()[0]
2930
2930
2931 def get_suffix(self):
2931 def get_suffix(self):
2932 """
2932 """
2933 get suffix that might have been used in _get_cache_key to
2933 get suffix that might have been used in _get_cache_key to
2934 generate self.cache_key. Only used for informational purposes
2934 generate self.cache_key. Only used for informational purposes
2935 in repo_edit.mako.
2935 in repo_edit.mako.
2936 """
2936 """
2937 # prefix, repo_name, suffix
2937 # prefix, repo_name, suffix
2938 return self._cache_key_partition()[2]
2938 return self._cache_key_partition()[2]
2939
2939
2940 @classmethod
2940 @classmethod
2941 def delete_all_cache(cls):
2941 def delete_all_cache(cls):
2942 """
2942 """
2943 Delete all cache keys from database.
2943 Delete all cache keys from database.
2944 Should only be run when all instances are down and all entries
2944 Should only be run when all instances are down and all entries
2945 thus stale.
2945 thus stale.
2946 """
2946 """
2947 cls.query().delete()
2947 cls.query().delete()
2948 Session().commit()
2948 Session().commit()
2949
2949
2950 @classmethod
2950 @classmethod
2951 def get_cache_key(cls, repo_name, cache_type):
2951 def get_cache_key(cls, repo_name, cache_type):
2952 """
2952 """
2953
2953
2954 Generate a cache key for this process of RhodeCode instance.
2954 Generate a cache key for this process of RhodeCode instance.
2955 Prefix most likely will be process id or maybe explicitly set
2955 Prefix most likely will be process id or maybe explicitly set
2956 instance_id from .ini file.
2956 instance_id from .ini file.
2957 """
2957 """
2958 import rhodecode
2958 import rhodecode
2959 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2959 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2960
2960
2961 repo_as_unicode = safe_unicode(repo_name)
2961 repo_as_unicode = safe_unicode(repo_name)
2962 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2962 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2963 if cache_type else repo_as_unicode
2963 if cache_type else repo_as_unicode
2964
2964
2965 return u'{}{}'.format(prefix, key)
2965 return u'{}{}'.format(prefix, key)
2966
2966
2967 @classmethod
2967 @classmethod
2968 def set_invalidate(cls, repo_name, delete=False):
2968 def set_invalidate(cls, repo_name, delete=False):
2969 """
2969 """
2970 Mark all caches of a repo as invalid in the database.
2970 Mark all caches of a repo as invalid in the database.
2971 """
2971 """
2972
2972
2973 try:
2973 try:
2974 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2974 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2975 if delete:
2975 if delete:
2976 log.debug('cache objects deleted for repo %s',
2976 log.debug('cache objects deleted for repo %s',
2977 safe_str(repo_name))
2977 safe_str(repo_name))
2978 qry.delete()
2978 qry.delete()
2979 else:
2979 else:
2980 log.debug('cache objects marked as invalid for repo %s',
2980 log.debug('cache objects marked as invalid for repo %s',
2981 safe_str(repo_name))
2981 safe_str(repo_name))
2982 qry.update({"cache_active": False})
2982 qry.update({"cache_active": False})
2983
2983
2984 Session().commit()
2984 Session().commit()
2985 except Exception:
2985 except Exception:
2986 log.exception(
2986 log.exception(
2987 'Cache key invalidation failed for repository %s',
2987 'Cache key invalidation failed for repository %s',
2988 safe_str(repo_name))
2988 safe_str(repo_name))
2989 Session().rollback()
2989 Session().rollback()
2990
2990
2991 @classmethod
2991 @classmethod
2992 def get_active_cache(cls, cache_key):
2992 def get_active_cache(cls, cache_key):
2993 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2993 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2994 if inv_obj:
2994 if inv_obj:
2995 return inv_obj
2995 return inv_obj
2996 return None
2996 return None
2997
2997
2998 @classmethod
2998 @classmethod
2999 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2999 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3000 thread_scoped=False):
3000 thread_scoped=False):
3001 """
3001 """
3002 @cache_region('long_term')
3002 @cache_region('long_term')
3003 def _heavy_calculation(cache_key):
3003 def _heavy_calculation(cache_key):
3004 return 'result'
3004 return 'result'
3005
3005
3006 cache_context = CacheKey.repo_context_cache(
3006 cache_context = CacheKey.repo_context_cache(
3007 _heavy_calculation, repo_name, cache_type)
3007 _heavy_calculation, repo_name, cache_type)
3008
3008
3009 with cache_context as context:
3009 with cache_context as context:
3010 context.invalidate()
3010 context.invalidate()
3011 computed = context.compute()
3011 computed = context.compute()
3012
3012
3013 assert computed == 'result'
3013 assert computed == 'result'
3014 """
3014 """
3015 from rhodecode.lib import caches
3015 from rhodecode.lib import caches
3016 return caches.InvalidationContext(
3016 return caches.InvalidationContext(
3017 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3017 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3018
3018
3019
3019
3020 class ChangesetComment(Base, BaseModel):
3020 class ChangesetComment(Base, BaseModel):
3021 __tablename__ = 'changeset_comments'
3021 __tablename__ = 'changeset_comments'
3022 __table_args__ = (
3022 __table_args__ = (
3023 Index('cc_revision_idx', 'revision'),
3023 Index('cc_revision_idx', 'revision'),
3024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3026 )
3026 )
3027
3027
3028 COMMENT_OUTDATED = u'comment_outdated'
3028 COMMENT_OUTDATED = u'comment_outdated'
3029 COMMENT_TYPE_NOTE = u'note'
3029 COMMENT_TYPE_NOTE = u'note'
3030 COMMENT_TYPE_TODO = u'todo'
3030 COMMENT_TYPE_TODO = u'todo'
3031 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3031 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3032
3032
3033 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3033 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3034 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3034 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3035 revision = Column('revision', String(40), nullable=True)
3035 revision = Column('revision', String(40), nullable=True)
3036 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3036 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3037 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3037 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3038 line_no = Column('line_no', Unicode(10), nullable=True)
3038 line_no = Column('line_no', Unicode(10), nullable=True)
3039 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3039 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3040 f_path = Column('f_path', Unicode(1000), nullable=True)
3040 f_path = Column('f_path', Unicode(1000), nullable=True)
3041 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3041 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3042 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3042 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3043 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3043 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3044 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3044 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3045 renderer = Column('renderer', Unicode(64), nullable=True)
3045 renderer = Column('renderer', Unicode(64), nullable=True)
3046 display_state = Column('display_state', Unicode(128), nullable=True)
3046 display_state = Column('display_state', Unicode(128), nullable=True)
3047
3047
3048 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3048 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3049 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3049 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3050 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3050 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3051 author = relationship('User', lazy='joined')
3051 author = relationship('User', lazy='joined')
3052 repo = relationship('Repository')
3052 repo = relationship('Repository')
3053 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3053 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3054 pull_request = relationship('PullRequest', lazy='joined')
3054 pull_request = relationship('PullRequest', lazy='joined')
3055 pull_request_version = relationship('PullRequestVersion')
3055 pull_request_version = relationship('PullRequestVersion')
3056
3056
3057 @classmethod
3057 @classmethod
3058 def get_users(cls, revision=None, pull_request_id=None):
3058 def get_users(cls, revision=None, pull_request_id=None):
3059 """
3059 """
3060 Returns user associated with this ChangesetComment. ie those
3060 Returns user associated with this ChangesetComment. ie those
3061 who actually commented
3061 who actually commented
3062
3062
3063 :param cls:
3063 :param cls:
3064 :param revision:
3064 :param revision:
3065 """
3065 """
3066 q = Session().query(User)\
3066 q = Session().query(User)\
3067 .join(ChangesetComment.author)
3067 .join(ChangesetComment.author)
3068 if revision:
3068 if revision:
3069 q = q.filter(cls.revision == revision)
3069 q = q.filter(cls.revision == revision)
3070 elif pull_request_id:
3070 elif pull_request_id:
3071 q = q.filter(cls.pull_request_id == pull_request_id)
3071 q = q.filter(cls.pull_request_id == pull_request_id)
3072 return q.all()
3072 return q.all()
3073
3073
3074 @classmethod
3074 @classmethod
3075 def get_index_from_version(cls, pr_version, versions):
3075 def get_index_from_version(cls, pr_version, versions):
3076 num_versions = [x.pull_request_version_id for x in versions]
3076 num_versions = [x.pull_request_version_id for x in versions]
3077 try:
3077 try:
3078 return num_versions.index(pr_version) +1
3078 return num_versions.index(pr_version) +1
3079 except (IndexError, ValueError):
3079 except (IndexError, ValueError):
3080 return
3080 return
3081
3081
3082 @property
3082 @property
3083 def outdated(self):
3083 def outdated(self):
3084 return self.display_state == self.COMMENT_OUTDATED
3084 return self.display_state == self.COMMENT_OUTDATED
3085
3085
3086 def outdated_at_version(self, version):
3086 def outdated_at_version(self, version):
3087 """
3087 """
3088 Checks if comment is outdated for given pull request version
3088 Checks if comment is outdated for given pull request version
3089 """
3089 """
3090 return self.outdated and self.pull_request_version_id != version
3090 return self.outdated and self.pull_request_version_id != version
3091
3091
3092 def older_than_version(self, version):
3092 def older_than_version(self, version):
3093 """
3093 """
3094 Checks if comment is made from previous version than given
3094 Checks if comment is made from previous version than given
3095 """
3095 """
3096 if version is None:
3096 if version is None:
3097 return self.pull_request_version_id is not None
3097 return self.pull_request_version_id is not None
3098
3098
3099 return self.pull_request_version_id < version
3099 return self.pull_request_version_id < version
3100
3100
3101 @property
3101 @property
3102 def resolved(self):
3102 def resolved(self):
3103 return self.resolved_by[0] if self.resolved_by else None
3103 return self.resolved_by[0] if self.resolved_by else None
3104
3104
3105 @property
3105 @property
3106 def is_todo(self):
3106 def is_todo(self):
3107 return self.comment_type == self.COMMENT_TYPE_TODO
3107 return self.comment_type == self.COMMENT_TYPE_TODO
3108
3108
3109 def get_index_version(self, versions):
3109 def get_index_version(self, versions):
3110 return self.get_index_from_version(
3110 return self.get_index_from_version(
3111 self.pull_request_version_id, versions)
3111 self.pull_request_version_id, versions)
3112
3112
3113 def __repr__(self):
3113 def __repr__(self):
3114 if self.comment_id:
3114 if self.comment_id:
3115 return '<DB:Comment #%s>' % self.comment_id
3115 return '<DB:Comment #%s>' % self.comment_id
3116 else:
3116 else:
3117 return '<DB:Comment at %#x>' % id(self)
3117 return '<DB:Comment at %#x>' % id(self)
3118
3118
3119
3119
3120 class ChangesetStatus(Base, BaseModel):
3120 class ChangesetStatus(Base, BaseModel):
3121 __tablename__ = 'changeset_statuses'
3121 __tablename__ = 'changeset_statuses'
3122 __table_args__ = (
3122 __table_args__ = (
3123 Index('cs_revision_idx', 'revision'),
3123 Index('cs_revision_idx', 'revision'),
3124 Index('cs_version_idx', 'version'),
3124 Index('cs_version_idx', 'version'),
3125 UniqueConstraint('repo_id', 'revision', 'version'),
3125 UniqueConstraint('repo_id', 'revision', 'version'),
3126 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3126 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3127 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3127 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3128 )
3128 )
3129 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3129 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3130 STATUS_APPROVED = 'approved'
3130 STATUS_APPROVED = 'approved'
3131 STATUS_REJECTED = 'rejected'
3131 STATUS_REJECTED = 'rejected'
3132 STATUS_UNDER_REVIEW = 'under_review'
3132 STATUS_UNDER_REVIEW = 'under_review'
3133
3133
3134 STATUSES = [
3134 STATUSES = [
3135 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3135 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3136 (STATUS_APPROVED, _("Approved")),
3136 (STATUS_APPROVED, _("Approved")),
3137 (STATUS_REJECTED, _("Rejected")),
3137 (STATUS_REJECTED, _("Rejected")),
3138 (STATUS_UNDER_REVIEW, _("Under Review")),
3138 (STATUS_UNDER_REVIEW, _("Under Review")),
3139 ]
3139 ]
3140
3140
3141 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3141 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3142 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3142 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3143 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3143 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3144 revision = Column('revision', String(40), nullable=False)
3144 revision = Column('revision', String(40), nullable=False)
3145 status = Column('status', String(128), nullable=False, default=DEFAULT)
3145 status = Column('status', String(128), nullable=False, default=DEFAULT)
3146 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3146 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3147 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3147 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3148 version = Column('version', Integer(), nullable=False, default=0)
3148 version = Column('version', Integer(), nullable=False, default=0)
3149 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3149 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3150
3150
3151 author = relationship('User', lazy='joined')
3151 author = relationship('User', lazy='joined')
3152 repo = relationship('Repository')
3152 repo = relationship('Repository')
3153 comment = relationship('ChangesetComment', lazy='joined')
3153 comment = relationship('ChangesetComment', lazy='joined')
3154 pull_request = relationship('PullRequest', lazy='joined')
3154 pull_request = relationship('PullRequest', lazy='joined')
3155
3155
3156 def __unicode__(self):
3156 def __unicode__(self):
3157 return u"<%s('%s[v%s]:%s')>" % (
3157 return u"<%s('%s[v%s]:%s')>" % (
3158 self.__class__.__name__,
3158 self.__class__.__name__,
3159 self.status, self.version, self.author
3159 self.status, self.version, self.author
3160 )
3160 )
3161
3161
3162 @classmethod
3162 @classmethod
3163 def get_status_lbl(cls, value):
3163 def get_status_lbl(cls, value):
3164 return dict(cls.STATUSES).get(value)
3164 return dict(cls.STATUSES).get(value)
3165
3165
3166 @property
3166 @property
3167 def status_lbl(self):
3167 def status_lbl(self):
3168 return ChangesetStatus.get_status_lbl(self.status)
3168 return ChangesetStatus.get_status_lbl(self.status)
3169
3169
3170
3170
3171 class _PullRequestBase(BaseModel):
3171 class _PullRequestBase(BaseModel):
3172 """
3172 """
3173 Common attributes of pull request and version entries.
3173 Common attributes of pull request and version entries.
3174 """
3174 """
3175
3175
3176 # .status values
3176 # .status values
3177 STATUS_NEW = u'new'
3177 STATUS_NEW = u'new'
3178 STATUS_OPEN = u'open'
3178 STATUS_OPEN = u'open'
3179 STATUS_CLOSED = u'closed'
3179 STATUS_CLOSED = u'closed'
3180
3180
3181 title = Column('title', Unicode(255), nullable=True)
3181 title = Column('title', Unicode(255), nullable=True)
3182 description = Column(
3182 description = Column(
3183 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3183 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3184 nullable=True)
3184 nullable=True)
3185 # new/open/closed status of pull request (not approve/reject/etc)
3185 # new/open/closed status of pull request (not approve/reject/etc)
3186 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3186 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3187 created_on = Column(
3187 created_on = Column(
3188 'created_on', DateTime(timezone=False), nullable=False,
3188 'created_on', DateTime(timezone=False), nullable=False,
3189 default=datetime.datetime.now)
3189 default=datetime.datetime.now)
3190 updated_on = Column(
3190 updated_on = Column(
3191 'updated_on', DateTime(timezone=False), nullable=False,
3191 'updated_on', DateTime(timezone=False), nullable=False,
3192 default=datetime.datetime.now)
3192 default=datetime.datetime.now)
3193
3193
3194 @declared_attr
3194 @declared_attr
3195 def user_id(cls):
3195 def user_id(cls):
3196 return Column(
3196 return Column(
3197 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3197 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3198 unique=None)
3198 unique=None)
3199
3199
3200 # 500 revisions max
3200 # 500 revisions max
3201 _revisions = Column(
3201 _revisions = Column(
3202 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3202 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3203
3203
3204 @declared_attr
3204 @declared_attr
3205 def source_repo_id(cls):
3205 def source_repo_id(cls):
3206 # TODO: dan: rename column to source_repo_id
3206 # TODO: dan: rename column to source_repo_id
3207 return Column(
3207 return Column(
3208 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3208 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3209 nullable=False)
3209 nullable=False)
3210
3210
3211 source_ref = Column('org_ref', Unicode(255), nullable=False)
3211 source_ref = Column('org_ref', Unicode(255), nullable=False)
3212
3212
3213 @declared_attr
3213 @declared_attr
3214 def target_repo_id(cls):
3214 def target_repo_id(cls):
3215 # TODO: dan: rename column to target_repo_id
3215 # TODO: dan: rename column to target_repo_id
3216 return Column(
3216 return Column(
3217 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3217 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3218 nullable=False)
3218 nullable=False)
3219
3219
3220 target_ref = Column('other_ref', Unicode(255), nullable=False)
3220 target_ref = Column('other_ref', Unicode(255), nullable=False)
3221 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3221 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3222
3222
3223 # TODO: dan: rename column to last_merge_source_rev
3223 # TODO: dan: rename column to last_merge_source_rev
3224 _last_merge_source_rev = Column(
3224 _last_merge_source_rev = Column(
3225 'last_merge_org_rev', String(40), nullable=True)
3225 'last_merge_org_rev', String(40), nullable=True)
3226 # TODO: dan: rename column to last_merge_target_rev
3226 # TODO: dan: rename column to last_merge_target_rev
3227 _last_merge_target_rev = Column(
3227 _last_merge_target_rev = Column(
3228 'last_merge_other_rev', String(40), nullable=True)
3228 'last_merge_other_rev', String(40), nullable=True)
3229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3230 merge_rev = Column('merge_rev', String(40), nullable=True)
3230 merge_rev = Column('merge_rev', String(40), nullable=True)
3231
3231
3232 reviewer_data = Column(
3233 'reviewer_data_json', MutationObj.as_mutable(
3234 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3235
3236 @property
3237 def reviewer_data_json(self):
3238 return json.dumps(self.reviewer_data)
3239
3232 @hybrid_property
3240 @hybrid_property
3233 def revisions(self):
3241 def revisions(self):
3234 return self._revisions.split(':') if self._revisions else []
3242 return self._revisions.split(':') if self._revisions else []
3235
3243
3236 @revisions.setter
3244 @revisions.setter
3237 def revisions(self, val):
3245 def revisions(self, val):
3238 self._revisions = ':'.join(val)
3246 self._revisions = ':'.join(val)
3239
3247
3240 @declared_attr
3248 @declared_attr
3241 def author(cls):
3249 def author(cls):
3242 return relationship('User', lazy='joined')
3250 return relationship('User', lazy='joined')
3243
3251
3244 @declared_attr
3252 @declared_attr
3245 def source_repo(cls):
3253 def source_repo(cls):
3246 return relationship(
3254 return relationship(
3247 'Repository',
3255 'Repository',
3248 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3256 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3249
3257
3250 @property
3258 @property
3251 def source_ref_parts(self):
3259 def source_ref_parts(self):
3252 return self.unicode_to_reference(self.source_ref)
3260 return self.unicode_to_reference(self.source_ref)
3253
3261
3254 @declared_attr
3262 @declared_attr
3255 def target_repo(cls):
3263 def target_repo(cls):
3256 return relationship(
3264 return relationship(
3257 'Repository',
3265 'Repository',
3258 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3266 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3259
3267
3260 @property
3268 @property
3261 def target_ref_parts(self):
3269 def target_ref_parts(self):
3262 return self.unicode_to_reference(self.target_ref)
3270 return self.unicode_to_reference(self.target_ref)
3263
3271
3264 @property
3272 @property
3265 def shadow_merge_ref(self):
3273 def shadow_merge_ref(self):
3266 return self.unicode_to_reference(self._shadow_merge_ref)
3274 return self.unicode_to_reference(self._shadow_merge_ref)
3267
3275
3268 @shadow_merge_ref.setter
3276 @shadow_merge_ref.setter
3269 def shadow_merge_ref(self, ref):
3277 def shadow_merge_ref(self, ref):
3270 self._shadow_merge_ref = self.reference_to_unicode(ref)
3278 self._shadow_merge_ref = self.reference_to_unicode(ref)
3271
3279
3272 def unicode_to_reference(self, raw):
3280 def unicode_to_reference(self, raw):
3273 """
3281 """
3274 Convert a unicode (or string) to a reference object.
3282 Convert a unicode (or string) to a reference object.
3275 If unicode evaluates to False it returns None.
3283 If unicode evaluates to False it returns None.
3276 """
3284 """
3277 if raw:
3285 if raw:
3278 refs = raw.split(':')
3286 refs = raw.split(':')
3279 return Reference(*refs)
3287 return Reference(*refs)
3280 else:
3288 else:
3281 return None
3289 return None
3282
3290
3283 def reference_to_unicode(self, ref):
3291 def reference_to_unicode(self, ref):
3284 """
3292 """
3285 Convert a reference object to unicode.
3293 Convert a reference object to unicode.
3286 If reference is None it returns None.
3294 If reference is None it returns None.
3287 """
3295 """
3288 if ref:
3296 if ref:
3289 return u':'.join(ref)
3297 return u':'.join(ref)
3290 else:
3298 else:
3291 return None
3299 return None
3292
3300
3293 def get_api_data(self):
3301 def get_api_data(self):
3294 from rhodecode.model.pull_request import PullRequestModel
3302 from rhodecode.model.pull_request import PullRequestModel
3295 pull_request = self
3303 pull_request = self
3296 merge_status = PullRequestModel().merge_status(pull_request)
3304 merge_status = PullRequestModel().merge_status(pull_request)
3297
3305
3298 pull_request_url = url(
3306 pull_request_url = url(
3299 'pullrequest_show', repo_name=self.target_repo.repo_name,
3307 'pullrequest_show', repo_name=self.target_repo.repo_name,
3300 pull_request_id=self.pull_request_id, qualified=True)
3308 pull_request_id=self.pull_request_id, qualified=True)
3301
3309
3302 merge_data = {
3310 merge_data = {
3303 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3311 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3304 'reference': (
3312 'reference': (
3305 pull_request.shadow_merge_ref._asdict()
3313 pull_request.shadow_merge_ref._asdict()
3306 if pull_request.shadow_merge_ref else None),
3314 if pull_request.shadow_merge_ref else None),
3307 }
3315 }
3308
3316
3309 data = {
3317 data = {
3310 'pull_request_id': pull_request.pull_request_id,
3318 'pull_request_id': pull_request.pull_request_id,
3311 'url': pull_request_url,
3319 'url': pull_request_url,
3312 'title': pull_request.title,
3320 'title': pull_request.title,
3313 'description': pull_request.description,
3321 'description': pull_request.description,
3314 'status': pull_request.status,
3322 'status': pull_request.status,
3315 'created_on': pull_request.created_on,
3323 'created_on': pull_request.created_on,
3316 'updated_on': pull_request.updated_on,
3324 'updated_on': pull_request.updated_on,
3317 'commit_ids': pull_request.revisions,
3325 'commit_ids': pull_request.revisions,
3318 'review_status': pull_request.calculated_review_status(),
3326 'review_status': pull_request.calculated_review_status(),
3319 'mergeable': {
3327 'mergeable': {
3320 'status': merge_status[0],
3328 'status': merge_status[0],
3321 'message': unicode(merge_status[1]),
3329 'message': unicode(merge_status[1]),
3322 },
3330 },
3323 'source': {
3331 'source': {
3324 'clone_url': pull_request.source_repo.clone_url(),
3332 'clone_url': pull_request.source_repo.clone_url(),
3325 'repository': pull_request.source_repo.repo_name,
3333 'repository': pull_request.source_repo.repo_name,
3326 'reference': {
3334 'reference': {
3327 'name': pull_request.source_ref_parts.name,
3335 'name': pull_request.source_ref_parts.name,
3328 'type': pull_request.source_ref_parts.type,
3336 'type': pull_request.source_ref_parts.type,
3329 'commit_id': pull_request.source_ref_parts.commit_id,
3337 'commit_id': pull_request.source_ref_parts.commit_id,
3330 },
3338 },
3331 },
3339 },
3332 'target': {
3340 'target': {
3333 'clone_url': pull_request.target_repo.clone_url(),
3341 'clone_url': pull_request.target_repo.clone_url(),
3334 'repository': pull_request.target_repo.repo_name,
3342 'repository': pull_request.target_repo.repo_name,
3335 'reference': {
3343 'reference': {
3336 'name': pull_request.target_ref_parts.name,
3344 'name': pull_request.target_ref_parts.name,
3337 'type': pull_request.target_ref_parts.type,
3345 'type': pull_request.target_ref_parts.type,
3338 'commit_id': pull_request.target_ref_parts.commit_id,
3346 'commit_id': pull_request.target_ref_parts.commit_id,
3339 },
3347 },
3340 },
3348 },
3341 'merge': merge_data,
3349 'merge': merge_data,
3342 'author': pull_request.author.get_api_data(include_secrets=False,
3350 'author': pull_request.author.get_api_data(include_secrets=False,
3343 details='basic'),
3351 details='basic'),
3344 'reviewers': [
3352 'reviewers': [
3345 {
3353 {
3346 'user': reviewer.get_api_data(include_secrets=False,
3354 'user': reviewer.get_api_data(include_secrets=False,
3347 details='basic'),
3355 details='basic'),
3348 'reasons': reasons,
3356 'reasons': reasons,
3349 'review_status': st[0][1].status if st else 'not_reviewed',
3357 'review_status': st[0][1].status if st else 'not_reviewed',
3350 }
3358 }
3351 for reviewer, reasons, st in pull_request.reviewers_statuses()
3359 for reviewer, reasons, mandatory, st in
3360 pull_request.reviewers_statuses()
3352 ]
3361 ]
3353 }
3362 }
3354
3363
3355 return data
3364 return data
3356
3365
3357
3366
3358 class PullRequest(Base, _PullRequestBase):
3367 class PullRequest(Base, _PullRequestBase):
3359 __tablename__ = 'pull_requests'
3368 __tablename__ = 'pull_requests'
3360 __table_args__ = (
3369 __table_args__ = (
3361 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3362 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3363 )
3372 )
3364
3373
3365 pull_request_id = Column(
3374 pull_request_id = Column(
3366 'pull_request_id', Integer(), nullable=False, primary_key=True)
3375 'pull_request_id', Integer(), nullable=False, primary_key=True)
3367
3376
3368 def __repr__(self):
3377 def __repr__(self):
3369 if self.pull_request_id:
3378 if self.pull_request_id:
3370 return '<DB:PullRequest #%s>' % self.pull_request_id
3379 return '<DB:PullRequest #%s>' % self.pull_request_id
3371 else:
3380 else:
3372 return '<DB:PullRequest at %#x>' % id(self)
3381 return '<DB:PullRequest at %#x>' % id(self)
3373
3382
3374 reviewers = relationship('PullRequestReviewers',
3383 reviewers = relationship('PullRequestReviewers',
3375 cascade="all, delete, delete-orphan")
3384 cascade="all, delete, delete-orphan")
3376 statuses = relationship('ChangesetStatus')
3385 statuses = relationship('ChangesetStatus')
3377 comments = relationship('ChangesetComment',
3386 comments = relationship('ChangesetComment',
3378 cascade="all, delete, delete-orphan")
3387 cascade="all, delete, delete-orphan")
3379 versions = relationship('PullRequestVersion',
3388 versions = relationship('PullRequestVersion',
3380 cascade="all, delete, delete-orphan",
3389 cascade="all, delete, delete-orphan",
3381 lazy='dynamic')
3390 lazy='dynamic')
3382
3391
3383 @classmethod
3392 @classmethod
3384 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3393 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3385 internal_methods=None):
3394 internal_methods=None):
3386
3395
3387 class PullRequestDisplay(object):
3396 class PullRequestDisplay(object):
3388 """
3397 """
3389 Special object wrapper for showing PullRequest data via Versions
3398 Special object wrapper for showing PullRequest data via Versions
3390 It mimics PR object as close as possible. This is read only object
3399 It mimics PR object as close as possible. This is read only object
3391 just for display
3400 just for display
3392 """
3401 """
3393
3402
3394 def __init__(self, attrs, internal=None):
3403 def __init__(self, attrs, internal=None):
3395 self.attrs = attrs
3404 self.attrs = attrs
3396 # internal have priority over the given ones via attrs
3405 # internal have priority over the given ones via attrs
3397 self.internal = internal or ['versions']
3406 self.internal = internal or ['versions']
3398
3407
3399 def __getattr__(self, item):
3408 def __getattr__(self, item):
3400 if item in self.internal:
3409 if item in self.internal:
3401 return getattr(self, item)
3410 return getattr(self, item)
3402 try:
3411 try:
3403 return self.attrs[item]
3412 return self.attrs[item]
3404 except KeyError:
3413 except KeyError:
3405 raise AttributeError(
3414 raise AttributeError(
3406 '%s object has no attribute %s' % (self, item))
3415 '%s object has no attribute %s' % (self, item))
3407
3416
3408 def __repr__(self):
3417 def __repr__(self):
3409 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3418 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3410
3419
3411 def versions(self):
3420 def versions(self):
3412 return pull_request_obj.versions.order_by(
3421 return pull_request_obj.versions.order_by(
3413 PullRequestVersion.pull_request_version_id).all()
3422 PullRequestVersion.pull_request_version_id).all()
3414
3423
3415 def is_closed(self):
3424 def is_closed(self):
3416 return pull_request_obj.is_closed()
3425 return pull_request_obj.is_closed()
3417
3426
3418 @property
3427 @property
3419 def pull_request_version_id(self):
3428 def pull_request_version_id(self):
3420 return getattr(pull_request_obj, 'pull_request_version_id', None)
3429 return getattr(pull_request_obj, 'pull_request_version_id', None)
3421
3430
3422 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3431 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3423
3432
3424 attrs.author = StrictAttributeDict(
3433 attrs.author = StrictAttributeDict(
3425 pull_request_obj.author.get_api_data())
3434 pull_request_obj.author.get_api_data())
3426 if pull_request_obj.target_repo:
3435 if pull_request_obj.target_repo:
3427 attrs.target_repo = StrictAttributeDict(
3436 attrs.target_repo = StrictAttributeDict(
3428 pull_request_obj.target_repo.get_api_data())
3437 pull_request_obj.target_repo.get_api_data())
3429 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3438 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3430
3439
3431 if pull_request_obj.source_repo:
3440 if pull_request_obj.source_repo:
3432 attrs.source_repo = StrictAttributeDict(
3441 attrs.source_repo = StrictAttributeDict(
3433 pull_request_obj.source_repo.get_api_data())
3442 pull_request_obj.source_repo.get_api_data())
3434 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3443 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3435
3444
3436 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3445 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3437 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3446 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3438 attrs.revisions = pull_request_obj.revisions
3447 attrs.revisions = pull_request_obj.revisions
3439
3448
3440 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3449 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3450 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3451 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3441
3452
3442 return PullRequestDisplay(attrs, internal=internal_methods)
3453 return PullRequestDisplay(attrs, internal=internal_methods)
3443
3454
3444 def is_closed(self):
3455 def is_closed(self):
3445 return self.status == self.STATUS_CLOSED
3456 return self.status == self.STATUS_CLOSED
3446
3457
3447 def __json__(self):
3458 def __json__(self):
3448 return {
3459 return {
3449 'revisions': self.revisions,
3460 'revisions': self.revisions,
3450 }
3461 }
3451
3462
3452 def calculated_review_status(self):
3463 def calculated_review_status(self):
3453 from rhodecode.model.changeset_status import ChangesetStatusModel
3464 from rhodecode.model.changeset_status import ChangesetStatusModel
3454 return ChangesetStatusModel().calculated_review_status(self)
3465 return ChangesetStatusModel().calculated_review_status(self)
3455
3466
3456 def reviewers_statuses(self):
3467 def reviewers_statuses(self):
3457 from rhodecode.model.changeset_status import ChangesetStatusModel
3468 from rhodecode.model.changeset_status import ChangesetStatusModel
3458 return ChangesetStatusModel().reviewers_statuses(self)
3469 return ChangesetStatusModel().reviewers_statuses(self)
3459
3470
3460 @property
3471 @property
3461 def workspace_id(self):
3472 def workspace_id(self):
3462 from rhodecode.model.pull_request import PullRequestModel
3473 from rhodecode.model.pull_request import PullRequestModel
3463 return PullRequestModel()._workspace_id(self)
3474 return PullRequestModel()._workspace_id(self)
3464
3475
3465 def get_shadow_repo(self):
3476 def get_shadow_repo(self):
3466 workspace_id = self.workspace_id
3477 workspace_id = self.workspace_id
3467 vcs_obj = self.target_repo.scm_instance()
3478 vcs_obj = self.target_repo.scm_instance()
3468 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3479 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3469 workspace_id)
3480 workspace_id)
3470 return vcs_obj._get_shadow_instance(shadow_repository_path)
3481 return vcs_obj._get_shadow_instance(shadow_repository_path)
3471
3482
3472
3483
3473 class PullRequestVersion(Base, _PullRequestBase):
3484 class PullRequestVersion(Base, _PullRequestBase):
3474 __tablename__ = 'pull_request_versions'
3485 __tablename__ = 'pull_request_versions'
3475 __table_args__ = (
3486 __table_args__ = (
3476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3487 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3477 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3488 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3478 )
3489 )
3479
3490
3480 pull_request_version_id = Column(
3491 pull_request_version_id = Column(
3481 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3492 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3482 pull_request_id = Column(
3493 pull_request_id = Column(
3483 'pull_request_id', Integer(),
3494 'pull_request_id', Integer(),
3484 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3495 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3485 pull_request = relationship('PullRequest')
3496 pull_request = relationship('PullRequest')
3486
3497
3487 def __repr__(self):
3498 def __repr__(self):
3488 if self.pull_request_version_id:
3499 if self.pull_request_version_id:
3489 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3500 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3490 else:
3501 else:
3491 return '<DB:PullRequestVersion at %#x>' % id(self)
3502 return '<DB:PullRequestVersion at %#x>' % id(self)
3492
3503
3493 @property
3504 @property
3494 def reviewers(self):
3505 def reviewers(self):
3495 return self.pull_request.reviewers
3506 return self.pull_request.reviewers
3496
3507
3497 @property
3508 @property
3498 def versions(self):
3509 def versions(self):
3499 return self.pull_request.versions
3510 return self.pull_request.versions
3500
3511
3501 def is_closed(self):
3512 def is_closed(self):
3502 # calculate from original
3513 # calculate from original
3503 return self.pull_request.status == self.STATUS_CLOSED
3514 return self.pull_request.status == self.STATUS_CLOSED
3504
3515
3505 def calculated_review_status(self):
3516 def calculated_review_status(self):
3506 return self.pull_request.calculated_review_status()
3517 return self.pull_request.calculated_review_status()
3507
3518
3508 def reviewers_statuses(self):
3519 def reviewers_statuses(self):
3509 return self.pull_request.reviewers_statuses()
3520 return self.pull_request.reviewers_statuses()
3510
3521
3511
3522
3512 class PullRequestReviewers(Base, BaseModel):
3523 class PullRequestReviewers(Base, BaseModel):
3513 __tablename__ = 'pull_request_reviewers'
3524 __tablename__ = 'pull_request_reviewers'
3514 __table_args__ = (
3525 __table_args__ = (
3515 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3526 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3516 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3527 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3517 )
3528 )
3518
3529
3519 def __init__(self, user=None, pull_request=None, reasons=None):
3520 self.user = user
3521 self.pull_request = pull_request
3522 self.reasons = reasons or []
3523
3524 @hybrid_property
3530 @hybrid_property
3525 def reasons(self):
3531 def reasons(self):
3526 if not self._reasons:
3532 if not self._reasons:
3527 return []
3533 return []
3528 return self._reasons
3534 return self._reasons
3529
3535
3530 @reasons.setter
3536 @reasons.setter
3531 def reasons(self, val):
3537 def reasons(self, val):
3532 val = val or []
3538 val = val or []
3533 if any(not isinstance(x, basestring) for x in val):
3539 if any(not isinstance(x, basestring) for x in val):
3534 raise Exception('invalid reasons type, must be list of strings')
3540 raise Exception('invalid reasons type, must be list of strings')
3535 self._reasons = val
3541 self._reasons = val
3536
3542
3537 pull_requests_reviewers_id = Column(
3543 pull_requests_reviewers_id = Column(
3538 'pull_requests_reviewers_id', Integer(), nullable=False,
3544 'pull_requests_reviewers_id', Integer(), nullable=False,
3539 primary_key=True)
3545 primary_key=True)
3540 pull_request_id = Column(
3546 pull_request_id = Column(
3541 "pull_request_id", Integer(),
3547 "pull_request_id", Integer(),
3542 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3548 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3543 user_id = Column(
3549 user_id = Column(
3544 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3550 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3545 _reasons = Column(
3551 _reasons = Column(
3546 'reason', MutationList.as_mutable(
3552 'reason', MutationList.as_mutable(
3547 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3553 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3548
3554 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3549 user = relationship('User')
3555 user = relationship('User')
3550 pull_request = relationship('PullRequest')
3556 pull_request = relationship('PullRequest')
3551
3557
3552
3558
3553 class Notification(Base, BaseModel):
3559 class Notification(Base, BaseModel):
3554 __tablename__ = 'notifications'
3560 __tablename__ = 'notifications'
3555 __table_args__ = (
3561 __table_args__ = (
3556 Index('notification_type_idx', 'type'),
3562 Index('notification_type_idx', 'type'),
3557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3563 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3564 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3559 )
3565 )
3560
3566
3561 TYPE_CHANGESET_COMMENT = u'cs_comment'
3567 TYPE_CHANGESET_COMMENT = u'cs_comment'
3562 TYPE_MESSAGE = u'message'
3568 TYPE_MESSAGE = u'message'
3563 TYPE_MENTION = u'mention'
3569 TYPE_MENTION = u'mention'
3564 TYPE_REGISTRATION = u'registration'
3570 TYPE_REGISTRATION = u'registration'
3565 TYPE_PULL_REQUEST = u'pull_request'
3571 TYPE_PULL_REQUEST = u'pull_request'
3566 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3572 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3567
3573
3568 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3574 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3569 subject = Column('subject', Unicode(512), nullable=True)
3575 subject = Column('subject', Unicode(512), nullable=True)
3570 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3576 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3571 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3577 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3572 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3578 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3573 type_ = Column('type', Unicode(255))
3579 type_ = Column('type', Unicode(255))
3574
3580
3575 created_by_user = relationship('User')
3581 created_by_user = relationship('User')
3576 notifications_to_users = relationship('UserNotification', lazy='joined',
3582 notifications_to_users = relationship('UserNotification', lazy='joined',
3577 cascade="all, delete, delete-orphan")
3583 cascade="all, delete, delete-orphan")
3578
3584
3579 @property
3585 @property
3580 def recipients(self):
3586 def recipients(self):
3581 return [x.user for x in UserNotification.query()\
3587 return [x.user for x in UserNotification.query()\
3582 .filter(UserNotification.notification == self)\
3588 .filter(UserNotification.notification == self)\
3583 .order_by(UserNotification.user_id.asc()).all()]
3589 .order_by(UserNotification.user_id.asc()).all()]
3584
3590
3585 @classmethod
3591 @classmethod
3586 def create(cls, created_by, subject, body, recipients, type_=None):
3592 def create(cls, created_by, subject, body, recipients, type_=None):
3587 if type_ is None:
3593 if type_ is None:
3588 type_ = Notification.TYPE_MESSAGE
3594 type_ = Notification.TYPE_MESSAGE
3589
3595
3590 notification = cls()
3596 notification = cls()
3591 notification.created_by_user = created_by
3597 notification.created_by_user = created_by
3592 notification.subject = subject
3598 notification.subject = subject
3593 notification.body = body
3599 notification.body = body
3594 notification.type_ = type_
3600 notification.type_ = type_
3595 notification.created_on = datetime.datetime.now()
3601 notification.created_on = datetime.datetime.now()
3596
3602
3597 for u in recipients:
3603 for u in recipients:
3598 assoc = UserNotification()
3604 assoc = UserNotification()
3599 assoc.notification = notification
3605 assoc.notification = notification
3600
3606
3601 # if created_by is inside recipients mark his notification
3607 # if created_by is inside recipients mark his notification
3602 # as read
3608 # as read
3603 if u.user_id == created_by.user_id:
3609 if u.user_id == created_by.user_id:
3604 assoc.read = True
3610 assoc.read = True
3605
3611
3606 u.notifications.append(assoc)
3612 u.notifications.append(assoc)
3607 Session().add(notification)
3613 Session().add(notification)
3608
3614
3609 return notification
3615 return notification
3610
3616
3611 @property
3617 @property
3612 def description(self):
3618 def description(self):
3613 from rhodecode.model.notification import NotificationModel
3619 from rhodecode.model.notification import NotificationModel
3614 return NotificationModel().make_description(self)
3620 return NotificationModel().make_description(self)
3615
3621
3616
3622
3617 class UserNotification(Base, BaseModel):
3623 class UserNotification(Base, BaseModel):
3618 __tablename__ = 'user_to_notification'
3624 __tablename__ = 'user_to_notification'
3619 __table_args__ = (
3625 __table_args__ = (
3620 UniqueConstraint('user_id', 'notification_id'),
3626 UniqueConstraint('user_id', 'notification_id'),
3621 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3627 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3622 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3628 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3623 )
3629 )
3624 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3630 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3625 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3631 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3626 read = Column('read', Boolean, default=False)
3632 read = Column('read', Boolean, default=False)
3627 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3633 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3628
3634
3629 user = relationship('User', lazy="joined")
3635 user = relationship('User', lazy="joined")
3630 notification = relationship('Notification', lazy="joined",
3636 notification = relationship('Notification', lazy="joined",
3631 order_by=lambda: Notification.created_on.desc(),)
3637 order_by=lambda: Notification.created_on.desc(),)
3632
3638
3633 def mark_as_read(self):
3639 def mark_as_read(self):
3634 self.read = True
3640 self.read = True
3635 Session().add(self)
3641 Session().add(self)
3636
3642
3637
3643
3638 class Gist(Base, BaseModel):
3644 class Gist(Base, BaseModel):
3639 __tablename__ = 'gists'
3645 __tablename__ = 'gists'
3640 __table_args__ = (
3646 __table_args__ = (
3641 Index('g_gist_access_id_idx', 'gist_access_id'),
3647 Index('g_gist_access_id_idx', 'gist_access_id'),
3642 Index('g_created_on_idx', 'created_on'),
3648 Index('g_created_on_idx', 'created_on'),
3643 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3649 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3644 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3650 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3645 )
3651 )
3646 GIST_PUBLIC = u'public'
3652 GIST_PUBLIC = u'public'
3647 GIST_PRIVATE = u'private'
3653 GIST_PRIVATE = u'private'
3648 DEFAULT_FILENAME = u'gistfile1.txt'
3654 DEFAULT_FILENAME = u'gistfile1.txt'
3649
3655
3650 ACL_LEVEL_PUBLIC = u'acl_public'
3656 ACL_LEVEL_PUBLIC = u'acl_public'
3651 ACL_LEVEL_PRIVATE = u'acl_private'
3657 ACL_LEVEL_PRIVATE = u'acl_private'
3652
3658
3653 gist_id = Column('gist_id', Integer(), primary_key=True)
3659 gist_id = Column('gist_id', Integer(), primary_key=True)
3654 gist_access_id = Column('gist_access_id', Unicode(250))
3660 gist_access_id = Column('gist_access_id', Unicode(250))
3655 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3661 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3656 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3662 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3657 gist_expires = Column('gist_expires', Float(53), nullable=False)
3663 gist_expires = Column('gist_expires', Float(53), nullable=False)
3658 gist_type = Column('gist_type', Unicode(128), nullable=False)
3664 gist_type = Column('gist_type', Unicode(128), nullable=False)
3659 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3665 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3660 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3666 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3661 acl_level = Column('acl_level', Unicode(128), nullable=True)
3667 acl_level = Column('acl_level', Unicode(128), nullable=True)
3662
3668
3663 owner = relationship('User')
3669 owner = relationship('User')
3664
3670
3665 def __repr__(self):
3671 def __repr__(self):
3666 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3672 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3667
3673
3668 @classmethod
3674 @classmethod
3669 def get_or_404(cls, id_, pyramid_exc=False):
3675 def get_or_404(cls, id_, pyramid_exc=False):
3670
3676
3671 if pyramid_exc:
3677 if pyramid_exc:
3672 from pyramid.httpexceptions import HTTPNotFound
3678 from pyramid.httpexceptions import HTTPNotFound
3673 else:
3679 else:
3674 from webob.exc import HTTPNotFound
3680 from webob.exc import HTTPNotFound
3675
3681
3676 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3682 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3677 if not res:
3683 if not res:
3678 raise HTTPNotFound
3684 raise HTTPNotFound
3679 return res
3685 return res
3680
3686
3681 @classmethod
3687 @classmethod
3682 def get_by_access_id(cls, gist_access_id):
3688 def get_by_access_id(cls, gist_access_id):
3683 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3689 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3684
3690
3685 def gist_url(self):
3691 def gist_url(self):
3686 import rhodecode
3692 import rhodecode
3687 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3693 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3688 if alias_url:
3694 if alias_url:
3689 return alias_url.replace('{gistid}', self.gist_access_id)
3695 return alias_url.replace('{gistid}', self.gist_access_id)
3690
3696
3691 return url('gist', gist_id=self.gist_access_id, qualified=True)
3697 return url('gist', gist_id=self.gist_access_id, qualified=True)
3692
3698
3693 @classmethod
3699 @classmethod
3694 def base_path(cls):
3700 def base_path(cls):
3695 """
3701 """
3696 Returns base path when all gists are stored
3702 Returns base path when all gists are stored
3697
3703
3698 :param cls:
3704 :param cls:
3699 """
3705 """
3700 from rhodecode.model.gist import GIST_STORE_LOC
3706 from rhodecode.model.gist import GIST_STORE_LOC
3701 q = Session().query(RhodeCodeUi)\
3707 q = Session().query(RhodeCodeUi)\
3702 .filter(RhodeCodeUi.ui_key == URL_SEP)
3708 .filter(RhodeCodeUi.ui_key == URL_SEP)
3703 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3709 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3704 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3710 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3705
3711
3706 def get_api_data(self):
3712 def get_api_data(self):
3707 """
3713 """
3708 Common function for generating gist related data for API
3714 Common function for generating gist related data for API
3709 """
3715 """
3710 gist = self
3716 gist = self
3711 data = {
3717 data = {
3712 'gist_id': gist.gist_id,
3718 'gist_id': gist.gist_id,
3713 'type': gist.gist_type,
3719 'type': gist.gist_type,
3714 'access_id': gist.gist_access_id,
3720 'access_id': gist.gist_access_id,
3715 'description': gist.gist_description,
3721 'description': gist.gist_description,
3716 'url': gist.gist_url(),
3722 'url': gist.gist_url(),
3717 'expires': gist.gist_expires,
3723 'expires': gist.gist_expires,
3718 'created_on': gist.created_on,
3724 'created_on': gist.created_on,
3719 'modified_at': gist.modified_at,
3725 'modified_at': gist.modified_at,
3720 'content': None,
3726 'content': None,
3721 'acl_level': gist.acl_level,
3727 'acl_level': gist.acl_level,
3722 }
3728 }
3723 return data
3729 return data
3724
3730
3725 def __json__(self):
3731 def __json__(self):
3726 data = dict(
3732 data = dict(
3727 )
3733 )
3728 data.update(self.get_api_data())
3734 data.update(self.get_api_data())
3729 return data
3735 return data
3730 # SCM functions
3736 # SCM functions
3731
3737
3732 def scm_instance(self, **kwargs):
3738 def scm_instance(self, **kwargs):
3733 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3739 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3734 return get_vcs_instance(
3740 return get_vcs_instance(
3735 repo_path=safe_str(full_repo_path), create=False)
3741 repo_path=safe_str(full_repo_path), create=False)
3736
3742
3737
3743
3738 class ExternalIdentity(Base, BaseModel):
3744 class ExternalIdentity(Base, BaseModel):
3739 __tablename__ = 'external_identities'
3745 __tablename__ = 'external_identities'
3740 __table_args__ = (
3746 __table_args__ = (
3741 Index('local_user_id_idx', 'local_user_id'),
3747 Index('local_user_id_idx', 'local_user_id'),
3742 Index('external_id_idx', 'external_id'),
3748 Index('external_id_idx', 'external_id'),
3743 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3749 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3744 'mysql_charset': 'utf8'})
3750 'mysql_charset': 'utf8'})
3745
3751
3746 external_id = Column('external_id', Unicode(255), default=u'',
3752 external_id = Column('external_id', Unicode(255), default=u'',
3747 primary_key=True)
3753 primary_key=True)
3748 external_username = Column('external_username', Unicode(1024), default=u'')
3754 external_username = Column('external_username', Unicode(1024), default=u'')
3749 local_user_id = Column('local_user_id', Integer(),
3755 local_user_id = Column('local_user_id', Integer(),
3750 ForeignKey('users.user_id'), primary_key=True)
3756 ForeignKey('users.user_id'), primary_key=True)
3751 provider_name = Column('provider_name', Unicode(255), default=u'',
3757 provider_name = Column('provider_name', Unicode(255), default=u'',
3752 primary_key=True)
3758 primary_key=True)
3753 access_token = Column('access_token', String(1024), default=u'')
3759 access_token = Column('access_token', String(1024), default=u'')
3754 alt_token = Column('alt_token', String(1024), default=u'')
3760 alt_token = Column('alt_token', String(1024), default=u'')
3755 token_secret = Column('token_secret', String(1024), default=u'')
3761 token_secret = Column('token_secret', String(1024), default=u'')
3756
3762
3757 @classmethod
3763 @classmethod
3758 def by_external_id_and_provider(cls, external_id, provider_name,
3764 def by_external_id_and_provider(cls, external_id, provider_name,
3759 local_user_id=None):
3765 local_user_id=None):
3760 """
3766 """
3761 Returns ExternalIdentity instance based on search params
3767 Returns ExternalIdentity instance based on search params
3762
3768
3763 :param external_id:
3769 :param external_id:
3764 :param provider_name:
3770 :param provider_name:
3765 :return: ExternalIdentity
3771 :return: ExternalIdentity
3766 """
3772 """
3767 query = cls.query()
3773 query = cls.query()
3768 query = query.filter(cls.external_id == external_id)
3774 query = query.filter(cls.external_id == external_id)
3769 query = query.filter(cls.provider_name == provider_name)
3775 query = query.filter(cls.provider_name == provider_name)
3770 if local_user_id:
3776 if local_user_id:
3771 query = query.filter(cls.local_user_id == local_user_id)
3777 query = query.filter(cls.local_user_id == local_user_id)
3772 return query.first()
3778 return query.first()
3773
3779
3774 @classmethod
3780 @classmethod
3775 def user_by_external_id_and_provider(cls, external_id, provider_name):
3781 def user_by_external_id_and_provider(cls, external_id, provider_name):
3776 """
3782 """
3777 Returns User instance based on search params
3783 Returns User instance based on search params
3778
3784
3779 :param external_id:
3785 :param external_id:
3780 :param provider_name:
3786 :param provider_name:
3781 :return: User
3787 :return: User
3782 """
3788 """
3783 query = User.query()
3789 query = User.query()
3784 query = query.filter(cls.external_id == external_id)
3790 query = query.filter(cls.external_id == external_id)
3785 query = query.filter(cls.provider_name == provider_name)
3791 query = query.filter(cls.provider_name == provider_name)
3786 query = query.filter(User.user_id == cls.local_user_id)
3792 query = query.filter(User.user_id == cls.local_user_id)
3787 return query.first()
3793 return query.first()
3788
3794
3789 @classmethod
3795 @classmethod
3790 def by_local_user_id(cls, local_user_id):
3796 def by_local_user_id(cls, local_user_id):
3791 """
3797 """
3792 Returns all tokens for user
3798 Returns all tokens for user
3793
3799
3794 :param local_user_id:
3800 :param local_user_id:
3795 :return: ExternalIdentity
3801 :return: ExternalIdentity
3796 """
3802 """
3797 query = cls.query()
3803 query = cls.query()
3798 query = query.filter(cls.local_user_id == local_user_id)
3804 query = query.filter(cls.local_user_id == local_user_id)
3799 return query
3805 return query
3800
3806
3801
3807
3802 class Integration(Base, BaseModel):
3808 class Integration(Base, BaseModel):
3803 __tablename__ = 'integrations'
3809 __tablename__ = 'integrations'
3804 __table_args__ = (
3810 __table_args__ = (
3805 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3811 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3806 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3812 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3807 )
3813 )
3808
3814
3809 integration_id = Column('integration_id', Integer(), primary_key=True)
3815 integration_id = Column('integration_id', Integer(), primary_key=True)
3810 integration_type = Column('integration_type', String(255))
3816 integration_type = Column('integration_type', String(255))
3811 enabled = Column('enabled', Boolean(), nullable=False)
3817 enabled = Column('enabled', Boolean(), nullable=False)
3812 name = Column('name', String(255), nullable=False)
3818 name = Column('name', String(255), nullable=False)
3813 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3819 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3814 default=False)
3820 default=False)
3815
3821
3816 settings = Column(
3822 settings = Column(
3817 'settings_json', MutationObj.as_mutable(
3823 'settings_json', MutationObj.as_mutable(
3818 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3824 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3819 repo_id = Column(
3825 repo_id = Column(
3820 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3826 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3821 nullable=True, unique=None, default=None)
3827 nullable=True, unique=None, default=None)
3822 repo = relationship('Repository', lazy='joined')
3828 repo = relationship('Repository', lazy='joined')
3823
3829
3824 repo_group_id = Column(
3830 repo_group_id = Column(
3825 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3831 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3826 nullable=True, unique=None, default=None)
3832 nullable=True, unique=None, default=None)
3827 repo_group = relationship('RepoGroup', lazy='joined')
3833 repo_group = relationship('RepoGroup', lazy='joined')
3828
3834
3829 @property
3835 @property
3830 def scope(self):
3836 def scope(self):
3831 if self.repo:
3837 if self.repo:
3832 return repr(self.repo)
3838 return repr(self.repo)
3833 if self.repo_group:
3839 if self.repo_group:
3834 if self.child_repos_only:
3840 if self.child_repos_only:
3835 return repr(self.repo_group) + ' (child repos only)'
3841 return repr(self.repo_group) + ' (child repos only)'
3836 else:
3842 else:
3837 return repr(self.repo_group) + ' (recursive)'
3843 return repr(self.repo_group) + ' (recursive)'
3838 if self.child_repos_only:
3844 if self.child_repos_only:
3839 return 'root_repos'
3845 return 'root_repos'
3840 return 'global'
3846 return 'global'
3841
3847
3842 def __repr__(self):
3848 def __repr__(self):
3843 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3849 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3844
3850
3845
3851
3846 class RepoReviewRuleUser(Base, BaseModel):
3852 class RepoReviewRuleUser(Base, BaseModel):
3847 __tablename__ = 'repo_review_rules_users'
3853 __tablename__ = 'repo_review_rules_users'
3848 __table_args__ = (
3854 __table_args__ = (
3849 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3855 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3850 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3856 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3851 )
3857 )
3852 repo_review_rule_user_id = Column(
3858 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3853 'repo_review_rule_user_id', Integer(), primary_key=True)
3859 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3854 repo_review_rule_id = Column("repo_review_rule_id",
3860 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3855 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3861 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3856 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3857 nullable=False)
3858 user = relationship('User')
3862 user = relationship('User')
3859
3863
3864 def rule_data(self):
3865 return {
3866 'mandatory': self.mandatory
3867 }
3868
3860
3869
3861 class RepoReviewRuleUserGroup(Base, BaseModel):
3870 class RepoReviewRuleUserGroup(Base, BaseModel):
3862 __tablename__ = 'repo_review_rules_users_groups'
3871 __tablename__ = 'repo_review_rules_users_groups'
3863 __table_args__ = (
3872 __table_args__ = (
3864 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3873 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3865 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3874 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3866 )
3875 )
3867 repo_review_rule_users_group_id = Column(
3876 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3868 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3877 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3869 repo_review_rule_id = Column("repo_review_rule_id",
3878 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3870 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3879 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3871 users_group_id = Column("users_group_id", Integer(),
3872 ForeignKey('users_groups.users_group_id'), nullable=False)
3873 users_group = relationship('UserGroup')
3880 users_group = relationship('UserGroup')
3874
3881
3882 def rule_data(self):
3883 return {
3884 'mandatory': self.mandatory
3885 }
3886
3875
3887
3876 class RepoReviewRule(Base, BaseModel):
3888 class RepoReviewRule(Base, BaseModel):
3877 __tablename__ = 'repo_review_rules'
3889 __tablename__ = 'repo_review_rules'
3878 __table_args__ = (
3890 __table_args__ = (
3879 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3891 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3880 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3892 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3881 )
3893 )
3882
3894
3883 repo_review_rule_id = Column(
3895 repo_review_rule_id = Column(
3884 'repo_review_rule_id', Integer(), primary_key=True)
3896 'repo_review_rule_id', Integer(), primary_key=True)
3885 repo_id = Column(
3897 repo_id = Column(
3886 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3898 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3887 repo = relationship('Repository', backref='review_rules')
3899 repo = relationship('Repository', backref='review_rules')
3888
3900
3889 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3901 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3890 default=u'*') # glob
3902 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3891 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3903
3892 default=u'*') # glob
3904 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3893
3905 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3894 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3906 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3895 nullable=False, default=False)
3907
3896 rule_users = relationship('RepoReviewRuleUser')
3908 rule_users = relationship('RepoReviewRuleUser')
3897 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3909 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3898
3910
3899 @hybrid_property
3911 @hybrid_property
3900 def branch_pattern(self):
3912 def branch_pattern(self):
3901 return self._branch_pattern or '*'
3913 return self._branch_pattern or '*'
3902
3914
3903 def _validate_glob(self, value):
3915 def _validate_glob(self, value):
3904 re.compile('^' + glob2re(value) + '$')
3916 re.compile('^' + glob2re(value) + '$')
3905
3917
3906 @branch_pattern.setter
3918 @branch_pattern.setter
3907 def branch_pattern(self, value):
3919 def branch_pattern(self, value):
3908 self._validate_glob(value)
3920 self._validate_glob(value)
3909 self._branch_pattern = value or '*'
3921 self._branch_pattern = value or '*'
3910
3922
3911 @hybrid_property
3923 @hybrid_property
3912 def file_pattern(self):
3924 def file_pattern(self):
3913 return self._file_pattern or '*'
3925 return self._file_pattern or '*'
3914
3926
3915 @file_pattern.setter
3927 @file_pattern.setter
3916 def file_pattern(self, value):
3928 def file_pattern(self, value):
3917 self._validate_glob(value)
3929 self._validate_glob(value)
3918 self._file_pattern = value or '*'
3930 self._file_pattern = value or '*'
3919
3931
3920 def matches(self, branch, files_changed):
3932 def matches(self, branch, files_changed):
3921 """
3933 """
3922 Check if this review rule matches a branch/files in a pull request
3934 Check if this review rule matches a branch/files in a pull request
3923
3935
3924 :param branch: branch name for the commit
3936 :param branch: branch name for the commit
3925 :param files_changed: list of file paths changed in the pull request
3937 :param files_changed: list of file paths changed in the pull request
3926 """
3938 """
3927
3939
3928 branch = branch or ''
3940 branch = branch or ''
3929 files_changed = files_changed or []
3941 files_changed = files_changed or []
3930
3942
3931 branch_matches = True
3943 branch_matches = True
3932 if branch:
3944 if branch:
3933 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3945 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3934 branch_matches = bool(branch_regex.search(branch))
3946 branch_matches = bool(branch_regex.search(branch))
3935
3947
3936 files_matches = True
3948 files_matches = True
3937 if self.file_pattern != '*':
3949 if self.file_pattern != '*':
3938 files_matches = False
3950 files_matches = False
3939 file_regex = re.compile(glob2re(self.file_pattern))
3951 file_regex = re.compile(glob2re(self.file_pattern))
3940 for filename in files_changed:
3952 for filename in files_changed:
3941 if file_regex.search(filename):
3953 if file_regex.search(filename):
3942 files_matches = True
3954 files_matches = True
3943 break
3955 break
3944
3956
3945 return branch_matches and files_matches
3957 return branch_matches and files_matches
3946
3958
3947 @property
3959 @property
3948 def review_users(self):
3960 def review_users(self):
3949 """ Returns the users which this rule applies to """
3961 """ Returns the users which this rule applies to """
3950
3962
3951 users = set()
3963 users = collections.OrderedDict()
3952 users |= set([
3964
3953 rule_user.user for rule_user in self.rule_users
3965 for rule_user in self.rule_users:
3954 if rule_user.user.active])
3966 if rule_user.user.active:
3955 users |= set(
3967 if rule_user.user not in users:
3956 member.user
3968 users[rule_user.user.username] = {
3957 for rule_user_group in self.rule_user_groups
3969 'user': rule_user.user,
3958 for member in rule_user_group.users_group.members
3970 'source': 'user',
3959 if member.user.active
3971 'data': rule_user.rule_data()
3960 )
3972 }
3973
3974 for rule_user_group in self.rule_user_groups:
3975 for member in rule_user_group.users_group.members:
3976 if member.user.active:
3977 users[member.user.username] = {
3978 'user': member.user,
3979 'source': 'user_group',
3980 'data': rule_user_group.rule_data()
3981 }
3982
3961 return users
3983 return users
3962
3984
3963 def __repr__(self):
3985 def __repr__(self):
3964 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3986 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3965 self.repo_review_rule_id, self.repo)
3987 self.repo_review_rule_id, self.repo)
3966
3988
3967
3989
3968 class DbMigrateVersion(Base, BaseModel):
3990 class DbMigrateVersion(Base, BaseModel):
3969 __tablename__ = 'db_migrate_version'
3991 __tablename__ = 'db_migrate_version'
3970 __table_args__ = (
3992 __table_args__ = (
3971 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3993 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3972 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3994 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3973 )
3995 )
3974 repository_id = Column('repository_id', String(250), primary_key=True)
3996 repository_id = Column('repository_id', String(250), primary_key=True)
3975 repository_path = Column('repository_path', Text)
3997 repository_path = Column('repository_path', Text)
3976 version = Column('version', Integer)
3998 version = Column('version', Integer)
3977
3999
3978
4000
3979 class DbSession(Base, BaseModel):
4001 class DbSession(Base, BaseModel):
3980 __tablename__ = 'db_session'
4002 __tablename__ = 'db_session'
3981 __table_args__ = (
4003 __table_args__ = (
3982 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4004 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3983 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4005 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3984 )
4006 )
3985
4007
3986 def __repr__(self):
4008 def __repr__(self):
3987 return '<DB:DbSession({})>'.format(self.id)
4009 return '<DB:DbSession({})>'.format(self.id)
3988
4010
3989 id = Column('id', Integer())
4011 id = Column('id', Integer())
3990 namespace = Column('namespace', String(255), primary_key=True)
4012 namespace = Column('namespace', String(255), primary_key=True)
3991 accessed = Column('accessed', DateTime, nullable=False)
4013 accessed = Column('accessed', DateTime, nullable=False)
3992 created = Column('created', DateTime, nullable=False)
4014 created = Column('created', DateTime, nullable=False)
3993 data = Column('data', PickleType, nullable=False)
4015 data = Column('data', PickleType, nullable=False)
@@ -1,562 +1,563 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 this is forms validation classes
22 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
23 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
24 for list off all availible validators
25
25
26 we can create our own validators
26 we can create our own validators
27
27
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
29 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
30 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35
35
36
36
37 <name> = formencode.validators.<name of validator>
37 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
38 <name> must equal form name
39 list=[1,2,3,4,5]
39 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
40 for SELECT use formencode.All(OneOf(list), Int())
41
41
42 """
42 """
43
43
44 import deform
44 import deform
45 import logging
45 import logging
46 import formencode
46 import formencode
47
47
48 from pkg_resources import resource_filename
48 from pkg_resources import resource_filename
49 from formencode import All, Pipe
49 from formencode import All, Pipe
50
50
51 from pylons.i18n.translation import _
51 from pylons.i18n.translation import _
52
52
53 from rhodecode import BACKENDS
53 from rhodecode import BACKENDS
54 from rhodecode.lib import helpers
54 from rhodecode.lib import helpers
55 from rhodecode.model import validators as v
55 from rhodecode.model import validators as v
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 deform_templates = resource_filename('deform', 'templates')
60 deform_templates = resource_filename('deform', 'templates')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 search_path = (rhodecode_templates, deform_templates)
62 search_path = (rhodecode_templates, deform_templates)
63
63
64
64
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 def __call__(self, template_name, **kw):
67 def __call__(self, template_name, **kw):
68 kw['h'] = helpers
68 kw['h'] = helpers
69 return self.load(template_name)(**kw)
69 return self.load(template_name)(**kw)
70
70
71
71
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 deform.Form.set_default_renderer(form_renderer)
73 deform.Form.set_default_renderer(form_renderer)
74
74
75
75
76 def LoginForm():
76 def LoginForm():
77 class _LoginForm(formencode.Schema):
77 class _LoginForm(formencode.Schema):
78 allow_extra_fields = True
78 allow_extra_fields = True
79 filter_extra_fields = True
79 filter_extra_fields = True
80 username = v.UnicodeString(
80 username = v.UnicodeString(
81 strip=True,
81 strip=True,
82 min=1,
82 min=1,
83 not_empty=True,
83 not_empty=True,
84 messages={
84 messages={
85 'empty': _(u'Please enter a login'),
85 'empty': _(u'Please enter a login'),
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 }
87 }
88 )
88 )
89
89
90 password = v.UnicodeString(
90 password = v.UnicodeString(
91 strip=False,
91 strip=False,
92 min=3,
92 min=3,
93 not_empty=True,
93 not_empty=True,
94 messages={
94 messages={
95 'empty': _(u'Please enter a password'),
95 'empty': _(u'Please enter a password'),
96 'tooShort': _(u'Enter %(min)i characters or more')}
96 'tooShort': _(u'Enter %(min)i characters or more')}
97 )
97 )
98
98
99 remember = v.StringBoolean(if_missing=False)
99 remember = v.StringBoolean(if_missing=False)
100
100
101 chained_validators = [v.ValidAuth()]
101 chained_validators = [v.ValidAuth()]
102 return _LoginForm
102 return _LoginForm
103
103
104
104
105 def UserForm(edit=False, available_languages=[], old_data={}):
105 def UserForm(edit=False, available_languages=[], old_data={}):
106 class _UserForm(formencode.Schema):
106 class _UserForm(formencode.Schema):
107 allow_extra_fields = True
107 allow_extra_fields = True
108 filter_extra_fields = True
108 filter_extra_fields = True
109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 v.ValidUsername(edit, old_data))
110 v.ValidUsername(edit, old_data))
111 if edit:
111 if edit:
112 new_password = All(
112 new_password = All(
113 v.ValidPassword(),
113 v.ValidPassword(),
114 v.UnicodeString(strip=False, min=6, not_empty=False)
114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 )
115 )
116 password_confirmation = All(
116 password_confirmation = All(
117 v.ValidPassword(),
117 v.ValidPassword(),
118 v.UnicodeString(strip=False, min=6, not_empty=False),
118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 )
119 )
120 admin = v.StringBoolean(if_missing=False)
120 admin = v.StringBoolean(if_missing=False)
121 else:
121 else:
122 password = All(
122 password = All(
123 v.ValidPassword(),
123 v.ValidPassword(),
124 v.UnicodeString(strip=False, min=6, not_empty=True)
124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 )
125 )
126 password_confirmation = All(
126 password_confirmation = All(
127 v.ValidPassword(),
127 v.ValidPassword(),
128 v.UnicodeString(strip=False, min=6, not_empty=False)
128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 )
129 )
130
130
131 password_change = v.StringBoolean(if_missing=False)
131 password_change = v.StringBoolean(if_missing=False)
132 create_repo_group = v.StringBoolean(if_missing=False)
132 create_repo_group = v.StringBoolean(if_missing=False)
133
133
134 active = v.StringBoolean(if_missing=False)
134 active = v.StringBoolean(if_missing=False)
135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 extern_name = v.UnicodeString(strip=True)
138 extern_name = v.UnicodeString(strip=True)
139 extern_type = v.UnicodeString(strip=True)
139 extern_type = v.UnicodeString(strip=True)
140 language = v.OneOf(available_languages, hideList=False,
140 language = v.OneOf(available_languages, hideList=False,
141 testValueList=True, if_missing=None)
141 testValueList=True, if_missing=None)
142 chained_validators = [v.ValidPasswordsMatch()]
142 chained_validators = [v.ValidPasswordsMatch()]
143 return _UserForm
143 return _UserForm
144
144
145
145
146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
146 def UserGroupForm(edit=False, old_data=None, allow_disabled=False):
147 old_data = old_data or {}
147 old_data = old_data or {}
148
148
149 class _UserGroupForm(formencode.Schema):
149 class _UserGroupForm(formencode.Schema):
150 allow_extra_fields = True
150 allow_extra_fields = True
151 filter_extra_fields = True
151 filter_extra_fields = True
152
152
153 users_group_name = All(
153 users_group_name = All(
154 v.UnicodeString(strip=True, min=1, not_empty=True),
154 v.UnicodeString(strip=True, min=1, not_empty=True),
155 v.ValidUserGroup(edit, old_data)
155 v.ValidUserGroup(edit, old_data)
156 )
156 )
157 user_group_description = v.UnicodeString(strip=True, min=1,
157 user_group_description = v.UnicodeString(strip=True, min=1,
158 not_empty=False)
158 not_empty=False)
159
159
160 users_group_active = v.StringBoolean(if_missing=False)
160 users_group_active = v.StringBoolean(if_missing=False)
161
161
162 if edit:
162 if edit:
163 # this is user group owner
163 # this is user group owner
164 user = All(
164 user = All(
165 v.UnicodeString(not_empty=True),
165 v.UnicodeString(not_empty=True),
166 v.ValidRepoUser(allow_disabled))
166 v.ValidRepoUser(allow_disabled))
167 return _UserGroupForm
167 return _UserGroupForm
168
168
169
169
170 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
170 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
171 can_create_in_root=False, allow_disabled=False):
171 can_create_in_root=False, allow_disabled=False):
172 old_data = old_data or {}
172 old_data = old_data or {}
173 available_groups = available_groups or []
173 available_groups = available_groups or []
174
174
175 class _RepoGroupForm(formencode.Schema):
175 class _RepoGroupForm(formencode.Schema):
176 allow_extra_fields = True
176 allow_extra_fields = True
177 filter_extra_fields = False
177 filter_extra_fields = False
178
178
179 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
179 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
180 v.SlugifyName(),)
180 v.SlugifyName(),)
181 group_description = v.UnicodeString(strip=True, min=1,
181 group_description = v.UnicodeString(strip=True, min=1,
182 not_empty=False)
182 not_empty=False)
183 group_copy_permissions = v.StringBoolean(if_missing=False)
183 group_copy_permissions = v.StringBoolean(if_missing=False)
184
184
185 group_parent_id = v.OneOf(available_groups, hideList=False,
185 group_parent_id = v.OneOf(available_groups, hideList=False,
186 testValueList=True, not_empty=True)
186 testValueList=True, not_empty=True)
187 enable_locking = v.StringBoolean(if_missing=False)
187 enable_locking = v.StringBoolean(if_missing=False)
188 chained_validators = [
188 chained_validators = [
189 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
189 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
190
190
191 if edit:
191 if edit:
192 # this is repo group owner
192 # this is repo group owner
193 user = All(
193 user = All(
194 v.UnicodeString(not_empty=True),
194 v.UnicodeString(not_empty=True),
195 v.ValidRepoUser(allow_disabled))
195 v.ValidRepoUser(allow_disabled))
196
196
197 return _RepoGroupForm
197 return _RepoGroupForm
198
198
199
199
200 def RegisterForm(edit=False, old_data={}):
200 def RegisterForm(edit=False, old_data={}):
201 class _RegisterForm(formencode.Schema):
201 class _RegisterForm(formencode.Schema):
202 allow_extra_fields = True
202 allow_extra_fields = True
203 filter_extra_fields = True
203 filter_extra_fields = True
204 username = All(
204 username = All(
205 v.ValidUsername(edit, old_data),
205 v.ValidUsername(edit, old_data),
206 v.UnicodeString(strip=True, min=1, not_empty=True)
206 v.UnicodeString(strip=True, min=1, not_empty=True)
207 )
207 )
208 password = All(
208 password = All(
209 v.ValidPassword(),
209 v.ValidPassword(),
210 v.UnicodeString(strip=False, min=6, not_empty=True)
210 v.UnicodeString(strip=False, min=6, not_empty=True)
211 )
211 )
212 password_confirmation = All(
212 password_confirmation = All(
213 v.ValidPassword(),
213 v.ValidPassword(),
214 v.UnicodeString(strip=False, min=6, not_empty=True)
214 v.UnicodeString(strip=False, min=6, not_empty=True)
215 )
215 )
216 active = v.StringBoolean(if_missing=False)
216 active = v.StringBoolean(if_missing=False)
217 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
217 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
218 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
218 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
219 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
219 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
220
220
221 chained_validators = [v.ValidPasswordsMatch()]
221 chained_validators = [v.ValidPasswordsMatch()]
222
222
223 return _RegisterForm
223 return _RegisterForm
224
224
225
225
226 def PasswordResetForm():
226 def PasswordResetForm():
227 class _PasswordResetForm(formencode.Schema):
227 class _PasswordResetForm(formencode.Schema):
228 allow_extra_fields = True
228 allow_extra_fields = True
229 filter_extra_fields = True
229 filter_extra_fields = True
230 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
230 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
231 return _PasswordResetForm
231 return _PasswordResetForm
232
232
233
233
234 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
234 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
235 allow_disabled=False):
235 allow_disabled=False):
236 old_data = old_data or {}
236 old_data = old_data or {}
237 repo_groups = repo_groups or []
237 repo_groups = repo_groups or []
238 landing_revs = landing_revs or []
238 landing_revs = landing_revs or []
239 supported_backends = BACKENDS.keys()
239 supported_backends = BACKENDS.keys()
240
240
241 class _RepoForm(formencode.Schema):
241 class _RepoForm(formencode.Schema):
242 allow_extra_fields = True
242 allow_extra_fields = True
243 filter_extra_fields = False
243 filter_extra_fields = False
244 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
244 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
245 v.SlugifyName(), v.CannotHaveGitSuffix())
245 v.SlugifyName(), v.CannotHaveGitSuffix())
246 repo_group = All(v.CanWriteGroup(old_data),
246 repo_group = All(v.CanWriteGroup(old_data),
247 v.OneOf(repo_groups, hideList=True))
247 v.OneOf(repo_groups, hideList=True))
248 repo_type = v.OneOf(supported_backends, required=False,
248 repo_type = v.OneOf(supported_backends, required=False,
249 if_missing=old_data.get('repo_type'))
249 if_missing=old_data.get('repo_type'))
250 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
250 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
251 repo_private = v.StringBoolean(if_missing=False)
251 repo_private = v.StringBoolean(if_missing=False)
252 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
252 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
253 repo_copy_permissions = v.StringBoolean(if_missing=False)
253 repo_copy_permissions = v.StringBoolean(if_missing=False)
254 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
254 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
255
255
256 repo_enable_statistics = v.StringBoolean(if_missing=False)
256 repo_enable_statistics = v.StringBoolean(if_missing=False)
257 repo_enable_downloads = v.StringBoolean(if_missing=False)
257 repo_enable_downloads = v.StringBoolean(if_missing=False)
258 repo_enable_locking = v.StringBoolean(if_missing=False)
258 repo_enable_locking = v.StringBoolean(if_missing=False)
259
259
260 if edit:
260 if edit:
261 # this is repo owner
261 # this is repo owner
262 user = All(
262 user = All(
263 v.UnicodeString(not_empty=True),
263 v.UnicodeString(not_empty=True),
264 v.ValidRepoUser(allow_disabled))
264 v.ValidRepoUser(allow_disabled))
265 clone_uri_change = v.UnicodeString(
265 clone_uri_change = v.UnicodeString(
266 not_empty=False, if_missing=v.Missing)
266 not_empty=False, if_missing=v.Missing)
267
267
268 chained_validators = [v.ValidCloneUri(),
268 chained_validators = [v.ValidCloneUri(),
269 v.ValidRepoName(edit, old_data)]
269 v.ValidRepoName(edit, old_data)]
270 return _RepoForm
270 return _RepoForm
271
271
272
272
273 def RepoPermsForm():
273 def RepoPermsForm():
274 class _RepoPermsForm(formencode.Schema):
274 class _RepoPermsForm(formencode.Schema):
275 allow_extra_fields = True
275 allow_extra_fields = True
276 filter_extra_fields = False
276 filter_extra_fields = False
277 chained_validators = [v.ValidPerms(type_='repo')]
277 chained_validators = [v.ValidPerms(type_='repo')]
278 return _RepoPermsForm
278 return _RepoPermsForm
279
279
280
280
281 def RepoGroupPermsForm(valid_recursive_choices):
281 def RepoGroupPermsForm(valid_recursive_choices):
282 class _RepoGroupPermsForm(formencode.Schema):
282 class _RepoGroupPermsForm(formencode.Schema):
283 allow_extra_fields = True
283 allow_extra_fields = True
284 filter_extra_fields = False
284 filter_extra_fields = False
285 recursive = v.OneOf(valid_recursive_choices)
285 recursive = v.OneOf(valid_recursive_choices)
286 chained_validators = [v.ValidPerms(type_='repo_group')]
286 chained_validators = [v.ValidPerms(type_='repo_group')]
287 return _RepoGroupPermsForm
287 return _RepoGroupPermsForm
288
288
289
289
290 def UserGroupPermsForm():
290 def UserGroupPermsForm():
291 class _UserPermsForm(formencode.Schema):
291 class _UserPermsForm(formencode.Schema):
292 allow_extra_fields = True
292 allow_extra_fields = True
293 filter_extra_fields = False
293 filter_extra_fields = False
294 chained_validators = [v.ValidPerms(type_='user_group')]
294 chained_validators = [v.ValidPerms(type_='user_group')]
295 return _UserPermsForm
295 return _UserPermsForm
296
296
297
297
298 def RepoFieldForm():
298 def RepoFieldForm():
299 class _RepoFieldForm(formencode.Schema):
299 class _RepoFieldForm(formencode.Schema):
300 filter_extra_fields = True
300 filter_extra_fields = True
301 allow_extra_fields = True
301 allow_extra_fields = True
302
302
303 new_field_key = All(v.FieldKey(),
303 new_field_key = All(v.FieldKey(),
304 v.UnicodeString(strip=True, min=3, not_empty=True))
304 v.UnicodeString(strip=True, min=3, not_empty=True))
305 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
305 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
306 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
306 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
307 if_missing='str')
307 if_missing='str')
308 new_field_label = v.UnicodeString(not_empty=False)
308 new_field_label = v.UnicodeString(not_empty=False)
309 new_field_desc = v.UnicodeString(not_empty=False)
309 new_field_desc = v.UnicodeString(not_empty=False)
310
310
311 return _RepoFieldForm
311 return _RepoFieldForm
312
312
313
313
314 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
314 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
315 repo_groups=[], landing_revs=[]):
315 repo_groups=[], landing_revs=[]):
316 class _RepoForkForm(formencode.Schema):
316 class _RepoForkForm(formencode.Schema):
317 allow_extra_fields = True
317 allow_extra_fields = True
318 filter_extra_fields = False
318 filter_extra_fields = False
319 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
319 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
320 v.SlugifyName())
320 v.SlugifyName())
321 repo_group = All(v.CanWriteGroup(),
321 repo_group = All(v.CanWriteGroup(),
322 v.OneOf(repo_groups, hideList=True))
322 v.OneOf(repo_groups, hideList=True))
323 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
323 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
324 description = v.UnicodeString(strip=True, min=1, not_empty=True)
324 description = v.UnicodeString(strip=True, min=1, not_empty=True)
325 private = v.StringBoolean(if_missing=False)
325 private = v.StringBoolean(if_missing=False)
326 copy_permissions = v.StringBoolean(if_missing=False)
326 copy_permissions = v.StringBoolean(if_missing=False)
327 fork_parent_id = v.UnicodeString()
327 fork_parent_id = v.UnicodeString()
328 chained_validators = [v.ValidForkName(edit, old_data)]
328 chained_validators = [v.ValidForkName(edit, old_data)]
329 landing_rev = v.OneOf(landing_revs, hideList=True)
329 landing_rev = v.OneOf(landing_revs, hideList=True)
330
330
331 return _RepoForkForm
331 return _RepoForkForm
332
332
333
333
334 def ApplicationSettingsForm():
334 def ApplicationSettingsForm():
335 class _ApplicationSettingsForm(formencode.Schema):
335 class _ApplicationSettingsForm(formencode.Schema):
336 allow_extra_fields = True
336 allow_extra_fields = True
337 filter_extra_fields = False
337 filter_extra_fields = False
338 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
338 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
339 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
339 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
340 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
340 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
341 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
341 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
342 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
342 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
343 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
343 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
344 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
344 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
345 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
345 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
346
346
347 return _ApplicationSettingsForm
347 return _ApplicationSettingsForm
348
348
349
349
350 def ApplicationVisualisationForm():
350 def ApplicationVisualisationForm():
351 class _ApplicationVisualisationForm(formencode.Schema):
351 class _ApplicationVisualisationForm(formencode.Schema):
352 allow_extra_fields = True
352 allow_extra_fields = True
353 filter_extra_fields = False
353 filter_extra_fields = False
354 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
354 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
355 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
355 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
356 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
356 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
357
357
358 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
358 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
359 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
359 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
360 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
360 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
361 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
361 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
362 rhodecode_show_version = v.StringBoolean(if_missing=False)
362 rhodecode_show_version = v.StringBoolean(if_missing=False)
363 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
363 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
364 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
364 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
365 rhodecode_gravatar_url = v.UnicodeString(min=3)
365 rhodecode_gravatar_url = v.UnicodeString(min=3)
366 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
366 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
367 rhodecode_support_url = v.UnicodeString()
367 rhodecode_support_url = v.UnicodeString()
368 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
368 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
369 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
369 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
370
370
371 return _ApplicationVisualisationForm
371 return _ApplicationVisualisationForm
372
372
373
373
374 class _BaseVcsSettingsForm(formencode.Schema):
374 class _BaseVcsSettingsForm(formencode.Schema):
375 allow_extra_fields = True
375 allow_extra_fields = True
376 filter_extra_fields = False
376 filter_extra_fields = False
377 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
377 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
378 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
378 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
379 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
379 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
380
380
381 # PR/Code-review
381 # PR/Code-review
382 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
382 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
383 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
383 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
384
384
385 # hg
385 # hg
386 extensions_largefiles = v.StringBoolean(if_missing=False)
386 extensions_largefiles = v.StringBoolean(if_missing=False)
387 extensions_evolve = v.StringBoolean(if_missing=False)
387 extensions_evolve = v.StringBoolean(if_missing=False)
388 phases_publish = v.StringBoolean(if_missing=False)
388 phases_publish = v.StringBoolean(if_missing=False)
389 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
389 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
390
390
391 # git
391 # git
392 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
392 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
393
393
394 # svn
394 # svn
395 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
395 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
396 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
396 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
397
397
398
398
399 def ApplicationUiSettingsForm():
399 def ApplicationUiSettingsForm():
400 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
400 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
401 web_push_ssl = v.StringBoolean(if_missing=False)
401 web_push_ssl = v.StringBoolean(if_missing=False)
402 paths_root_path = All(
402 paths_root_path = All(
403 v.ValidPath(),
403 v.ValidPath(),
404 v.UnicodeString(strip=True, min=1, not_empty=True)
404 v.UnicodeString(strip=True, min=1, not_empty=True)
405 )
405 )
406 largefiles_usercache = All(
406 largefiles_usercache = All(
407 v.ValidPath(),
407 v.ValidPath(),
408 v.UnicodeString(strip=True, min=2, not_empty=True))
408 v.UnicodeString(strip=True, min=2, not_empty=True))
409 vcs_git_lfs_store_location = All(
409 vcs_git_lfs_store_location = All(
410 v.ValidPath(),
410 v.ValidPath(),
411 v.UnicodeString(strip=True, min=2, not_empty=True))
411 v.UnicodeString(strip=True, min=2, not_empty=True))
412 extensions_hgsubversion = v.StringBoolean(if_missing=False)
412 extensions_hgsubversion = v.StringBoolean(if_missing=False)
413 extensions_hggit = v.StringBoolean(if_missing=False)
413 extensions_hggit = v.StringBoolean(if_missing=False)
414 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
414 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
415 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
415 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
416
416
417 return _ApplicationUiSettingsForm
417 return _ApplicationUiSettingsForm
418
418
419
419
420 def RepoVcsSettingsForm(repo_name):
420 def RepoVcsSettingsForm(repo_name):
421 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
421 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
422 inherit_global_settings = v.StringBoolean(if_missing=False)
422 inherit_global_settings = v.StringBoolean(if_missing=False)
423 new_svn_branch = v.ValidSvnPattern(
423 new_svn_branch = v.ValidSvnPattern(
424 section='vcs_svn_branch', repo_name=repo_name)
424 section='vcs_svn_branch', repo_name=repo_name)
425 new_svn_tag = v.ValidSvnPattern(
425 new_svn_tag = v.ValidSvnPattern(
426 section='vcs_svn_tag', repo_name=repo_name)
426 section='vcs_svn_tag', repo_name=repo_name)
427
427
428 return _RepoVcsSettingsForm
428 return _RepoVcsSettingsForm
429
429
430
430
431 def LabsSettingsForm():
431 def LabsSettingsForm():
432 class _LabSettingsForm(formencode.Schema):
432 class _LabSettingsForm(formencode.Schema):
433 allow_extra_fields = True
433 allow_extra_fields = True
434 filter_extra_fields = False
434 filter_extra_fields = False
435
435
436 return _LabSettingsForm
436 return _LabSettingsForm
437
437
438
438
439 def ApplicationPermissionsForm(
439 def ApplicationPermissionsForm(
440 register_choices, password_reset_choices, extern_activate_choices):
440 register_choices, password_reset_choices, extern_activate_choices):
441 class _DefaultPermissionsForm(formencode.Schema):
441 class _DefaultPermissionsForm(formencode.Schema):
442 allow_extra_fields = True
442 allow_extra_fields = True
443 filter_extra_fields = True
443 filter_extra_fields = True
444
444
445 anonymous = v.StringBoolean(if_missing=False)
445 anonymous = v.StringBoolean(if_missing=False)
446 default_register = v.OneOf(register_choices)
446 default_register = v.OneOf(register_choices)
447 default_register_message = v.UnicodeString()
447 default_register_message = v.UnicodeString()
448 default_password_reset = v.OneOf(password_reset_choices)
448 default_password_reset = v.OneOf(password_reset_choices)
449 default_extern_activate = v.OneOf(extern_activate_choices)
449 default_extern_activate = v.OneOf(extern_activate_choices)
450
450
451 return _DefaultPermissionsForm
451 return _DefaultPermissionsForm
452
452
453
453
454 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
454 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
455 user_group_perms_choices):
455 user_group_perms_choices):
456 class _ObjectPermissionsForm(formencode.Schema):
456 class _ObjectPermissionsForm(formencode.Schema):
457 allow_extra_fields = True
457 allow_extra_fields = True
458 filter_extra_fields = True
458 filter_extra_fields = True
459 overwrite_default_repo = v.StringBoolean(if_missing=False)
459 overwrite_default_repo = v.StringBoolean(if_missing=False)
460 overwrite_default_group = v.StringBoolean(if_missing=False)
460 overwrite_default_group = v.StringBoolean(if_missing=False)
461 overwrite_default_user_group = v.StringBoolean(if_missing=False)
461 overwrite_default_user_group = v.StringBoolean(if_missing=False)
462 default_repo_perm = v.OneOf(repo_perms_choices)
462 default_repo_perm = v.OneOf(repo_perms_choices)
463 default_group_perm = v.OneOf(group_perms_choices)
463 default_group_perm = v.OneOf(group_perms_choices)
464 default_user_group_perm = v.OneOf(user_group_perms_choices)
464 default_user_group_perm = v.OneOf(user_group_perms_choices)
465
465
466 return _ObjectPermissionsForm
466 return _ObjectPermissionsForm
467
467
468
468
469 def UserPermissionsForm(create_choices, create_on_write_choices,
469 def UserPermissionsForm(create_choices, create_on_write_choices,
470 repo_group_create_choices, user_group_create_choices,
470 repo_group_create_choices, user_group_create_choices,
471 fork_choices, inherit_default_permissions_choices):
471 fork_choices, inherit_default_permissions_choices):
472 class _DefaultPermissionsForm(formencode.Schema):
472 class _DefaultPermissionsForm(formencode.Schema):
473 allow_extra_fields = True
473 allow_extra_fields = True
474 filter_extra_fields = True
474 filter_extra_fields = True
475
475
476 anonymous = v.StringBoolean(if_missing=False)
476 anonymous = v.StringBoolean(if_missing=False)
477
477
478 default_repo_create = v.OneOf(create_choices)
478 default_repo_create = v.OneOf(create_choices)
479 default_repo_create_on_write = v.OneOf(create_on_write_choices)
479 default_repo_create_on_write = v.OneOf(create_on_write_choices)
480 default_user_group_create = v.OneOf(user_group_create_choices)
480 default_user_group_create = v.OneOf(user_group_create_choices)
481 default_repo_group_create = v.OneOf(repo_group_create_choices)
481 default_repo_group_create = v.OneOf(repo_group_create_choices)
482 default_fork_create = v.OneOf(fork_choices)
482 default_fork_create = v.OneOf(fork_choices)
483 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
483 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
484
484
485 return _DefaultPermissionsForm
485 return _DefaultPermissionsForm
486
486
487
487
488 def UserIndividualPermissionsForm():
488 def UserIndividualPermissionsForm():
489 class _DefaultPermissionsForm(formencode.Schema):
489 class _DefaultPermissionsForm(formencode.Schema):
490 allow_extra_fields = True
490 allow_extra_fields = True
491 filter_extra_fields = True
491 filter_extra_fields = True
492
492
493 inherit_default_permissions = v.StringBoolean(if_missing=False)
493 inherit_default_permissions = v.StringBoolean(if_missing=False)
494
494
495 return _DefaultPermissionsForm
495 return _DefaultPermissionsForm
496
496
497
497
498 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
498 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
499 class _DefaultsForm(formencode.Schema):
499 class _DefaultsForm(formencode.Schema):
500 allow_extra_fields = True
500 allow_extra_fields = True
501 filter_extra_fields = True
501 filter_extra_fields = True
502 default_repo_type = v.OneOf(supported_backends)
502 default_repo_type = v.OneOf(supported_backends)
503 default_repo_private = v.StringBoolean(if_missing=False)
503 default_repo_private = v.StringBoolean(if_missing=False)
504 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
504 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
505 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
505 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
506 default_repo_enable_locking = v.StringBoolean(if_missing=False)
506 default_repo_enable_locking = v.StringBoolean(if_missing=False)
507
507
508 return _DefaultsForm
508 return _DefaultsForm
509
509
510
510
511 def AuthSettingsForm():
511 def AuthSettingsForm():
512 class _AuthSettingsForm(formencode.Schema):
512 class _AuthSettingsForm(formencode.Schema):
513 allow_extra_fields = True
513 allow_extra_fields = True
514 filter_extra_fields = True
514 filter_extra_fields = True
515 auth_plugins = All(v.ValidAuthPlugins(),
515 auth_plugins = All(v.ValidAuthPlugins(),
516 v.UniqueListFromString()(not_empty=True))
516 v.UniqueListFromString()(not_empty=True))
517
517
518 return _AuthSettingsForm
518 return _AuthSettingsForm
519
519
520
520
521 def UserExtraEmailForm():
521 def UserExtraEmailForm():
522 class _UserExtraEmailForm(formencode.Schema):
522 class _UserExtraEmailForm(formencode.Schema):
523 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
523 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
524 return _UserExtraEmailForm
524 return _UserExtraEmailForm
525
525
526
526
527 def UserExtraIpForm():
527 def UserExtraIpForm():
528 class _UserExtraIpForm(formencode.Schema):
528 class _UserExtraIpForm(formencode.Schema):
529 ip = v.ValidIp()(not_empty=True)
529 ip = v.ValidIp()(not_empty=True)
530 return _UserExtraIpForm
530 return _UserExtraIpForm
531
531
532
532
533
533
534 def PullRequestForm(repo_id):
534 def PullRequestForm(repo_id):
535 class ReviewerForm(formencode.Schema):
535 class ReviewerForm(formencode.Schema):
536 user_id = v.Int(not_empty=True)
536 user_id = v.Int(not_empty=True)
537 reasons = All()
537 reasons = All()
538 mandatory = v.StringBoolean()
538
539
539 class _PullRequestForm(formencode.Schema):
540 class _PullRequestForm(formencode.Schema):
540 allow_extra_fields = True
541 allow_extra_fields = True
541 filter_extra_fields = True
542 filter_extra_fields = True
542
543
543 user = v.UnicodeString(strip=True, required=True)
544 common_ancestor = v.UnicodeString(strip=True, required=True)
544 source_repo = v.UnicodeString(strip=True, required=True)
545 source_repo = v.UnicodeString(strip=True, required=True)
545 source_ref = v.UnicodeString(strip=True, required=True)
546 source_ref = v.UnicodeString(strip=True, required=True)
546 target_repo = v.UnicodeString(strip=True, required=True)
547 target_repo = v.UnicodeString(strip=True, required=True)
547 target_ref = v.UnicodeString(strip=True, required=True)
548 target_ref = v.UnicodeString(strip=True, required=True)
548 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 revisions = All(#v.NotReviewedRevisions(repo_id)(),
549 v.UniqueList()(not_empty=True))
550 v.UniqueList()(not_empty=True))
550 review_members = formencode.ForEach(ReviewerForm())
551 review_members = formencode.ForEach(ReviewerForm())
551 pullrequest_title = v.UnicodeString(strip=True, required=True)
552 pullrequest_title = v.UnicodeString(strip=True, required=True)
552 pullrequest_desc = v.UnicodeString(strip=True, required=False)
553 pullrequest_desc = v.UnicodeString(strip=True, required=False)
553
554
554 return _PullRequestForm
555 return _PullRequestForm
555
556
556
557
557 def IssueTrackerPatternsForm():
558 def IssueTrackerPatternsForm():
558 class _IssueTrackerPatternsForm(formencode.Schema):
559 class _IssueTrackerPatternsForm(formencode.Schema):
559 allow_extra_fields = True
560 allow_extra_fields = True
560 filter_extra_fields = False
561 filter_extra_fields = False
561 chained_validators = [v.ValidPattern()]
562 chained_validators = [v.ValidPattern()]
562 return _IssueTrackerPatternsForm
563 return _IssueTrackerPatternsForm
@@ -1,1471 +1,1499 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 from sqlalchemy import or_
35
35
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 PullRequestVersion, ChangesetComment, Repository)
53 PullRequestVersion, ChangesetComment, Repository)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
56 EmailNotificationModel
56 EmailNotificationModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
65 # request update.
65 # request update.
66 UpdateResponse = namedtuple('UpdateResponse', [
66 UpdateResponse = namedtuple('UpdateResponse', [
67 'executed', 'reason', 'new', 'old', 'changes',
67 'executed', 'reason', 'new', 'old', 'changes',
68 'source_changed', 'target_changed'])
68 'source_changed', 'target_changed'])
69
69
70
70
71 class PullRequestModel(BaseModel):
71 class PullRequestModel(BaseModel):
72
72
73 cls = PullRequest
73 cls = PullRequest
74
74
75 DIFF_CONTEXT = 3
75 DIFF_CONTEXT = 3
76
76
77 MERGE_STATUS_MESSAGES = {
77 MERGE_STATUS_MESSAGES = {
78 MergeFailureReason.NONE: lazy_ugettext(
78 MergeFailureReason.NONE: lazy_ugettext(
79 'This pull request can be automatically merged.'),
79 'This pull request can be automatically merged.'),
80 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 'This pull request cannot be merged because of an unhandled'
81 'This pull request cannot be merged because of an unhandled'
82 ' exception.'),
82 ' exception.'),
83 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 'This pull request cannot be merged because of merge conflicts.'),
84 'This pull request cannot be merged because of merge conflicts.'),
85 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 'This pull request could not be merged because push to target'
86 'This pull request could not be merged because push to target'
87 ' failed.'),
87 ' failed.'),
88 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 'This pull request cannot be merged because the target is not a'
89 'This pull request cannot be merged because the target is not a'
90 ' head.'),
90 ' head.'),
91 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 'This pull request cannot be merged because the source contains'
92 'This pull request cannot be merged because the source contains'
93 ' more branches than the target.'),
93 ' more branches than the target.'),
94 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 'This pull request cannot be merged because the target has'
95 'This pull request cannot be merged because the target has'
96 ' multiple heads.'),
96 ' multiple heads.'),
97 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 'This pull request cannot be merged because the target repository'
98 'This pull request cannot be merged because the target repository'
99 ' is locked.'),
99 ' is locked.'),
100 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 'This pull request cannot be merged because the target or the '
101 'This pull request cannot be merged because the target or the '
102 'source reference is missing.'),
102 'source reference is missing.'),
103 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 'This pull request cannot be merged because the target '
104 'This pull request cannot be merged because the target '
105 'reference is missing.'),
105 'reference is missing.'),
106 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 'This pull request cannot be merged because the source '
107 'This pull request cannot be merged because the source '
108 'reference is missing.'),
108 'reference is missing.'),
109 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 'This pull request cannot be merged because of conflicts related '
110 'This pull request cannot be merged because of conflicts related '
111 'to sub repositories.'),
111 'to sub repositories.'),
112 }
112 }
113
113
114 UPDATE_STATUS_MESSAGES = {
114 UPDATE_STATUS_MESSAGES = {
115 UpdateFailureReason.NONE: lazy_ugettext(
115 UpdateFailureReason.NONE: lazy_ugettext(
116 'Pull request update successful.'),
116 'Pull request update successful.'),
117 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 'Pull request update failed because of an unknown error.'),
118 'Pull request update failed because of an unknown error.'),
119 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 'No update needed because the source and target have not changed.'),
120 'No update needed because the source and target have not changed.'),
121 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 'Pull request cannot be updated because the reference type is '
122 'Pull request cannot be updated because the reference type is '
123 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
123 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 'This pull request cannot be updated because the target '
125 'This pull request cannot be updated because the target '
126 'reference is missing.'),
126 'reference is missing.'),
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 'This pull request cannot be updated because the source '
128 'This pull request cannot be updated because the source '
129 'reference is missing.'),
129 'reference is missing.'),
130 }
130 }
131
131
132 def __get_pull_request(self, pull_request):
132 def __get_pull_request(self, pull_request):
133 return self._get_instance((
133 return self._get_instance((
134 PullRequest, PullRequestVersion), pull_request)
134 PullRequest, PullRequestVersion), pull_request)
135
135
136 def _check_perms(self, perms, pull_request, user, api=False):
136 def _check_perms(self, perms, pull_request, user, api=False):
137 if not api:
137 if not api:
138 return h.HasRepoPermissionAny(*perms)(
138 return h.HasRepoPermissionAny(*perms)(
139 user=user, repo_name=pull_request.target_repo.repo_name)
139 user=user, repo_name=pull_request.target_repo.repo_name)
140 else:
140 else:
141 return h.HasRepoPermissionAnyApi(*perms)(
141 return h.HasRepoPermissionAnyApi(*perms)(
142 user=user, repo_name=pull_request.target_repo.repo_name)
142 user=user, repo_name=pull_request.target_repo.repo_name)
143
143
144 def check_user_read(self, pull_request, user, api=False):
144 def check_user_read(self, pull_request, user, api=False):
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
147
147
148 def check_user_merge(self, pull_request, user, api=False):
148 def check_user_merge(self, pull_request, user, api=False):
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 return self._check_perms(_perms, pull_request, user, api)
150 return self._check_perms(_perms, pull_request, user, api)
151
151
152 def check_user_update(self, pull_request, user, api=False):
152 def check_user_update(self, pull_request, user, api=False):
153 owner = user.user_id == pull_request.user_id
153 owner = user.user_id == pull_request.user_id
154 return self.check_user_merge(pull_request, user, api) or owner
154 return self.check_user_merge(pull_request, user, api) or owner
155
155
156 def check_user_delete(self, pull_request, user):
156 def check_user_delete(self, pull_request, user):
157 owner = user.user_id == pull_request.user_id
157 owner = user.user_id == pull_request.user_id
158 _perms = ('repository.admin',)
158 _perms = ('repository.admin',)
159 return self._check_perms(_perms, pull_request, user) or owner
159 return self._check_perms(_perms, pull_request, user) or owner
160
160
161 def check_user_change_status(self, pull_request, user, api=False):
161 def check_user_change_status(self, pull_request, user, api=False):
162 reviewer = user.user_id in [x.user_id for x in
162 reviewer = user.user_id in [x.user_id for x in
163 pull_request.reviewers]
163 pull_request.reviewers]
164 return self.check_user_update(pull_request, user, api) or reviewer
164 return self.check_user_update(pull_request, user, api) or reviewer
165
165
166 def get(self, pull_request):
166 def get(self, pull_request):
167 return self.__get_pull_request(pull_request)
167 return self.__get_pull_request(pull_request)
168
168
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 opened_by=None, order_by=None,
170 opened_by=None, order_by=None,
171 order_dir='desc'):
171 order_dir='desc'):
172 repo = None
172 repo = None
173 if repo_name:
173 if repo_name:
174 repo = self._get_repo(repo_name)
174 repo = self._get_repo(repo_name)
175
175
176 q = PullRequest.query()
176 q = PullRequest.query()
177
177
178 # source or target
178 # source or target
179 if repo and source:
179 if repo and source:
180 q = q.filter(PullRequest.source_repo == repo)
180 q = q.filter(PullRequest.source_repo == repo)
181 elif repo:
181 elif repo:
182 q = q.filter(PullRequest.target_repo == repo)
182 q = q.filter(PullRequest.target_repo == repo)
183
183
184 # closed,opened
184 # closed,opened
185 if statuses:
185 if statuses:
186 q = q.filter(PullRequest.status.in_(statuses))
186 q = q.filter(PullRequest.status.in_(statuses))
187
187
188 # opened by filter
188 # opened by filter
189 if opened_by:
189 if opened_by:
190 q = q.filter(PullRequest.user_id.in_(opened_by))
190 q = q.filter(PullRequest.user_id.in_(opened_by))
191
191
192 if order_by:
192 if order_by:
193 order_map = {
193 order_map = {
194 'name_raw': PullRequest.pull_request_id,
194 'name_raw': PullRequest.pull_request_id,
195 'title': PullRequest.title,
195 'title': PullRequest.title,
196 'updated_on_raw': PullRequest.updated_on,
196 'updated_on_raw': PullRequest.updated_on,
197 'target_repo': PullRequest.target_repo_id
197 'target_repo': PullRequest.target_repo_id
198 }
198 }
199 if order_dir == 'asc':
199 if order_dir == 'asc':
200 q = q.order_by(order_map[order_by].asc())
200 q = q.order_by(order_map[order_by].asc())
201 else:
201 else:
202 q = q.order_by(order_map[order_by].desc())
202 q = q.order_by(order_map[order_by].desc())
203
203
204 return q
204 return q
205
205
206 def count_all(self, repo_name, source=False, statuses=None,
206 def count_all(self, repo_name, source=False, statuses=None,
207 opened_by=None):
207 opened_by=None):
208 """
208 """
209 Count the number of pull requests for a specific repository.
209 Count the number of pull requests for a specific repository.
210
210
211 :param repo_name: target or source repo
211 :param repo_name: target or source repo
212 :param source: boolean flag to specify if repo_name refers to source
212 :param source: boolean flag to specify if repo_name refers to source
213 :param statuses: list of pull request statuses
213 :param statuses: list of pull request statuses
214 :param opened_by: author user of the pull request
214 :param opened_by: author user of the pull request
215 :returns: int number of pull requests
215 :returns: int number of pull requests
216 """
216 """
217 q = self._prepare_get_all_query(
217 q = self._prepare_get_all_query(
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219
219
220 return q.count()
220 return q.count()
221
221
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 offset=0, length=None, order_by=None, order_dir='desc'):
223 offset=0, length=None, order_by=None, order_dir='desc'):
224 """
224 """
225 Get all pull requests for a specific repository.
225 Get all pull requests for a specific repository.
226
226
227 :param repo_name: target or source repo
227 :param repo_name: target or source repo
228 :param source: boolean flag to specify if repo_name refers to source
228 :param source: boolean flag to specify if repo_name refers to source
229 :param statuses: list of pull request statuses
229 :param statuses: list of pull request statuses
230 :param opened_by: author user of the pull request
230 :param opened_by: author user of the pull request
231 :param offset: pagination offset
231 :param offset: pagination offset
232 :param length: length of returned list
232 :param length: length of returned list
233 :param order_by: order of the returned list
233 :param order_by: order of the returned list
234 :param order_dir: 'asc' or 'desc' ordering direction
234 :param order_dir: 'asc' or 'desc' ordering direction
235 :returns: list of pull requests
235 :returns: list of pull requests
236 """
236 """
237 q = self._prepare_get_all_query(
237 q = self._prepare_get_all_query(
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 order_by=order_by, order_dir=order_dir)
239 order_by=order_by, order_dir=order_dir)
240
240
241 if length:
241 if length:
242 pull_requests = q.limit(length).offset(offset).all()
242 pull_requests = q.limit(length).offset(offset).all()
243 else:
243 else:
244 pull_requests = q.all()
244 pull_requests = q.all()
245
245
246 return pull_requests
246 return pull_requests
247
247
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 opened_by=None):
249 opened_by=None):
250 """
250 """
251 Count the number of pull requests for a specific repository that are
251 Count the number of pull requests for a specific repository that are
252 awaiting review.
252 awaiting review.
253
253
254 :param repo_name: target or source repo
254 :param repo_name: target or source repo
255 :param source: boolean flag to specify if repo_name refers to source
255 :param source: boolean flag to specify if repo_name refers to source
256 :param statuses: list of pull request statuses
256 :param statuses: list of pull request statuses
257 :param opened_by: author user of the pull request
257 :param opened_by: author user of the pull request
258 :returns: int number of pull requests
258 :returns: int number of pull requests
259 """
259 """
260 pull_requests = self.get_awaiting_review(
260 pull_requests = self.get_awaiting_review(
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262
262
263 return len(pull_requests)
263 return len(pull_requests)
264
264
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 opened_by=None, offset=0, length=None,
266 opened_by=None, offset=0, length=None,
267 order_by=None, order_dir='desc'):
267 order_by=None, order_dir='desc'):
268 """
268 """
269 Get all pull requests for a specific repository that are awaiting
269 Get all pull requests for a specific repository that are awaiting
270 review.
270 review.
271
271
272 :param repo_name: target or source repo
272 :param repo_name: target or source repo
273 :param source: boolean flag to specify if repo_name refers to source
273 :param source: boolean flag to specify if repo_name refers to source
274 :param statuses: list of pull request statuses
274 :param statuses: list of pull request statuses
275 :param opened_by: author user of the pull request
275 :param opened_by: author user of the pull request
276 :param offset: pagination offset
276 :param offset: pagination offset
277 :param length: length of returned list
277 :param length: length of returned list
278 :param order_by: order of the returned list
278 :param order_by: order of the returned list
279 :param order_dir: 'asc' or 'desc' ordering direction
279 :param order_dir: 'asc' or 'desc' ordering direction
280 :returns: list of pull requests
280 :returns: list of pull requests
281 """
281 """
282 pull_requests = self.get_all(
282 pull_requests = self.get_all(
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 order_by=order_by, order_dir=order_dir)
284 order_by=order_by, order_dir=order_dir)
285
285
286 _filtered_pull_requests = []
286 _filtered_pull_requests = []
287 for pr in pull_requests:
287 for pr in pull_requests:
288 status = pr.calculated_review_status()
288 status = pr.calculated_review_status()
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 _filtered_pull_requests.append(pr)
291 _filtered_pull_requests.append(pr)
292 if length:
292 if length:
293 return _filtered_pull_requests[offset:offset+length]
293 return _filtered_pull_requests[offset:offset+length]
294 else:
294 else:
295 return _filtered_pull_requests
295 return _filtered_pull_requests
296
296
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 opened_by=None, user_id=None):
298 opened_by=None, user_id=None):
299 """
299 """
300 Count the number of pull requests for a specific repository that are
300 Count the number of pull requests for a specific repository that are
301 awaiting review from a specific user.
301 awaiting review from a specific user.
302
302
303 :param repo_name: target or source repo
303 :param repo_name: target or source repo
304 :param source: boolean flag to specify if repo_name refers to source
304 :param source: boolean flag to specify if repo_name refers to source
305 :param statuses: list of pull request statuses
305 :param statuses: list of pull request statuses
306 :param opened_by: author user of the pull request
306 :param opened_by: author user of the pull request
307 :param user_id: reviewer user of the pull request
307 :param user_id: reviewer user of the pull request
308 :returns: int number of pull requests
308 :returns: int number of pull requests
309 """
309 """
310 pull_requests = self.get_awaiting_my_review(
310 pull_requests = self.get_awaiting_my_review(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 user_id=user_id)
312 user_id=user_id)
313
313
314 return len(pull_requests)
314 return len(pull_requests)
315
315
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 opened_by=None, user_id=None, offset=0,
317 opened_by=None, user_id=None, offset=0,
318 length=None, order_by=None, order_dir='desc'):
318 length=None, order_by=None, order_dir='desc'):
319 """
319 """
320 Get all pull requests for a specific repository that are awaiting
320 Get all pull requests for a specific repository that are awaiting
321 review from a specific user.
321 review from a specific user.
322
322
323 :param repo_name: target or source repo
323 :param repo_name: target or source repo
324 :param source: boolean flag to specify if repo_name refers to source
324 :param source: boolean flag to specify if repo_name refers to source
325 :param statuses: list of pull request statuses
325 :param statuses: list of pull request statuses
326 :param opened_by: author user of the pull request
326 :param opened_by: author user of the pull request
327 :param user_id: reviewer user of the pull request
327 :param user_id: reviewer user of the pull request
328 :param offset: pagination offset
328 :param offset: pagination offset
329 :param length: length of returned list
329 :param length: length of returned list
330 :param order_by: order of the returned list
330 :param order_by: order of the returned list
331 :param order_dir: 'asc' or 'desc' ordering direction
331 :param order_dir: 'asc' or 'desc' ordering direction
332 :returns: list of pull requests
332 :returns: list of pull requests
333 """
333 """
334 pull_requests = self.get_all(
334 pull_requests = self.get_all(
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 order_by=order_by, order_dir=order_dir)
336 order_by=order_by, order_dir=order_dir)
337
337
338 _my = PullRequestModel().get_not_reviewed(user_id)
338 _my = PullRequestModel().get_not_reviewed(user_id)
339 my_participation = []
339 my_participation = []
340 for pr in pull_requests:
340 for pr in pull_requests:
341 if pr in _my:
341 if pr in _my:
342 my_participation.append(pr)
342 my_participation.append(pr)
343 _filtered_pull_requests = my_participation
343 _filtered_pull_requests = my_participation
344 if length:
344 if length:
345 return _filtered_pull_requests[offset:offset+length]
345 return _filtered_pull_requests[offset:offset+length]
346 else:
346 else:
347 return _filtered_pull_requests
347 return _filtered_pull_requests
348
348
349 def get_not_reviewed(self, user_id):
349 def get_not_reviewed(self, user_id):
350 return [
350 return [
351 x.pull_request for x in PullRequestReviewers.query().filter(
351 x.pull_request for x in PullRequestReviewers.query().filter(
352 PullRequestReviewers.user_id == user_id).all()
352 PullRequestReviewers.user_id == user_id).all()
353 ]
353 ]
354
354
355 def _prepare_participating_query(self, user_id=None, statuses=None,
355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 order_by=None, order_dir='desc'):
356 order_by=None, order_dir='desc'):
357 q = PullRequest.query()
357 q = PullRequest.query()
358 if user_id:
358 if user_id:
359 reviewers_subquery = Session().query(
359 reviewers_subquery = Session().query(
360 PullRequestReviewers.pull_request_id).filter(
360 PullRequestReviewers.pull_request_id).filter(
361 PullRequestReviewers.user_id == user_id).subquery()
361 PullRequestReviewers.user_id == user_id).subquery()
362 user_filter= or_(
362 user_filter= or_(
363 PullRequest.user_id == user_id,
363 PullRequest.user_id == user_id,
364 PullRequest.pull_request_id.in_(reviewers_subquery)
364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 )
365 )
366 q = PullRequest.query().filter(user_filter)
366 q = PullRequest.query().filter(user_filter)
367
367
368 # closed,opened
368 # closed,opened
369 if statuses:
369 if statuses:
370 q = q.filter(PullRequest.status.in_(statuses))
370 q = q.filter(PullRequest.status.in_(statuses))
371
371
372 if order_by:
372 if order_by:
373 order_map = {
373 order_map = {
374 'name_raw': PullRequest.pull_request_id,
374 'name_raw': PullRequest.pull_request_id,
375 'title': PullRequest.title,
375 'title': PullRequest.title,
376 'updated_on_raw': PullRequest.updated_on,
376 'updated_on_raw': PullRequest.updated_on,
377 'target_repo': PullRequest.target_repo_id
377 'target_repo': PullRequest.target_repo_id
378 }
378 }
379 if order_dir == 'asc':
379 if order_dir == 'asc':
380 q = q.order_by(order_map[order_by].asc())
380 q = q.order_by(order_map[order_by].asc())
381 else:
381 else:
382 q = q.order_by(order_map[order_by].desc())
382 q = q.order_by(order_map[order_by].desc())
383
383
384 return q
384 return q
385
385
386 def count_im_participating_in(self, user_id=None, statuses=None):
386 def count_im_participating_in(self, user_id=None, statuses=None):
387 q = self._prepare_participating_query(user_id, statuses=statuses)
387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 return q.count()
388 return q.count()
389
389
390 def get_im_participating_in(
390 def get_im_participating_in(
391 self, user_id=None, statuses=None, offset=0,
391 self, user_id=None, statuses=None, offset=0,
392 length=None, order_by=None, order_dir='desc'):
392 length=None, order_by=None, order_dir='desc'):
393 """
393 """
394 Get all Pull requests that i'm participating in, or i have opened
394 Get all Pull requests that i'm participating in, or i have opened
395 """
395 """
396
396
397 q = self._prepare_participating_query(
397 q = self._prepare_participating_query(
398 user_id, statuses=statuses, order_by=order_by,
398 user_id, statuses=statuses, order_by=order_by,
399 order_dir=order_dir)
399 order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def get_versions(self, pull_request):
408 def get_versions(self, pull_request):
409 """
409 """
410 returns version of pull request sorted by ID descending
410 returns version of pull request sorted by ID descending
411 """
411 """
412 return PullRequestVersion.query()\
412 return PullRequestVersion.query()\
413 .filter(PullRequestVersion.pull_request == pull_request)\
413 .filter(PullRequestVersion.pull_request == pull_request)\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 .all()
415 .all()
416
416
417 def create(self, created_by, source_repo, source_ref, target_repo,
417 def create(self, created_by, source_repo, source_ref, target_repo,
418 target_ref, revisions, reviewers, title, description=None):
418 target_ref, revisions, reviewers, title, description=None,
419 reviewer_data=None):
420
419 created_by_user = self._get_user(created_by)
421 created_by_user = self._get_user(created_by)
420 source_repo = self._get_repo(source_repo)
422 source_repo = self._get_repo(source_repo)
421 target_repo = self._get_repo(target_repo)
423 target_repo = self._get_repo(target_repo)
422
424
423 pull_request = PullRequest()
425 pull_request = PullRequest()
424 pull_request.source_repo = source_repo
426 pull_request.source_repo = source_repo
425 pull_request.source_ref = source_ref
427 pull_request.source_ref = source_ref
426 pull_request.target_repo = target_repo
428 pull_request.target_repo = target_repo
427 pull_request.target_ref = target_ref
429 pull_request.target_ref = target_ref
428 pull_request.revisions = revisions
430 pull_request.revisions = revisions
429 pull_request.title = title
431 pull_request.title = title
430 pull_request.description = description
432 pull_request.description = description
431 pull_request.author = created_by_user
433 pull_request.author = created_by_user
434 pull_request.reviewer_data = reviewer_data
432
435
433 Session().add(pull_request)
436 Session().add(pull_request)
434 Session().flush()
437 Session().flush()
435
438
436 reviewer_ids = set()
439 reviewer_ids = set()
437 # members / reviewers
440 # members / reviewers
438 for reviewer_object in reviewers:
441 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
442 user_id, reasons, mandatory = reviewer_object
440 user_id, reasons = reviewer_object
441 else:
442 user_id, reasons = reviewer_object, []
443
443
444 user = self._get_user(user_id)
444 user = self._get_user(user_id)
445 reviewer_ids.add(user.user_id)
445 reviewer_ids.add(user.user_id)
446
446
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
447 reviewer = PullRequestReviewers()
448 reviewer.user = user
449 reviewer.pull_request = pull_request
450 reviewer.reasons = reasons
451 reviewer.mandatory = mandatory
448 Session().add(reviewer)
452 Session().add(reviewer)
449
453
450 # Set approval status to "Under Review" for all commits which are
454 # Set approval status to "Under Review" for all commits which are
451 # part of this pull request.
455 # part of this pull request.
452 ChangesetStatusModel().set_status(
456 ChangesetStatusModel().set_status(
453 repo=target_repo,
457 repo=target_repo,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
458 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 user=created_by_user,
459 user=created_by_user,
456 pull_request=pull_request
460 pull_request=pull_request
457 )
461 )
458
462
459 self.notify_reviewers(pull_request, reviewer_ids)
463 self.notify_reviewers(pull_request, reviewer_ids)
460 self._trigger_pull_request_hook(
464 self._trigger_pull_request_hook(
461 pull_request, created_by_user, 'create')
465 pull_request, created_by_user, 'create')
462
466
463 return pull_request
467 return pull_request
464
468
465 def _trigger_pull_request_hook(self, pull_request, user, action):
469 def _trigger_pull_request_hook(self, pull_request, user, action):
466 pull_request = self.__get_pull_request(pull_request)
470 pull_request = self.__get_pull_request(pull_request)
467 target_scm = pull_request.target_repo.scm_instance()
471 target_scm = pull_request.target_repo.scm_instance()
468 if action == 'create':
472 if action == 'create':
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 elif action == 'merge':
474 elif action == 'merge':
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 elif action == 'close':
476 elif action == 'close':
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 elif action == 'review_status_change':
478 elif action == 'review_status_change':
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
479 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 elif action == 'update':
480 elif action == 'update':
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
481 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 else:
482 else:
479 return
483 return
480
484
481 trigger_hook(
485 trigger_hook(
482 username=user.username,
486 username=user.username,
483 repo_name=pull_request.target_repo.repo_name,
487 repo_name=pull_request.target_repo.repo_name,
484 repo_alias=target_scm.alias,
488 repo_alias=target_scm.alias,
485 pull_request=pull_request)
489 pull_request=pull_request)
486
490
487 def _get_commit_ids(self, pull_request):
491 def _get_commit_ids(self, pull_request):
488 """
492 """
489 Return the commit ids of the merged pull request.
493 Return the commit ids of the merged pull request.
490
494
491 This method is not dealing correctly yet with the lack of autoupdates
495 This method is not dealing correctly yet with the lack of autoupdates
492 nor with the implicit target updates.
496 nor with the implicit target updates.
493 For example: if a commit in the source repo is already in the target it
497 For example: if a commit in the source repo is already in the target it
494 will be reported anyways.
498 will be reported anyways.
495 """
499 """
496 merge_rev = pull_request.merge_rev
500 merge_rev = pull_request.merge_rev
497 if merge_rev is None:
501 if merge_rev is None:
498 raise ValueError('This pull request was not merged yet')
502 raise ValueError('This pull request was not merged yet')
499
503
500 commit_ids = list(pull_request.revisions)
504 commit_ids = list(pull_request.revisions)
501 if merge_rev not in commit_ids:
505 if merge_rev not in commit_ids:
502 commit_ids.append(merge_rev)
506 commit_ids.append(merge_rev)
503
507
504 return commit_ids
508 return commit_ids
505
509
506 def merge(self, pull_request, user, extras):
510 def merge(self, pull_request, user, extras):
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
511 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
512 merge_state = self._merge_pull_request(pull_request, user, extras)
509 if merge_state.executed:
513 if merge_state.executed:
510 log.debug(
514 log.debug(
511 "Merge was successful, updating the pull request comments.")
515 "Merge was successful, updating the pull request comments.")
512 self._comment_and_close_pr(pull_request, user, merge_state)
516 self._comment_and_close_pr(pull_request, user, merge_state)
513 self._log_action('user_merged_pull_request', user, pull_request)
517 self._log_action('user_merged_pull_request', user, pull_request)
514 else:
518 else:
515 log.warn("Merge failed, not updating the pull request.")
519 log.warn("Merge failed, not updating the pull request.")
516 return merge_state
520 return merge_state
517
521
518 def _merge_pull_request(self, pull_request, user, extras):
522 def _merge_pull_request(self, pull_request, user, extras):
519 target_vcs = pull_request.target_repo.scm_instance()
523 target_vcs = pull_request.target_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
524 source_vcs = pull_request.source_repo.scm_instance()
521 target_ref = self._refresh_reference(
525 target_ref = self._refresh_reference(
522 pull_request.target_ref_parts, target_vcs)
526 pull_request.target_ref_parts, target_vcs)
523
527
524 message = _(
528 message = _(
525 'Merge pull request #%(pr_id)s from '
529 'Merge pull request #%(pr_id)s from '
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
530 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 'pr_id': pull_request.pull_request_id,
531 'pr_id': pull_request.pull_request_id,
528 'source_repo': source_vcs.name,
532 'source_repo': source_vcs.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
533 'source_ref_name': pull_request.source_ref_parts.name,
530 'pr_title': pull_request.title
534 'pr_title': pull_request.title
531 }
535 }
532
536
533 workspace_id = self._workspace_id(pull_request)
537 workspace_id = self._workspace_id(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
538 use_rebase = self._use_rebase_for_merging(pull_request)
535
539
536 callback_daemon, extras = prepare_callback_daemon(
540 callback_daemon, extras = prepare_callback_daemon(
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
541 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
542 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539
543
540 with callback_daemon:
544 with callback_daemon:
541 # TODO: johbo: Implement a clean way to run a config_override
545 # TODO: johbo: Implement a clean way to run a config_override
542 # for a single call.
546 # for a single call.
543 target_vcs.config.set(
547 target_vcs.config.set(
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
548 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 merge_state = target_vcs.merge(
549 merge_state = target_vcs.merge(
546 target_ref, source_vcs, pull_request.source_ref_parts,
550 target_ref, source_vcs, pull_request.source_ref_parts,
547 workspace_id, user_name=user.username,
551 workspace_id, user_name=user.username,
548 user_email=user.email, message=message, use_rebase=use_rebase)
552 user_email=user.email, message=message, use_rebase=use_rebase)
549 return merge_state
553 return merge_state
550
554
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
555 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
556 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 pull_request.updated_on = datetime.datetime.now()
557 pull_request.updated_on = datetime.datetime.now()
554
558
555 CommentsModel().create(
559 CommentsModel().create(
556 text=unicode(_('Pull request merged and closed')),
560 text=unicode(_('Pull request merged and closed')),
557 repo=pull_request.target_repo.repo_id,
561 repo=pull_request.target_repo.repo_id,
558 user=user.user_id,
562 user=user.user_id,
559 pull_request=pull_request.pull_request_id,
563 pull_request=pull_request.pull_request_id,
560 f_path=None,
564 f_path=None,
561 line_no=None,
565 line_no=None,
562 closing_pr=True
566 closing_pr=True
563 )
567 )
564
568
565 Session().add(pull_request)
569 Session().add(pull_request)
566 Session().flush()
570 Session().flush()
567 # TODO: paris: replace invalidation with less radical solution
571 # TODO: paris: replace invalidation with less radical solution
568 ScmModel().mark_for_invalidation(
572 ScmModel().mark_for_invalidation(
569 pull_request.target_repo.repo_name)
573 pull_request.target_repo.repo_name)
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
574 self._trigger_pull_request_hook(pull_request, user, 'merge')
571
575
572 def has_valid_update_type(self, pull_request):
576 def has_valid_update_type(self, pull_request):
573 source_ref_type = pull_request.source_ref_parts.type
577 source_ref_type = pull_request.source_ref_parts.type
574 return source_ref_type in ['book', 'branch', 'tag']
578 return source_ref_type in ['book', 'branch', 'tag']
575
579
576 def update_commits(self, pull_request):
580 def update_commits(self, pull_request):
577 """
581 """
578 Get the updated list of commits for the pull request
582 Get the updated list of commits for the pull request
579 and return the new pull request version and the list
583 and return the new pull request version and the list
580 of commits processed by this update action
584 of commits processed by this update action
581 """
585 """
582 pull_request = self.__get_pull_request(pull_request)
586 pull_request = self.__get_pull_request(pull_request)
583 source_ref_type = pull_request.source_ref_parts.type
587 source_ref_type = pull_request.source_ref_parts.type
584 source_ref_name = pull_request.source_ref_parts.name
588 source_ref_name = pull_request.source_ref_parts.name
585 source_ref_id = pull_request.source_ref_parts.commit_id
589 source_ref_id = pull_request.source_ref_parts.commit_id
586
590
587 target_ref_type = pull_request.target_ref_parts.type
591 target_ref_type = pull_request.target_ref_parts.type
588 target_ref_name = pull_request.target_ref_parts.name
592 target_ref_name = pull_request.target_ref_parts.name
589 target_ref_id = pull_request.target_ref_parts.commit_id
593 target_ref_id = pull_request.target_ref_parts.commit_id
590
594
591 if not self.has_valid_update_type(pull_request):
595 if not self.has_valid_update_type(pull_request):
592 log.debug(
596 log.debug(
593 "Skipping update of pull request %s due to ref type: %s",
597 "Skipping update of pull request %s due to ref type: %s",
594 pull_request, source_ref_type)
598 pull_request, source_ref_type)
595 return UpdateResponse(
599 return UpdateResponse(
596 executed=False,
600 executed=False,
597 reason=UpdateFailureReason.WRONG_REF_TYPE,
601 reason=UpdateFailureReason.WRONG_REF_TYPE,
598 old=pull_request, new=None, changes=None,
602 old=pull_request, new=None, changes=None,
599 source_changed=False, target_changed=False)
603 source_changed=False, target_changed=False)
600
604
601 # source repo
605 # source repo
602 source_repo = pull_request.source_repo.scm_instance()
606 source_repo = pull_request.source_repo.scm_instance()
603 try:
607 try:
604 source_commit = source_repo.get_commit(commit_id=source_ref_name)
608 source_commit = source_repo.get_commit(commit_id=source_ref_name)
605 except CommitDoesNotExistError:
609 except CommitDoesNotExistError:
606 return UpdateResponse(
610 return UpdateResponse(
607 executed=False,
611 executed=False,
608 reason=UpdateFailureReason.MISSING_SOURCE_REF,
612 reason=UpdateFailureReason.MISSING_SOURCE_REF,
609 old=pull_request, new=None, changes=None,
613 old=pull_request, new=None, changes=None,
610 source_changed=False, target_changed=False)
614 source_changed=False, target_changed=False)
611
615
612 source_changed = source_ref_id != source_commit.raw_id
616 source_changed = source_ref_id != source_commit.raw_id
613
617
614 # target repo
618 # target repo
615 target_repo = pull_request.target_repo.scm_instance()
619 target_repo = pull_request.target_repo.scm_instance()
616 try:
620 try:
617 target_commit = target_repo.get_commit(commit_id=target_ref_name)
621 target_commit = target_repo.get_commit(commit_id=target_ref_name)
618 except CommitDoesNotExistError:
622 except CommitDoesNotExistError:
619 return UpdateResponse(
623 return UpdateResponse(
620 executed=False,
624 executed=False,
621 reason=UpdateFailureReason.MISSING_TARGET_REF,
625 reason=UpdateFailureReason.MISSING_TARGET_REF,
622 old=pull_request, new=None, changes=None,
626 old=pull_request, new=None, changes=None,
623 source_changed=False, target_changed=False)
627 source_changed=False, target_changed=False)
624 target_changed = target_ref_id != target_commit.raw_id
628 target_changed = target_ref_id != target_commit.raw_id
625
629
626 if not (source_changed or target_changed):
630 if not (source_changed or target_changed):
627 log.debug("Nothing changed in pull request %s", pull_request)
631 log.debug("Nothing changed in pull request %s", pull_request)
628 return UpdateResponse(
632 return UpdateResponse(
629 executed=False,
633 executed=False,
630 reason=UpdateFailureReason.NO_CHANGE,
634 reason=UpdateFailureReason.NO_CHANGE,
631 old=pull_request, new=None, changes=None,
635 old=pull_request, new=None, changes=None,
632 source_changed=target_changed, target_changed=source_changed)
636 source_changed=target_changed, target_changed=source_changed)
633
637
634 change_in_found = 'target repo' if target_changed else 'source repo'
638 change_in_found = 'target repo' if target_changed else 'source repo'
635 log.debug('Updating pull request because of change in %s detected',
639 log.debug('Updating pull request because of change in %s detected',
636 change_in_found)
640 change_in_found)
637
641
638 # Finally there is a need for an update, in case of source change
642 # Finally there is a need for an update, in case of source change
639 # we create a new version, else just an update
643 # we create a new version, else just an update
640 if source_changed:
644 if source_changed:
641 pull_request_version = self._create_version_from_snapshot(pull_request)
645 pull_request_version = self._create_version_from_snapshot(pull_request)
642 self._link_comments_to_version(pull_request_version)
646 self._link_comments_to_version(pull_request_version)
643 else:
647 else:
644 try:
648 try:
645 ver = pull_request.versions[-1]
649 ver = pull_request.versions[-1]
646 except IndexError:
650 except IndexError:
647 ver = None
651 ver = None
648
652
649 pull_request.pull_request_version_id = \
653 pull_request.pull_request_version_id = \
650 ver.pull_request_version_id if ver else None
654 ver.pull_request_version_id if ver else None
651 pull_request_version = pull_request
655 pull_request_version = pull_request
652
656
653 try:
657 try:
654 if target_ref_type in ('tag', 'branch', 'book'):
658 if target_ref_type in ('tag', 'branch', 'book'):
655 target_commit = target_repo.get_commit(target_ref_name)
659 target_commit = target_repo.get_commit(target_ref_name)
656 else:
660 else:
657 target_commit = target_repo.get_commit(target_ref_id)
661 target_commit = target_repo.get_commit(target_ref_id)
658 except CommitDoesNotExistError:
662 except CommitDoesNotExistError:
659 return UpdateResponse(
663 return UpdateResponse(
660 executed=False,
664 executed=False,
661 reason=UpdateFailureReason.MISSING_TARGET_REF,
665 reason=UpdateFailureReason.MISSING_TARGET_REF,
662 old=pull_request, new=None, changes=None,
666 old=pull_request, new=None, changes=None,
663 source_changed=source_changed, target_changed=target_changed)
667 source_changed=source_changed, target_changed=target_changed)
664
668
665 # re-compute commit ids
669 # re-compute commit ids
666 old_commit_ids = pull_request.revisions
670 old_commit_ids = pull_request.revisions
667 pre_load = ["author", "branch", "date", "message"]
671 pre_load = ["author", "branch", "date", "message"]
668 commit_ranges = target_repo.compare(
672 commit_ranges = target_repo.compare(
669 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
673 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
670 pre_load=pre_load)
674 pre_load=pre_load)
671
675
672 ancestor = target_repo.get_common_ancestor(
676 ancestor = target_repo.get_common_ancestor(
673 target_commit.raw_id, source_commit.raw_id, source_repo)
677 target_commit.raw_id, source_commit.raw_id, source_repo)
674
678
675 pull_request.source_ref = '%s:%s:%s' % (
679 pull_request.source_ref = '%s:%s:%s' % (
676 source_ref_type, source_ref_name, source_commit.raw_id)
680 source_ref_type, source_ref_name, source_commit.raw_id)
677 pull_request.target_ref = '%s:%s:%s' % (
681 pull_request.target_ref = '%s:%s:%s' % (
678 target_ref_type, target_ref_name, ancestor)
682 target_ref_type, target_ref_name, ancestor)
679
683
680 pull_request.revisions = [
684 pull_request.revisions = [
681 commit.raw_id for commit in reversed(commit_ranges)]
685 commit.raw_id for commit in reversed(commit_ranges)]
682 pull_request.updated_on = datetime.datetime.now()
686 pull_request.updated_on = datetime.datetime.now()
683 Session().add(pull_request)
687 Session().add(pull_request)
684 new_commit_ids = pull_request.revisions
688 new_commit_ids = pull_request.revisions
685
689
686 old_diff_data, new_diff_data = self._generate_update_diffs(
690 old_diff_data, new_diff_data = self._generate_update_diffs(
687 pull_request, pull_request_version)
691 pull_request, pull_request_version)
688
692
689 # calculate commit and file changes
693 # calculate commit and file changes
690 changes = self._calculate_commit_id_changes(
694 changes = self._calculate_commit_id_changes(
691 old_commit_ids, new_commit_ids)
695 old_commit_ids, new_commit_ids)
692 file_changes = self._calculate_file_changes(
696 file_changes = self._calculate_file_changes(
693 old_diff_data, new_diff_data)
697 old_diff_data, new_diff_data)
694
698
695 # set comments as outdated if DIFFS changed
699 # set comments as outdated if DIFFS changed
696 CommentsModel().outdate_comments(
700 CommentsModel().outdate_comments(
697 pull_request, old_diff_data=old_diff_data,
701 pull_request, old_diff_data=old_diff_data,
698 new_diff_data=new_diff_data)
702 new_diff_data=new_diff_data)
699
703
700 commit_changes = (changes.added or changes.removed)
704 commit_changes = (changes.added or changes.removed)
701 file_node_changes = (
705 file_node_changes = (
702 file_changes.added or file_changes.modified or file_changes.removed)
706 file_changes.added or file_changes.modified or file_changes.removed)
703 pr_has_changes = commit_changes or file_node_changes
707 pr_has_changes = commit_changes or file_node_changes
704
708
705 # Add an automatic comment to the pull request, in case
709 # Add an automatic comment to the pull request, in case
706 # anything has changed
710 # anything has changed
707 if pr_has_changes:
711 if pr_has_changes:
708 update_comment = CommentsModel().create(
712 update_comment = CommentsModel().create(
709 text=self._render_update_message(changes, file_changes),
713 text=self._render_update_message(changes, file_changes),
710 repo=pull_request.target_repo,
714 repo=pull_request.target_repo,
711 user=pull_request.author,
715 user=pull_request.author,
712 pull_request=pull_request,
716 pull_request=pull_request,
713 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
717 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
714
718
715 # Update status to "Under Review" for added commits
719 # Update status to "Under Review" for added commits
716 for commit_id in changes.added:
720 for commit_id in changes.added:
717 ChangesetStatusModel().set_status(
721 ChangesetStatusModel().set_status(
718 repo=pull_request.source_repo,
722 repo=pull_request.source_repo,
719 status=ChangesetStatus.STATUS_UNDER_REVIEW,
723 status=ChangesetStatus.STATUS_UNDER_REVIEW,
720 comment=update_comment,
724 comment=update_comment,
721 user=pull_request.author,
725 user=pull_request.author,
722 pull_request=pull_request,
726 pull_request=pull_request,
723 revision=commit_id)
727 revision=commit_id)
724
728
725 log.debug(
729 log.debug(
726 'Updated pull request %s, added_ids: %s, common_ids: %s, '
730 'Updated pull request %s, added_ids: %s, common_ids: %s, '
727 'removed_ids: %s', pull_request.pull_request_id,
731 'removed_ids: %s', pull_request.pull_request_id,
728 changes.added, changes.common, changes.removed)
732 changes.added, changes.common, changes.removed)
729 log.debug(
733 log.debug(
730 'Updated pull request with the following file changes: %s',
734 'Updated pull request with the following file changes: %s',
731 file_changes)
735 file_changes)
732
736
733 log.info(
737 log.info(
734 "Updated pull request %s from commit %s to commit %s, "
738 "Updated pull request %s from commit %s to commit %s, "
735 "stored new version %s of this pull request.",
739 "stored new version %s of this pull request.",
736 pull_request.pull_request_id, source_ref_id,
740 pull_request.pull_request_id, source_ref_id,
737 pull_request.source_ref_parts.commit_id,
741 pull_request.source_ref_parts.commit_id,
738 pull_request_version.pull_request_version_id)
742 pull_request_version.pull_request_version_id)
739 Session().commit()
743 Session().commit()
740 self._trigger_pull_request_hook(
744 self._trigger_pull_request_hook(
741 pull_request, pull_request.author, 'update')
745 pull_request, pull_request.author, 'update')
742
746
743 return UpdateResponse(
747 return UpdateResponse(
744 executed=True, reason=UpdateFailureReason.NONE,
748 executed=True, reason=UpdateFailureReason.NONE,
745 old=pull_request, new=pull_request_version, changes=changes,
749 old=pull_request, new=pull_request_version, changes=changes,
746 source_changed=source_changed, target_changed=target_changed)
750 source_changed=source_changed, target_changed=target_changed)
747
751
748 def _create_version_from_snapshot(self, pull_request):
752 def _create_version_from_snapshot(self, pull_request):
749 version = PullRequestVersion()
753 version = PullRequestVersion()
750 version.title = pull_request.title
754 version.title = pull_request.title
751 version.description = pull_request.description
755 version.description = pull_request.description
752 version.status = pull_request.status
756 version.status = pull_request.status
753 version.created_on = datetime.datetime.now()
757 version.created_on = datetime.datetime.now()
754 version.updated_on = pull_request.updated_on
758 version.updated_on = pull_request.updated_on
755 version.user_id = pull_request.user_id
759 version.user_id = pull_request.user_id
756 version.source_repo = pull_request.source_repo
760 version.source_repo = pull_request.source_repo
757 version.source_ref = pull_request.source_ref
761 version.source_ref = pull_request.source_ref
758 version.target_repo = pull_request.target_repo
762 version.target_repo = pull_request.target_repo
759 version.target_ref = pull_request.target_ref
763 version.target_ref = pull_request.target_ref
760
764
761 version._last_merge_source_rev = pull_request._last_merge_source_rev
765 version._last_merge_source_rev = pull_request._last_merge_source_rev
762 version._last_merge_target_rev = pull_request._last_merge_target_rev
766 version._last_merge_target_rev = pull_request._last_merge_target_rev
763 version._last_merge_status = pull_request._last_merge_status
767 version._last_merge_status = pull_request._last_merge_status
764 version.shadow_merge_ref = pull_request.shadow_merge_ref
768 version.shadow_merge_ref = pull_request.shadow_merge_ref
765 version.merge_rev = pull_request.merge_rev
769 version.merge_rev = pull_request.merge_rev
770 version.reviewer_data = pull_request.reviewer_data
766
771
767 version.revisions = pull_request.revisions
772 version.revisions = pull_request.revisions
768 version.pull_request = pull_request
773 version.pull_request = pull_request
769 Session().add(version)
774 Session().add(version)
770 Session().flush()
775 Session().flush()
771
776
772 return version
777 return version
773
778
774 def _generate_update_diffs(self, pull_request, pull_request_version):
779 def _generate_update_diffs(self, pull_request, pull_request_version):
775
780
776 diff_context = (
781 diff_context = (
777 self.DIFF_CONTEXT +
782 self.DIFF_CONTEXT +
778 CommentsModel.needed_extra_diff_context())
783 CommentsModel.needed_extra_diff_context())
779
784
780 source_repo = pull_request_version.source_repo
785 source_repo = pull_request_version.source_repo
781 source_ref_id = pull_request_version.source_ref_parts.commit_id
786 source_ref_id = pull_request_version.source_ref_parts.commit_id
782 target_ref_id = pull_request_version.target_ref_parts.commit_id
787 target_ref_id = pull_request_version.target_ref_parts.commit_id
783 old_diff = self._get_diff_from_pr_or_version(
788 old_diff = self._get_diff_from_pr_or_version(
784 source_repo, source_ref_id, target_ref_id, context=diff_context)
789 source_repo, source_ref_id, target_ref_id, context=diff_context)
785
790
786 source_repo = pull_request.source_repo
791 source_repo = pull_request.source_repo
787 source_ref_id = pull_request.source_ref_parts.commit_id
792 source_ref_id = pull_request.source_ref_parts.commit_id
788 target_ref_id = pull_request.target_ref_parts.commit_id
793 target_ref_id = pull_request.target_ref_parts.commit_id
789
794
790 new_diff = self._get_diff_from_pr_or_version(
795 new_diff = self._get_diff_from_pr_or_version(
791 source_repo, source_ref_id, target_ref_id, context=diff_context)
796 source_repo, source_ref_id, target_ref_id, context=diff_context)
792
797
793 old_diff_data = diffs.DiffProcessor(old_diff)
798 old_diff_data = diffs.DiffProcessor(old_diff)
794 old_diff_data.prepare()
799 old_diff_data.prepare()
795 new_diff_data = diffs.DiffProcessor(new_diff)
800 new_diff_data = diffs.DiffProcessor(new_diff)
796 new_diff_data.prepare()
801 new_diff_data.prepare()
797
802
798 return old_diff_data, new_diff_data
803 return old_diff_data, new_diff_data
799
804
800 def _link_comments_to_version(self, pull_request_version):
805 def _link_comments_to_version(self, pull_request_version):
801 """
806 """
802 Link all unlinked comments of this pull request to the given version.
807 Link all unlinked comments of this pull request to the given version.
803
808
804 :param pull_request_version: The `PullRequestVersion` to which
809 :param pull_request_version: The `PullRequestVersion` to which
805 the comments shall be linked.
810 the comments shall be linked.
806
811
807 """
812 """
808 pull_request = pull_request_version.pull_request
813 pull_request = pull_request_version.pull_request
809 comments = ChangesetComment.query()\
814 comments = ChangesetComment.query()\
810 .filter(
815 .filter(
811 # TODO: johbo: Should we query for the repo at all here?
816 # TODO: johbo: Should we query for the repo at all here?
812 # Pending decision on how comments of PRs are to be related
817 # Pending decision on how comments of PRs are to be related
813 # to either the source repo, the target repo or no repo at all.
818 # to either the source repo, the target repo or no repo at all.
814 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
819 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
815 ChangesetComment.pull_request == pull_request,
820 ChangesetComment.pull_request == pull_request,
816 ChangesetComment.pull_request_version == None)\
821 ChangesetComment.pull_request_version == None)\
817 .order_by(ChangesetComment.comment_id.asc())
822 .order_by(ChangesetComment.comment_id.asc())
818
823
819 # TODO: johbo: Find out why this breaks if it is done in a bulk
824 # TODO: johbo: Find out why this breaks if it is done in a bulk
820 # operation.
825 # operation.
821 for comment in comments:
826 for comment in comments:
822 comment.pull_request_version_id = (
827 comment.pull_request_version_id = (
823 pull_request_version.pull_request_version_id)
828 pull_request_version.pull_request_version_id)
824 Session().add(comment)
829 Session().add(comment)
825
830
826 def _calculate_commit_id_changes(self, old_ids, new_ids):
831 def _calculate_commit_id_changes(self, old_ids, new_ids):
827 added = [x for x in new_ids if x not in old_ids]
832 added = [x for x in new_ids if x not in old_ids]
828 common = [x for x in new_ids if x in old_ids]
833 common = [x for x in new_ids if x in old_ids]
829 removed = [x for x in old_ids if x not in new_ids]
834 removed = [x for x in old_ids if x not in new_ids]
830 total = new_ids
835 total = new_ids
831 return ChangeTuple(added, common, removed, total)
836 return ChangeTuple(added, common, removed, total)
832
837
833 def _calculate_file_changes(self, old_diff_data, new_diff_data):
838 def _calculate_file_changes(self, old_diff_data, new_diff_data):
834
839
835 old_files = OrderedDict()
840 old_files = OrderedDict()
836 for diff_data in old_diff_data.parsed_diff:
841 for diff_data in old_diff_data.parsed_diff:
837 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
842 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
838
843
839 added_files = []
844 added_files = []
840 modified_files = []
845 modified_files = []
841 removed_files = []
846 removed_files = []
842 for diff_data in new_diff_data.parsed_diff:
847 for diff_data in new_diff_data.parsed_diff:
843 new_filename = diff_data['filename']
848 new_filename = diff_data['filename']
844 new_hash = md5_safe(diff_data['raw_diff'])
849 new_hash = md5_safe(diff_data['raw_diff'])
845
850
846 old_hash = old_files.get(new_filename)
851 old_hash = old_files.get(new_filename)
847 if not old_hash:
852 if not old_hash:
848 # file is not present in old diff, means it's added
853 # file is not present in old diff, means it's added
849 added_files.append(new_filename)
854 added_files.append(new_filename)
850 else:
855 else:
851 if new_hash != old_hash:
856 if new_hash != old_hash:
852 modified_files.append(new_filename)
857 modified_files.append(new_filename)
853 # now remove a file from old, since we have seen it already
858 # now remove a file from old, since we have seen it already
854 del old_files[new_filename]
859 del old_files[new_filename]
855
860
856 # removed files is when there are present in old, but not in NEW,
861 # removed files is when there are present in old, but not in NEW,
857 # since we remove old files that are present in new diff, left-overs
862 # since we remove old files that are present in new diff, left-overs
858 # if any should be the removed files
863 # if any should be the removed files
859 removed_files.extend(old_files.keys())
864 removed_files.extend(old_files.keys())
860
865
861 return FileChangeTuple(added_files, modified_files, removed_files)
866 return FileChangeTuple(added_files, modified_files, removed_files)
862
867
863 def _render_update_message(self, changes, file_changes):
868 def _render_update_message(self, changes, file_changes):
864 """
869 """
865 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
870 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
866 so it's always looking the same disregarding on which default
871 so it's always looking the same disregarding on which default
867 renderer system is using.
872 renderer system is using.
868
873
869 :param changes: changes named tuple
874 :param changes: changes named tuple
870 :param file_changes: file changes named tuple
875 :param file_changes: file changes named tuple
871
876
872 """
877 """
873 new_status = ChangesetStatus.get_status_lbl(
878 new_status = ChangesetStatus.get_status_lbl(
874 ChangesetStatus.STATUS_UNDER_REVIEW)
879 ChangesetStatus.STATUS_UNDER_REVIEW)
875
880
876 changed_files = (
881 changed_files = (
877 file_changes.added + file_changes.modified + file_changes.removed)
882 file_changes.added + file_changes.modified + file_changes.removed)
878
883
879 params = {
884 params = {
880 'under_review_label': new_status,
885 'under_review_label': new_status,
881 'added_commits': changes.added,
886 'added_commits': changes.added,
882 'removed_commits': changes.removed,
887 'removed_commits': changes.removed,
883 'changed_files': changed_files,
888 'changed_files': changed_files,
884 'added_files': file_changes.added,
889 'added_files': file_changes.added,
885 'modified_files': file_changes.modified,
890 'modified_files': file_changes.modified,
886 'removed_files': file_changes.removed,
891 'removed_files': file_changes.removed,
887 }
892 }
888 renderer = RstTemplateRenderer()
893 renderer = RstTemplateRenderer()
889 return renderer.render('pull_request_update.mako', **params)
894 return renderer.render('pull_request_update.mako', **params)
890
895
891 def edit(self, pull_request, title, description):
896 def edit(self, pull_request, title, description):
892 pull_request = self.__get_pull_request(pull_request)
897 pull_request = self.__get_pull_request(pull_request)
893 if pull_request.is_closed():
898 if pull_request.is_closed():
894 raise ValueError('This pull request is closed')
899 raise ValueError('This pull request is closed')
895 if title:
900 if title:
896 pull_request.title = title
901 pull_request.title = title
897 pull_request.description = description
902 pull_request.description = description
898 pull_request.updated_on = datetime.datetime.now()
903 pull_request.updated_on = datetime.datetime.now()
899 Session().add(pull_request)
904 Session().add(pull_request)
900
905
901 def update_reviewers(self, pull_request, reviewer_data):
906 def update_reviewers(self, pull_request, reviewer_data):
902 """
907 """
903 Update the reviewers in the pull request
908 Update the reviewers in the pull request
904
909
905 :param pull_request: the pr to update
910 :param pull_request: the pr to update
906 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
911 :param reviewer_data: list of tuples
912 [(user, ['reason1', 'reason2'], mandatory_flag)]
907 """
913 """
908
914
909 reviewers_reasons = {}
915 reviewers = {}
910 for user_id, reasons in reviewer_data:
916 for user_id, reasons, mandatory in reviewer_data:
911 if isinstance(user_id, (int, basestring)):
917 if isinstance(user_id, (int, basestring)):
912 user_id = self._get_user(user_id).user_id
918 user_id = self._get_user(user_id).user_id
913 reviewers_reasons[user_id] = reasons
919 reviewers[user_id] = {
920 'reasons': reasons, 'mandatory': mandatory}
914
921
915 reviewers_ids = set(reviewers_reasons.keys())
922 reviewers_ids = set(reviewers.keys())
916 pull_request = self.__get_pull_request(pull_request)
923 pull_request = self.__get_pull_request(pull_request)
917 current_reviewers = PullRequestReviewers.query()\
924 current_reviewers = PullRequestReviewers.query()\
918 .filter(PullRequestReviewers.pull_request ==
925 .filter(PullRequestReviewers.pull_request ==
919 pull_request).all()
926 pull_request).all()
920 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
927 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
921
928
922 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
929 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
923 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
930 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
924
931
925 log.debug("Adding %s reviewers", ids_to_add)
932 log.debug("Adding %s reviewers", ids_to_add)
926 log.debug("Removing %s reviewers", ids_to_remove)
933 log.debug("Removing %s reviewers", ids_to_remove)
927 changed = False
934 changed = False
928 for uid in ids_to_add:
935 for uid in ids_to_add:
929 changed = True
936 changed = True
930 _usr = self._get_user(uid)
937 _usr = self._get_user(uid)
931 reasons = reviewers_reasons[uid]
938 reviewer = PullRequestReviewers()
932 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
939 reviewer.user = _usr
940 reviewer.pull_request = pull_request
941 reviewer.reasons = reviewers[uid]['reasons']
942 # NOTE(marcink): mandatory shouldn't be changed now
943 #reviewer.mandatory = reviewers[uid]['reasons']
933 Session().add(reviewer)
944 Session().add(reviewer)
934
945
935 for uid in ids_to_remove:
946 for uid in ids_to_remove:
936 changed = True
947 changed = True
937 reviewers = PullRequestReviewers.query()\
948 reviewers = PullRequestReviewers.query()\
938 .filter(PullRequestReviewers.user_id == uid,
949 .filter(PullRequestReviewers.user_id == uid,
939 PullRequestReviewers.pull_request == pull_request)\
950 PullRequestReviewers.pull_request == pull_request)\
940 .all()
951 .all()
941 # use .all() in case we accidentally added the same person twice
952 # use .all() in case we accidentally added the same person twice
942 # this CAN happen due to the lack of DB checks
953 # this CAN happen due to the lack of DB checks
943 for obj in reviewers:
954 for obj in reviewers:
944 Session().delete(obj)
955 Session().delete(obj)
945
956
946 if changed:
957 if changed:
947 pull_request.updated_on = datetime.datetime.now()
958 pull_request.updated_on = datetime.datetime.now()
948 Session().add(pull_request)
959 Session().add(pull_request)
949
960
950 self.notify_reviewers(pull_request, ids_to_add)
961 self.notify_reviewers(pull_request, ids_to_add)
951 return ids_to_add, ids_to_remove
962 return ids_to_add, ids_to_remove
952
963
953 def get_url(self, pull_request):
964 def get_url(self, pull_request):
954 return h.url('pullrequest_show',
965 return h.url('pullrequest_show',
955 repo_name=safe_str(pull_request.target_repo.repo_name),
966 repo_name=safe_str(pull_request.target_repo.repo_name),
956 pull_request_id=pull_request.pull_request_id,
967 pull_request_id=pull_request.pull_request_id,
957 qualified=True)
968 qualified=True)
958
969
959 def get_shadow_clone_url(self, pull_request):
970 def get_shadow_clone_url(self, pull_request):
960 """
971 """
961 Returns qualified url pointing to the shadow repository. If this pull
972 Returns qualified url pointing to the shadow repository. If this pull
962 request is closed there is no shadow repository and ``None`` will be
973 request is closed there is no shadow repository and ``None`` will be
963 returned.
974 returned.
964 """
975 """
965 if pull_request.is_closed():
976 if pull_request.is_closed():
966 return None
977 return None
967 else:
978 else:
968 pr_url = urllib.unquote(self.get_url(pull_request))
979 pr_url = urllib.unquote(self.get_url(pull_request))
969 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
980 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
970
981
971 def notify_reviewers(self, pull_request, reviewers_ids):
982 def notify_reviewers(self, pull_request, reviewers_ids):
972 # notification to reviewers
983 # notification to reviewers
973 if not reviewers_ids:
984 if not reviewers_ids:
974 return
985 return
975
986
976 pull_request_obj = pull_request
987 pull_request_obj = pull_request
977 # get the current participants of this pull request
988 # get the current participants of this pull request
978 recipients = reviewers_ids
989 recipients = reviewers_ids
979 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
990 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
980
991
981 pr_source_repo = pull_request_obj.source_repo
992 pr_source_repo = pull_request_obj.source_repo
982 pr_target_repo = pull_request_obj.target_repo
993 pr_target_repo = pull_request_obj.target_repo
983
994
984 pr_url = h.url(
995 pr_url = h.url(
985 'pullrequest_show',
996 'pullrequest_show',
986 repo_name=pr_target_repo.repo_name,
997 repo_name=pr_target_repo.repo_name,
987 pull_request_id=pull_request_obj.pull_request_id,
998 pull_request_id=pull_request_obj.pull_request_id,
988 qualified=True,)
999 qualified=True,)
989
1000
990 # set some variables for email notification
1001 # set some variables for email notification
991 pr_target_repo_url = h.url(
1002 pr_target_repo_url = h.url(
992 'summary_home',
1003 'summary_home',
993 repo_name=pr_target_repo.repo_name,
1004 repo_name=pr_target_repo.repo_name,
994 qualified=True)
1005 qualified=True)
995
1006
996 pr_source_repo_url = h.url(
1007 pr_source_repo_url = h.url(
997 'summary_home',
1008 'summary_home',
998 repo_name=pr_source_repo.repo_name,
1009 repo_name=pr_source_repo.repo_name,
999 qualified=True)
1010 qualified=True)
1000
1011
1001 # pull request specifics
1012 # pull request specifics
1002 pull_request_commits = [
1013 pull_request_commits = [
1003 (x.raw_id, x.message)
1014 (x.raw_id, x.message)
1004 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1015 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1005
1016
1006 kwargs = {
1017 kwargs = {
1007 'user': pull_request.author,
1018 'user': pull_request.author,
1008 'pull_request': pull_request_obj,
1019 'pull_request': pull_request_obj,
1009 'pull_request_commits': pull_request_commits,
1020 'pull_request_commits': pull_request_commits,
1010
1021
1011 'pull_request_target_repo': pr_target_repo,
1022 'pull_request_target_repo': pr_target_repo,
1012 'pull_request_target_repo_url': pr_target_repo_url,
1023 'pull_request_target_repo_url': pr_target_repo_url,
1013
1024
1014 'pull_request_source_repo': pr_source_repo,
1025 'pull_request_source_repo': pr_source_repo,
1015 'pull_request_source_repo_url': pr_source_repo_url,
1026 'pull_request_source_repo_url': pr_source_repo_url,
1016
1027
1017 'pull_request_url': pr_url,
1028 'pull_request_url': pr_url,
1018 }
1029 }
1019
1030
1020 # pre-generate the subject for notification itself
1031 # pre-generate the subject for notification itself
1021 (subject,
1032 (subject,
1022 _h, _e, # we don't care about those
1033 _h, _e, # we don't care about those
1023 body_plaintext) = EmailNotificationModel().render_email(
1034 body_plaintext) = EmailNotificationModel().render_email(
1024 notification_type, **kwargs)
1035 notification_type, **kwargs)
1025
1036
1026 # create notification objects, and emails
1037 # create notification objects, and emails
1027 NotificationModel().create(
1038 NotificationModel().create(
1028 created_by=pull_request.author,
1039 created_by=pull_request.author,
1029 notification_subject=subject,
1040 notification_subject=subject,
1030 notification_body=body_plaintext,
1041 notification_body=body_plaintext,
1031 notification_type=notification_type,
1042 notification_type=notification_type,
1032 recipients=recipients,
1043 recipients=recipients,
1033 email_kwargs=kwargs,
1044 email_kwargs=kwargs,
1034 )
1045 )
1035
1046
1036 def delete(self, pull_request):
1047 def delete(self, pull_request):
1037 pull_request = self.__get_pull_request(pull_request)
1048 pull_request = self.__get_pull_request(pull_request)
1038 self._cleanup_merge_workspace(pull_request)
1049 self._cleanup_merge_workspace(pull_request)
1039 Session().delete(pull_request)
1050 Session().delete(pull_request)
1040
1051
1041 def close_pull_request(self, pull_request, user):
1052 def close_pull_request(self, pull_request, user):
1042 pull_request = self.__get_pull_request(pull_request)
1053 pull_request = self.__get_pull_request(pull_request)
1043 self._cleanup_merge_workspace(pull_request)
1054 self._cleanup_merge_workspace(pull_request)
1044 pull_request.status = PullRequest.STATUS_CLOSED
1055 pull_request.status = PullRequest.STATUS_CLOSED
1045 pull_request.updated_on = datetime.datetime.now()
1056 pull_request.updated_on = datetime.datetime.now()
1046 Session().add(pull_request)
1057 Session().add(pull_request)
1047 self._trigger_pull_request_hook(
1058 self._trigger_pull_request_hook(
1048 pull_request, pull_request.author, 'close')
1059 pull_request, pull_request.author, 'close')
1049 self._log_action('user_closed_pull_request', user, pull_request)
1060 self._log_action('user_closed_pull_request', user, pull_request)
1050
1061
1051 def close_pull_request_with_comment(self, pull_request, user, repo,
1062 def close_pull_request_with_comment(self, pull_request, user, repo,
1052 message=None):
1063 message=None):
1053 status = ChangesetStatus.STATUS_REJECTED
1064 status = ChangesetStatus.STATUS_REJECTED
1054
1065
1055 if not message:
1066 if not message:
1056 message = (
1067 message = (
1057 _('Status change %(transition_icon)s %(status)s') % {
1068 _('Status change %(transition_icon)s %(status)s') % {
1058 'transition_icon': '>',
1069 'transition_icon': '>',
1059 'status': ChangesetStatus.get_status_lbl(status)})
1070 'status': ChangesetStatus.get_status_lbl(status)})
1060
1071
1061 internal_message = _('Closing with') + ' ' + message
1072 internal_message = _('Closing with') + ' ' + message
1062
1073
1063 comm = CommentsModel().create(
1074 comm = CommentsModel().create(
1064 text=internal_message,
1075 text=internal_message,
1065 repo=repo.repo_id,
1076 repo=repo.repo_id,
1066 user=user.user_id,
1077 user=user.user_id,
1067 pull_request=pull_request.pull_request_id,
1078 pull_request=pull_request.pull_request_id,
1068 f_path=None,
1079 f_path=None,
1069 line_no=None,
1080 line_no=None,
1070 status_change=ChangesetStatus.get_status_lbl(status),
1081 status_change=ChangesetStatus.get_status_lbl(status),
1071 status_change_type=status,
1082 status_change_type=status,
1072 closing_pr=True
1083 closing_pr=True
1073 )
1084 )
1074
1085
1075 ChangesetStatusModel().set_status(
1086 ChangesetStatusModel().set_status(
1076 repo.repo_id,
1087 repo.repo_id,
1077 status,
1088 status,
1078 user.user_id,
1089 user.user_id,
1079 comm,
1090 comm,
1080 pull_request=pull_request.pull_request_id
1091 pull_request=pull_request.pull_request_id
1081 )
1092 )
1082 Session().flush()
1093 Session().flush()
1083
1094
1084 PullRequestModel().close_pull_request(
1095 PullRequestModel().close_pull_request(
1085 pull_request.pull_request_id, user)
1096 pull_request.pull_request_id, user)
1086
1097
1087 def merge_status(self, pull_request):
1098 def merge_status(self, pull_request):
1088 if not self._is_merge_enabled(pull_request):
1099 if not self._is_merge_enabled(pull_request):
1089 return False, _('Server-side pull request merging is disabled.')
1100 return False, _('Server-side pull request merging is disabled.')
1090 if pull_request.is_closed():
1101 if pull_request.is_closed():
1091 return False, _('This pull request is closed.')
1102 return False, _('This pull request is closed.')
1092 merge_possible, msg = self._check_repo_requirements(
1103 merge_possible, msg = self._check_repo_requirements(
1093 target=pull_request.target_repo, source=pull_request.source_repo)
1104 target=pull_request.target_repo, source=pull_request.source_repo)
1094 if not merge_possible:
1105 if not merge_possible:
1095 return merge_possible, msg
1106 return merge_possible, msg
1096
1107
1097 try:
1108 try:
1098 resp = self._try_merge(pull_request)
1109 resp = self._try_merge(pull_request)
1099 log.debug("Merge response: %s", resp)
1110 log.debug("Merge response: %s", resp)
1100 status = resp.possible, self.merge_status_message(
1111 status = resp.possible, self.merge_status_message(
1101 resp.failure_reason)
1112 resp.failure_reason)
1102 except NotImplementedError:
1113 except NotImplementedError:
1103 status = False, _('Pull request merging is not supported.')
1114 status = False, _('Pull request merging is not supported.')
1104
1115
1105 return status
1116 return status
1106
1117
1107 def _check_repo_requirements(self, target, source):
1118 def _check_repo_requirements(self, target, source):
1108 """
1119 """
1109 Check if `target` and `source` have compatible requirements.
1120 Check if `target` and `source` have compatible requirements.
1110
1121
1111 Currently this is just checking for largefiles.
1122 Currently this is just checking for largefiles.
1112 """
1123 """
1113 target_has_largefiles = self._has_largefiles(target)
1124 target_has_largefiles = self._has_largefiles(target)
1114 source_has_largefiles = self._has_largefiles(source)
1125 source_has_largefiles = self._has_largefiles(source)
1115 merge_possible = True
1126 merge_possible = True
1116 message = u''
1127 message = u''
1117
1128
1118 if target_has_largefiles != source_has_largefiles:
1129 if target_has_largefiles != source_has_largefiles:
1119 merge_possible = False
1130 merge_possible = False
1120 if source_has_largefiles:
1131 if source_has_largefiles:
1121 message = _(
1132 message = _(
1122 'Target repository large files support is disabled.')
1133 'Target repository large files support is disabled.')
1123 else:
1134 else:
1124 message = _(
1135 message = _(
1125 'Source repository large files support is disabled.')
1136 'Source repository large files support is disabled.')
1126
1137
1127 return merge_possible, message
1138 return merge_possible, message
1128
1139
1129 def _has_largefiles(self, repo):
1140 def _has_largefiles(self, repo):
1130 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1141 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1131 'extensions', 'largefiles')
1142 'extensions', 'largefiles')
1132 return largefiles_ui and largefiles_ui[0].active
1143 return largefiles_ui and largefiles_ui[0].active
1133
1144
1134 def _try_merge(self, pull_request):
1145 def _try_merge(self, pull_request):
1135 """
1146 """
1136 Try to merge the pull request and return the merge status.
1147 Try to merge the pull request and return the merge status.
1137 """
1148 """
1138 log.debug(
1149 log.debug(
1139 "Trying out if the pull request %s can be merged.",
1150 "Trying out if the pull request %s can be merged.",
1140 pull_request.pull_request_id)
1151 pull_request.pull_request_id)
1141 target_vcs = pull_request.target_repo.scm_instance()
1152 target_vcs = pull_request.target_repo.scm_instance()
1142
1153
1143 # Refresh the target reference.
1154 # Refresh the target reference.
1144 try:
1155 try:
1145 target_ref = self._refresh_reference(
1156 target_ref = self._refresh_reference(
1146 pull_request.target_ref_parts, target_vcs)
1157 pull_request.target_ref_parts, target_vcs)
1147 except CommitDoesNotExistError:
1158 except CommitDoesNotExistError:
1148 merge_state = MergeResponse(
1159 merge_state = MergeResponse(
1149 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1160 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1150 return merge_state
1161 return merge_state
1151
1162
1152 target_locked = pull_request.target_repo.locked
1163 target_locked = pull_request.target_repo.locked
1153 if target_locked and target_locked[0]:
1164 if target_locked and target_locked[0]:
1154 log.debug("The target repository is locked.")
1165 log.debug("The target repository is locked.")
1155 merge_state = MergeResponse(
1166 merge_state = MergeResponse(
1156 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1167 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1157 elif self._needs_merge_state_refresh(pull_request, target_ref):
1168 elif self._needs_merge_state_refresh(pull_request, target_ref):
1158 log.debug("Refreshing the merge status of the repository.")
1169 log.debug("Refreshing the merge status of the repository.")
1159 merge_state = self._refresh_merge_state(
1170 merge_state = self._refresh_merge_state(
1160 pull_request, target_vcs, target_ref)
1171 pull_request, target_vcs, target_ref)
1161 else:
1172 else:
1162 possible = pull_request.\
1173 possible = pull_request.\
1163 _last_merge_status == MergeFailureReason.NONE
1174 _last_merge_status == MergeFailureReason.NONE
1164 merge_state = MergeResponse(
1175 merge_state = MergeResponse(
1165 possible, False, None, pull_request._last_merge_status)
1176 possible, False, None, pull_request._last_merge_status)
1166
1177
1167 return merge_state
1178 return merge_state
1168
1179
1169 def _refresh_reference(self, reference, vcs_repository):
1180 def _refresh_reference(self, reference, vcs_repository):
1170 if reference.type in ('branch', 'book'):
1181 if reference.type in ('branch', 'book'):
1171 name_or_id = reference.name
1182 name_or_id = reference.name
1172 else:
1183 else:
1173 name_or_id = reference.commit_id
1184 name_or_id = reference.commit_id
1174 refreshed_commit = vcs_repository.get_commit(name_or_id)
1185 refreshed_commit = vcs_repository.get_commit(name_or_id)
1175 refreshed_reference = Reference(
1186 refreshed_reference = Reference(
1176 reference.type, reference.name, refreshed_commit.raw_id)
1187 reference.type, reference.name, refreshed_commit.raw_id)
1177 return refreshed_reference
1188 return refreshed_reference
1178
1189
1179 def _needs_merge_state_refresh(self, pull_request, target_reference):
1190 def _needs_merge_state_refresh(self, pull_request, target_reference):
1180 return not(
1191 return not(
1181 pull_request.revisions and
1192 pull_request.revisions and
1182 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1193 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1183 target_reference.commit_id == pull_request._last_merge_target_rev)
1194 target_reference.commit_id == pull_request._last_merge_target_rev)
1184
1195
1185 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1196 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1186 workspace_id = self._workspace_id(pull_request)
1197 workspace_id = self._workspace_id(pull_request)
1187 source_vcs = pull_request.source_repo.scm_instance()
1198 source_vcs = pull_request.source_repo.scm_instance()
1188 use_rebase = self._use_rebase_for_merging(pull_request)
1199 use_rebase = self._use_rebase_for_merging(pull_request)
1189 merge_state = target_vcs.merge(
1200 merge_state = target_vcs.merge(
1190 target_reference, source_vcs, pull_request.source_ref_parts,
1201 target_reference, source_vcs, pull_request.source_ref_parts,
1191 workspace_id, dry_run=True, use_rebase=use_rebase)
1202 workspace_id, dry_run=True, use_rebase=use_rebase)
1192
1203
1193 # Do not store the response if there was an unknown error.
1204 # Do not store the response if there was an unknown error.
1194 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1205 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1195 pull_request._last_merge_source_rev = \
1206 pull_request._last_merge_source_rev = \
1196 pull_request.source_ref_parts.commit_id
1207 pull_request.source_ref_parts.commit_id
1197 pull_request._last_merge_target_rev = target_reference.commit_id
1208 pull_request._last_merge_target_rev = target_reference.commit_id
1198 pull_request._last_merge_status = merge_state.failure_reason
1209 pull_request._last_merge_status = merge_state.failure_reason
1199 pull_request.shadow_merge_ref = merge_state.merge_ref
1210 pull_request.shadow_merge_ref = merge_state.merge_ref
1200 Session().add(pull_request)
1211 Session().add(pull_request)
1201 Session().commit()
1212 Session().commit()
1202
1213
1203 return merge_state
1214 return merge_state
1204
1215
1205 def _workspace_id(self, pull_request):
1216 def _workspace_id(self, pull_request):
1206 workspace_id = 'pr-%s' % pull_request.pull_request_id
1217 workspace_id = 'pr-%s' % pull_request.pull_request_id
1207 return workspace_id
1218 return workspace_id
1208
1219
1209 def merge_status_message(self, status_code):
1220 def merge_status_message(self, status_code):
1210 """
1221 """
1211 Return a human friendly error message for the given merge status code.
1222 Return a human friendly error message for the given merge status code.
1212 """
1223 """
1213 return self.MERGE_STATUS_MESSAGES[status_code]
1224 return self.MERGE_STATUS_MESSAGES[status_code]
1214
1225
1215 def generate_repo_data(self, repo, commit_id=None, branch=None,
1226 def generate_repo_data(self, repo, commit_id=None, branch=None,
1216 bookmark=None):
1227 bookmark=None):
1217 all_refs, selected_ref = \
1228 all_refs, selected_ref = \
1218 self._get_repo_pullrequest_sources(
1229 self._get_repo_pullrequest_sources(
1219 repo.scm_instance(), commit_id=commit_id,
1230 repo.scm_instance(), commit_id=commit_id,
1220 branch=branch, bookmark=bookmark)
1231 branch=branch, bookmark=bookmark)
1221
1232
1222 refs_select2 = []
1233 refs_select2 = []
1223 for element in all_refs:
1234 for element in all_refs:
1224 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1235 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1225 refs_select2.append({'text': element[1], 'children': children})
1236 refs_select2.append({'text': element[1], 'children': children})
1226
1237
1227 return {
1238 return {
1228 'user': {
1239 'user': {
1229 'user_id': repo.user.user_id,
1240 'user_id': repo.user.user_id,
1230 'username': repo.user.username,
1241 'username': repo.user.username,
1231 'firstname': repo.user.firstname,
1242 'firstname': repo.user.firstname,
1232 'lastname': repo.user.lastname,
1243 'lastname': repo.user.lastname,
1233 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1244 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1234 },
1245 },
1235 'description': h.chop_at_smart(repo.description, '\n'),
1246 'description': h.chop_at_smart(repo.description, '\n'),
1236 'refs': {
1247 'refs': {
1237 'all_refs': all_refs,
1248 'all_refs': all_refs,
1238 'selected_ref': selected_ref,
1249 'selected_ref': selected_ref,
1239 'select2_refs': refs_select2
1250 'select2_refs': refs_select2
1240 }
1251 }
1241 }
1252 }
1242
1253
1243 def generate_pullrequest_title(self, source, source_ref, target):
1254 def generate_pullrequest_title(self, source, source_ref, target):
1244 return u'{source}#{at_ref} to {target}'.format(
1255 return u'{source}#{at_ref} to {target}'.format(
1245 source=source,
1256 source=source,
1246 at_ref=source_ref,
1257 at_ref=source_ref,
1247 target=target,
1258 target=target,
1248 )
1259 )
1249
1260
1250 def _cleanup_merge_workspace(self, pull_request):
1261 def _cleanup_merge_workspace(self, pull_request):
1251 # Merging related cleanup
1262 # Merging related cleanup
1252 target_scm = pull_request.target_repo.scm_instance()
1263 target_scm = pull_request.target_repo.scm_instance()
1253 workspace_id = 'pr-%s' % pull_request.pull_request_id
1264 workspace_id = 'pr-%s' % pull_request.pull_request_id
1254
1265
1255 try:
1266 try:
1256 target_scm.cleanup_merge_workspace(workspace_id)
1267 target_scm.cleanup_merge_workspace(workspace_id)
1257 except NotImplementedError:
1268 except NotImplementedError:
1258 pass
1269 pass
1259
1270
1260 def _get_repo_pullrequest_sources(
1271 def _get_repo_pullrequest_sources(
1261 self, repo, commit_id=None, branch=None, bookmark=None):
1272 self, repo, commit_id=None, branch=None, bookmark=None):
1262 """
1273 """
1263 Return a structure with repo's interesting commits, suitable for
1274 Return a structure with repo's interesting commits, suitable for
1264 the selectors in pullrequest controller
1275 the selectors in pullrequest controller
1265
1276
1266 :param commit_id: a commit that must be in the list somehow
1277 :param commit_id: a commit that must be in the list somehow
1267 and selected by default
1278 and selected by default
1268 :param branch: a branch that must be in the list and selected
1279 :param branch: a branch that must be in the list and selected
1269 by default - even if closed
1280 by default - even if closed
1270 :param bookmark: a bookmark that must be in the list and selected
1281 :param bookmark: a bookmark that must be in the list and selected
1271 """
1282 """
1272
1283
1273 commit_id = safe_str(commit_id) if commit_id else None
1284 commit_id = safe_str(commit_id) if commit_id else None
1274 branch = safe_str(branch) if branch else None
1285 branch = safe_str(branch) if branch else None
1275 bookmark = safe_str(bookmark) if bookmark else None
1286 bookmark = safe_str(bookmark) if bookmark else None
1276
1287
1277 selected = None
1288 selected = None
1278
1289
1279 # order matters: first source that has commit_id in it will be selected
1290 # order matters: first source that has commit_id in it will be selected
1280 sources = []
1291 sources = []
1281 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1292 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1282 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1293 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1283
1294
1284 if commit_id:
1295 if commit_id:
1285 ref_commit = (h.short_id(commit_id), commit_id)
1296 ref_commit = (h.short_id(commit_id), commit_id)
1286 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1297 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1287
1298
1288 sources.append(
1299 sources.append(
1289 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1300 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1290 )
1301 )
1291
1302
1292 groups = []
1303 groups = []
1293 for group_key, ref_list, group_name, match in sources:
1304 for group_key, ref_list, group_name, match in sources:
1294 group_refs = []
1305 group_refs = []
1295 for ref_name, ref_id in ref_list:
1306 for ref_name, ref_id in ref_list:
1296 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1307 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1297 group_refs.append((ref_key, ref_name))
1308 group_refs.append((ref_key, ref_name))
1298
1309
1299 if not selected:
1310 if not selected:
1300 if set([commit_id, match]) & set([ref_id, ref_name]):
1311 if set([commit_id, match]) & set([ref_id, ref_name]):
1301 selected = ref_key
1312 selected = ref_key
1302
1313
1303 if group_refs:
1314 if group_refs:
1304 groups.append((group_refs, group_name))
1315 groups.append((group_refs, group_name))
1305
1316
1306 if not selected:
1317 if not selected:
1307 ref = commit_id or branch or bookmark
1318 ref = commit_id or branch or bookmark
1308 if ref:
1319 if ref:
1309 raise CommitDoesNotExistError(
1320 raise CommitDoesNotExistError(
1310 'No commit refs could be found matching: %s' % ref)
1321 'No commit refs could be found matching: %s' % ref)
1311 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1322 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1312 selected = 'branch:%s:%s' % (
1323 selected = 'branch:%s:%s' % (
1313 repo.DEFAULT_BRANCH_NAME,
1324 repo.DEFAULT_BRANCH_NAME,
1314 repo.branches[repo.DEFAULT_BRANCH_NAME]
1325 repo.branches[repo.DEFAULT_BRANCH_NAME]
1315 )
1326 )
1316 elif repo.commit_ids:
1327 elif repo.commit_ids:
1317 rev = repo.commit_ids[0]
1328 rev = repo.commit_ids[0]
1318 selected = 'rev:%s:%s' % (rev, rev)
1329 selected = 'rev:%s:%s' % (rev, rev)
1319 else:
1330 else:
1320 raise EmptyRepositoryError()
1331 raise EmptyRepositoryError()
1321 return groups, selected
1332 return groups, selected
1322
1333
1323 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1334 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1324 return self._get_diff_from_pr_or_version(
1335 return self._get_diff_from_pr_or_version(
1325 source_repo, source_ref_id, target_ref_id, context=context)
1336 source_repo, source_ref_id, target_ref_id, context=context)
1326
1337
1327 def _get_diff_from_pr_or_version(
1338 def _get_diff_from_pr_or_version(
1328 self, source_repo, source_ref_id, target_ref_id, context):
1339 self, source_repo, source_ref_id, target_ref_id, context):
1329 target_commit = source_repo.get_commit(
1340 target_commit = source_repo.get_commit(
1330 commit_id=safe_str(target_ref_id))
1341 commit_id=safe_str(target_ref_id))
1331 source_commit = source_repo.get_commit(
1342 source_commit = source_repo.get_commit(
1332 commit_id=safe_str(source_ref_id))
1343 commit_id=safe_str(source_ref_id))
1333 if isinstance(source_repo, Repository):
1344 if isinstance(source_repo, Repository):
1334 vcs_repo = source_repo.scm_instance()
1345 vcs_repo = source_repo.scm_instance()
1335 else:
1346 else:
1336 vcs_repo = source_repo
1347 vcs_repo = source_repo
1337
1348
1338 # TODO: johbo: In the context of an update, we cannot reach
1349 # TODO: johbo: In the context of an update, we cannot reach
1339 # the old commit anymore with our normal mechanisms. It needs
1350 # the old commit anymore with our normal mechanisms. It needs
1340 # some sort of special support in the vcs layer to avoid this
1351 # some sort of special support in the vcs layer to avoid this
1341 # workaround.
1352 # workaround.
1342 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1353 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1343 vcs_repo.alias == 'git'):
1354 vcs_repo.alias == 'git'):
1344 source_commit.raw_id = safe_str(source_ref_id)
1355 source_commit.raw_id = safe_str(source_ref_id)
1345
1356
1346 log.debug('calculating diff between '
1357 log.debug('calculating diff between '
1347 'source_ref:%s and target_ref:%s for repo `%s`',
1358 'source_ref:%s and target_ref:%s for repo `%s`',
1348 target_ref_id, source_ref_id,
1359 target_ref_id, source_ref_id,
1349 safe_unicode(vcs_repo.path))
1360 safe_unicode(vcs_repo.path))
1350
1361
1351 vcs_diff = vcs_repo.get_diff(
1362 vcs_diff = vcs_repo.get_diff(
1352 commit1=target_commit, commit2=source_commit, context=context)
1363 commit1=target_commit, commit2=source_commit, context=context)
1353 return vcs_diff
1364 return vcs_diff
1354
1365
1355 def _is_merge_enabled(self, pull_request):
1366 def _is_merge_enabled(self, pull_request):
1356 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1367 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1357 settings = settings_model.get_general_settings()
1368 settings = settings_model.get_general_settings()
1358 return settings.get('rhodecode_pr_merge_enabled', False)
1369 return settings.get('rhodecode_pr_merge_enabled', False)
1359
1370
1360 def _use_rebase_for_merging(self, pull_request):
1371 def _use_rebase_for_merging(self, pull_request):
1361 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1372 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1362 settings = settings_model.get_general_settings()
1373 settings = settings_model.get_general_settings()
1363 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1374 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1364
1375
1365 def _log_action(self, action, user, pull_request):
1376 def _log_action(self, action, user, pull_request):
1366 action_logger(
1377 action_logger(
1367 user,
1378 user,
1368 '{action}:{pr_id}'.format(
1379 '{action}:{pr_id}'.format(
1369 action=action, pr_id=pull_request.pull_request_id),
1380 action=action, pr_id=pull_request.pull_request_id),
1370 pull_request.target_repo)
1381 pull_request.target_repo)
1371
1382
1383 def get_reviewer_functions(self):
1384 """
1385 Fetches functions for validation and fetching default reviewers.
1386 If available we use the EE package, else we fallback to CE
1387 package functions
1388 """
1389 try:
1390 from rc_reviewers.utils import get_default_reviewers_data
1391 from rc_reviewers.utils import validate_default_reviewers
1392 except ImportError:
1393 from rhodecode.apps.repository.utils import \
1394 get_default_reviewers_data
1395 from rhodecode.apps.repository.utils import \
1396 validate_default_reviewers
1397
1398 return get_default_reviewers_data, validate_default_reviewers
1399
1372
1400
1373 class MergeCheck(object):
1401 class MergeCheck(object):
1374 """
1402 """
1375 Perform Merge Checks and returns a check object which stores information
1403 Perform Merge Checks and returns a check object which stores information
1376 about merge errors, and merge conditions
1404 about merge errors, and merge conditions
1377 """
1405 """
1378 TODO_CHECK = 'todo'
1406 TODO_CHECK = 'todo'
1379 PERM_CHECK = 'perm'
1407 PERM_CHECK = 'perm'
1380 REVIEW_CHECK = 'review'
1408 REVIEW_CHECK = 'review'
1381 MERGE_CHECK = 'merge'
1409 MERGE_CHECK = 'merge'
1382
1410
1383 def __init__(self):
1411 def __init__(self):
1384 self.review_status = None
1412 self.review_status = None
1385 self.merge_possible = None
1413 self.merge_possible = None
1386 self.merge_msg = ''
1414 self.merge_msg = ''
1387 self.failed = None
1415 self.failed = None
1388 self.errors = []
1416 self.errors = []
1389 self.error_details = OrderedDict()
1417 self.error_details = OrderedDict()
1390
1418
1391 def push_error(self, error_type, message, error_key, details):
1419 def push_error(self, error_type, message, error_key, details):
1392 self.failed = True
1420 self.failed = True
1393 self.errors.append([error_type, message])
1421 self.errors.append([error_type, message])
1394 self.error_details[error_key] = dict(
1422 self.error_details[error_key] = dict(
1395 details=details,
1423 details=details,
1396 error_type=error_type,
1424 error_type=error_type,
1397 message=message
1425 message=message
1398 )
1426 )
1399
1427
1400 @classmethod
1428 @classmethod
1401 def validate(cls, pull_request, user, fail_early=False, translator=None):
1429 def validate(cls, pull_request, user, fail_early=False, translator=None):
1402 # if migrated to pyramid...
1430 # if migrated to pyramid...
1403 # _ = lambda: translator or _ # use passed in translator if any
1431 # _ = lambda: translator or _ # use passed in translator if any
1404
1432
1405 merge_check = cls()
1433 merge_check = cls()
1406
1434
1407 # permissions to merge
1435 # permissions to merge
1408 user_allowed_to_merge = PullRequestModel().check_user_merge(
1436 user_allowed_to_merge = PullRequestModel().check_user_merge(
1409 pull_request, user)
1437 pull_request, user)
1410 if not user_allowed_to_merge:
1438 if not user_allowed_to_merge:
1411 log.debug("MergeCheck: cannot merge, approval is pending.")
1439 log.debug("MergeCheck: cannot merge, approval is pending.")
1412
1440
1413 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1441 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1414 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1442 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1415 if fail_early:
1443 if fail_early:
1416 return merge_check
1444 return merge_check
1417
1445
1418 # review status, must be always present
1446 # review status, must be always present
1419 review_status = pull_request.calculated_review_status()
1447 review_status = pull_request.calculated_review_status()
1420 merge_check.review_status = review_status
1448 merge_check.review_status = review_status
1421
1449
1422 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1450 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1423 if not status_approved:
1451 if not status_approved:
1424 log.debug("MergeCheck: cannot merge, approval is pending.")
1452 log.debug("MergeCheck: cannot merge, approval is pending.")
1425
1453
1426 msg = _('Pull request reviewer approval is pending.')
1454 msg = _('Pull request reviewer approval is pending.')
1427
1455
1428 merge_check.push_error(
1456 merge_check.push_error(
1429 'warning', msg, cls.REVIEW_CHECK, review_status)
1457 'warning', msg, cls.REVIEW_CHECK, review_status)
1430
1458
1431 if fail_early:
1459 if fail_early:
1432 return merge_check
1460 return merge_check
1433
1461
1434 # left over TODOs
1462 # left over TODOs
1435 todos = CommentsModel().get_unresolved_todos(pull_request)
1463 todos = CommentsModel().get_unresolved_todos(pull_request)
1436 if todos:
1464 if todos:
1437 log.debug("MergeCheck: cannot merge, {} "
1465 log.debug("MergeCheck: cannot merge, {} "
1438 "unresolved todos left.".format(len(todos)))
1466 "unresolved todos left.".format(len(todos)))
1439
1467
1440 if len(todos) == 1:
1468 if len(todos) == 1:
1441 msg = _('Cannot merge, {} TODO still not resolved.').format(
1469 msg = _('Cannot merge, {} TODO still not resolved.').format(
1442 len(todos))
1470 len(todos))
1443 else:
1471 else:
1444 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1472 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1445 len(todos))
1473 len(todos))
1446
1474
1447 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1475 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1448
1476
1449 if fail_early:
1477 if fail_early:
1450 return merge_check
1478 return merge_check
1451
1479
1452 # merge possible
1480 # merge possible
1453 merge_status, msg = PullRequestModel().merge_status(pull_request)
1481 merge_status, msg = PullRequestModel().merge_status(pull_request)
1454 merge_check.merge_possible = merge_status
1482 merge_check.merge_possible = merge_status
1455 merge_check.merge_msg = msg
1483 merge_check.merge_msg = msg
1456 if not merge_status:
1484 if not merge_status:
1457 log.debug(
1485 log.debug(
1458 "MergeCheck: cannot merge, pull request merge not possible.")
1486 "MergeCheck: cannot merge, pull request merge not possible.")
1459 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1487 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1460
1488
1461 if fail_early:
1489 if fail_early:
1462 return merge_check
1490 return merge_check
1463
1491
1464 return merge_check
1492 return merge_check
1465
1493
1466
1494
1467 ChangeTuple = namedtuple('ChangeTuple',
1495 ChangeTuple = namedtuple('ChangeTuple',
1468 ['added', 'common', 'removed', 'total'])
1496 ['added', 'common', 'removed', 'total'])
1469
1497
1470 FileChangeTuple = namedtuple('FileChangeTuple',
1498 FileChangeTuple = namedtuple('FileChangeTuple',
1471 ['added', 'modified', 'removed'])
1499 ['added', 'modified', 'removed'])
@@ -1,196 +1,196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22
22
23 import colander
23 import colander
24 from rhodecode.model.validation_schema import preparers
24 from rhodecode.model.validation_schema import preparers
25 from rhodecode.model.db import User, UserGroup
25 from rhodecode.model.db import User, UserGroup
26
26
27
27
28 class _RootLocation(object):
28 class _RootLocation(object):
29 pass
29 pass
30
30
31 RootLocation = _RootLocation()
31 RootLocation = _RootLocation()
32
32
33
33
34 def _normalize(seperator, path):
34 def _normalize(seperator, path):
35
35
36 if not path:
36 if not path:
37 return ''
37 return ''
38 elif path is colander.null:
38 elif path is colander.null:
39 return colander.null
39 return colander.null
40
40
41 parts = path.split(seperator)
41 parts = path.split(seperator)
42
42
43 def bad_parts(value):
43 def bad_parts(value):
44 if not value:
44 if not value:
45 return False
45 return False
46 if re.match(r'^[.]+$', value):
46 if re.match(r'^[.]+$', value):
47 return False
47 return False
48
48
49 return True
49 return True
50
50
51 def slugify(value):
51 def slugify(value):
52 value = preparers.slugify_preparer(value)
52 value = preparers.slugify_preparer(value)
53 value = re.sub(r'[.]{2,}', '.', value)
53 value = re.sub(r'[.]{2,}', '.', value)
54 return value
54 return value
55
55
56 clean_parts = [slugify(item) for item in parts if item]
56 clean_parts = [slugify(item) for item in parts if item]
57 path = filter(bad_parts, clean_parts)
57 path = filter(bad_parts, clean_parts)
58 return seperator.join(path)
58 return seperator.join(path)
59
59
60
60
61 class RepoNameType(colander.String):
61 class RepoNameType(colander.String):
62 SEPARATOR = '/'
62 SEPARATOR = '/'
63
63
64 def deserialize(self, node, cstruct):
64 def deserialize(self, node, cstruct):
65 result = super(RepoNameType, self).deserialize(node, cstruct)
65 result = super(RepoNameType, self).deserialize(node, cstruct)
66 if cstruct is colander.null:
66 if cstruct is colander.null:
67 return colander.null
67 return colander.null
68 return self._normalize(result)
68 return self._normalize(result)
69
69
70 def _normalize(self, path):
70 def _normalize(self, path):
71 return _normalize(self.SEPARATOR, path)
71 return _normalize(self.SEPARATOR, path)
72
72
73
73
74 class GroupNameType(colander.String):
74 class GroupNameType(colander.String):
75 SEPARATOR = '/'
75 SEPARATOR = '/'
76
76
77 def deserialize(self, node, cstruct):
77 def deserialize(self, node, cstruct):
78 if cstruct is RootLocation:
78 if cstruct is RootLocation:
79 return cstruct
79 return cstruct
80
80
81 result = super(GroupNameType, self).deserialize(node, cstruct)
81 result = super(GroupNameType, self).deserialize(node, cstruct)
82 if cstruct is colander.null:
82 if cstruct is colander.null:
83 return colander.null
83 return colander.null
84 return self._normalize(result)
84 return self._normalize(result)
85
85
86 def _normalize(self, path):
86 def _normalize(self, path):
87 return _normalize(self.SEPARATOR, path)
87 return _normalize(self.SEPARATOR, path)
88
88
89
89
90 class StringBooleanType(colander.String):
90 class StringBooleanType(colander.String):
91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
91 true_values = ['true', 't', 'yes', 'y', 'on', '1']
92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
92 false_values = ['false', 'f', 'no', 'n', 'off', '0']
93
93
94 def serialize(self, node, appstruct):
94 def serialize(self, node, appstruct):
95 if appstruct is colander.null:
95 if appstruct is colander.null:
96 return colander.null
96 return colander.null
97 if not isinstance(appstruct, bool):
97 if not isinstance(appstruct, bool):
98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
98 raise colander.Invalid(node, '%r is not a boolean' % appstruct)
99
99
100 return appstruct and 'true' or 'false'
100 return appstruct and 'true' or 'false'
101
101
102 def deserialize(self, node, cstruct):
102 def deserialize(self, node, cstruct):
103 if cstruct is colander.null:
103 if cstruct is colander.null:
104 return colander.null
104 return colander.null
105
105
106 if isinstance(cstruct, bool):
106 if isinstance(cstruct, bool):
107 return cstruct
107 return cstruct
108
108
109 if not isinstance(cstruct, basestring):
109 if not isinstance(cstruct, basestring):
110 raise colander.Invalid(node, '%r is not a string' % cstruct)
110 raise colander.Invalid(node, '%r is not a string' % cstruct)
111
111
112 value = cstruct.lower()
112 value = cstruct.lower()
113 if value in self.true_values:
113 if value in self.true_values:
114 return True
114 return True
115 elif value in self.false_values:
115 elif value in self.false_values:
116 return False
116 return False
117 else:
117 else:
118 raise colander.Invalid(
118 raise colander.Invalid(
119 node, '{} value cannot be translated to bool'.format(value))
119 node, '{} value cannot be translated to bool'.format(value))
120
120
121
121
122 class UserOrUserGroupType(colander.SchemaType):
122 class UserOrUserGroupType(colander.SchemaType):
123 """ colander Schema type for valid rhodecode user and/or usergroup """
123 """ colander Schema type for valid rhodecode user and/or usergroup """
124 scopes = ('user', 'usergroup')
124 scopes = ('user', 'usergroup')
125
125
126 def __init__(self):
126 def __init__(self):
127 self.users = 'user' in self.scopes
127 self.users = 'user' in self.scopes
128 self.usergroups = 'usergroup' in self.scopes
128 self.usergroups = 'usergroup' in self.scopes
129
129
130 def serialize(self, node, appstruct):
130 def serialize(self, node, appstruct):
131 if appstruct is colander.null:
131 if appstruct is colander.null:
132 return colander.null
132 return colander.null
133
133
134 if self.users:
134 if self.users:
135 if isinstance(appstruct, User):
135 if isinstance(appstruct, User):
136 if self.usergroups:
136 if self.usergroups:
137 return 'user:%s' % appstruct.username
137 return 'user:%s' % appstruct.username
138 return appstruct.username
138 return appstruct.username
139
139
140 if self.usergroups:
140 if self.usergroups:
141 if isinstance(appstruct, UserGroup):
141 if isinstance(appstruct, UserGroup):
142 if self.users:
142 if self.users:
143 return 'usergroup:%s' % appstruct.users_group_name
143 return 'usergroup:%s' % appstruct.users_group_name
144 return appstruct.users_group_name
144 return appstruct.users_group_name
145
145
146 raise colander.Invalid(
146 raise colander.Invalid(
147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
147 node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
148
148
149 def deserialize(self, node, cstruct):
149 def deserialize(self, node, cstruct):
150 if cstruct is colander.null:
150 if cstruct is colander.null:
151 return colander.null
151 return colander.null
152
152
153 user, usergroup = None, None
153 user, usergroup = None, None
154 if self.users:
154 if self.users:
155 if cstruct.startswith('user:'):
155 if cstruct.startswith('user:'):
156 user = User.get_by_username(cstruct.split(':')[1])
156 user = User.get_by_username(cstruct.split(':')[1])
157 else:
157 else:
158 user = User.get_by_username(cstruct)
158 user = User.get_by_username(cstruct)
159
159
160 if self.usergroups:
160 if self.usergroups:
161 if cstruct.startswith('usergroup:'):
161 if cstruct.startswith('usergroup:'):
162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
162 usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
163 else:
163 else:
164 usergroup = UserGroup.get_by_group_name(cstruct)
164 usergroup = UserGroup.get_by_group_name(cstruct)
165
165
166 if self.users and self.usergroups:
166 if self.users and self.usergroups:
167 if user and usergroup:
167 if user and usergroup:
168 raise colander.Invalid(node, (
168 raise colander.Invalid(node, (
169 '%s is both a user and usergroup, specify which '
169 '%s is both a user and usergroup, specify which '
170 'one was wanted by prepending user: or usergroup: to the '
170 'one was wanted by prepending user: or usergroup: to the '
171 'name') % cstruct)
171 'name') % cstruct)
172
172
173 if self.users and user:
173 if self.users and user:
174 return user
174 return user
175
175
176 if self.usergroups and usergroup:
176 if self.usergroups and usergroup:
177 return usergroup
177 return usergroup
178
178
179 raise colander.Invalid(
179 raise colander.Invalid(
180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
180 node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
181
181
182
182
183 class UserType(UserOrUserGroupType):
183 class UserType(UserOrUserGroupType):
184 scopes = ('user',)
184 scopes = ('user',)
185
185
186
186
187 class UserGroupType(UserOrUserGroupType):
187 class UserGroupType(UserOrUserGroupType):
188 scopes = ('usergroup',)
188 scopes = ('usergroup',)
189
189
190
190
191 class StrOrIntType(colander.String):
191 class StrOrIntType(colander.String):
192 def deserialize(self, node, cstruct):
192 def deserialize(self, node, cstruct):
193 if isinstance(node, basestring):
193 if isinstance(cstruct, basestring):
194 return super(StrOrIntType, self).deserialize(node, cstruct)
194 return super(StrOrIntType, self).deserialize(node, cstruct)
195 else:
195 else:
196 return colander.Integer().deserialize(node, cstruct)
196 return colander.Integer().deserialize(node, cstruct)
@@ -1,408 +1,418 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey4;
17 color: @grey4;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey4 );
21 .border ( @border-thickness-buttons, @grey4 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29
29
30 a {
30 a {
31 display: block;
31 display: block;
32 margin: 0;
32 margin: 0;
33 padding: 0;
33 padding: 0;
34 color: inherit;
34 color: inherit;
35 text-decoration: none;
35 text-decoration: none;
36
36
37 &:hover {
37 &:hover {
38 text-decoration: none;
38 text-decoration: none;
39 }
39 }
40 }
40 }
41
41
42 &:focus,
42 &:focus,
43 &:active {
43 &:active {
44 outline:none;
44 outline:none;
45 }
45 }
46 &:hover {
46 &:hover {
47 color: white;
47 color: white;
48 background-color: @grey4;
48 background-color: @grey4;
49 }
49 }
50
50
51 .icon-remove-sign {
51 .icon-remove-sign {
52 display: none;
52 display: none;
53 }
53 }
54
54
55 //disabled buttons
55 //disabled buttons
56 //last; overrides any other styles
56 //last; overrides any other styles
57 &:disabled {
57 &:disabled {
58 opacity: .7;
58 opacity: .7;
59 cursor: auto;
59 cursor: auto;
60 background-color: white;
60 background-color: white;
61 color: @grey4;
61 color: @grey4;
62 text-shadow: none;
62 text-shadow: none;
63 }
63 }
64
64
65 &.no-margin {
66 margin: 0 0 0 0;
67 }
68
65 }
69 }
66
70
67
71
68 .btn-default {
72 .btn-default {
69 .border ( @border-thickness-buttons, @rcblue );
73 .border ( @border-thickness-buttons, @rcblue );
70 background-image: none;
74 background-image: none;
71 color: @rcblue;
75 color: @rcblue;
72
76
73 a {
77 a {
74 color: @rcblue;
78 color: @rcblue;
75 }
79 }
76
80
77 &:hover,
81 &:hover,
78 &.active {
82 &.active {
79 color: white;
83 color: white;
80 background-color: @rcdarkblue;
84 background-color: @rcdarkblue;
81 .border ( @border-thickness, @rcdarkblue );
85 .border ( @border-thickness, @rcdarkblue );
82
86
83 a {
87 a {
84 color: white;
88 color: white;
85 }
89 }
86 }
90 }
87 &:disabled {
91 &:disabled {
88 .border ( @border-thickness-buttons, @grey4 );
92 .border ( @border-thickness-buttons, @grey4 );
89 background-color: transparent;
93 background-color: transparent;
90 }
94 }
91 }
95 }
92
96
93 .btn-primary,
97 .btn-primary,
94 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
98 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
95 .btn-success {
99 .btn-success {
96 .border ( @border-thickness, @rcblue );
100 .border ( @border-thickness, @rcblue );
97 background-color: @rcblue;
101 background-color: @rcblue;
98 color: white;
102 color: white;
99
103
100 a {
104 a {
101 color: white;
105 color: white;
102 }
106 }
103
107
104 &:hover,
108 &:hover,
105 &.active {
109 &.active {
106 .border ( @border-thickness, @rcdarkblue );
110 .border ( @border-thickness, @rcdarkblue );
107 color: white;
111 color: white;
108 background-color: @rcdarkblue;
112 background-color: @rcdarkblue;
109
113
110 a {
114 a {
111 color: white;
115 color: white;
112 }
116 }
113 }
117 }
114 &:disabled {
118 &:disabled {
115 background-color: @rcblue;
119 background-color: @rcblue;
116 }
120 }
117 }
121 }
118
122
119 .btn-secondary {
123 .btn-secondary {
120 &:extend(.btn-default);
124 &:extend(.btn-default);
121
125
122 background-color: white;
126 background-color: white;
123
127
124 &:focus {
128 &:focus {
125 outline: 0;
129 outline: 0;
126 }
130 }
127
131
128 &:hover {
132 &:hover {
129 &:extend(.btn-default:hover);
133 &:extend(.btn-default:hover);
130 }
134 }
131
135
132 &.btn-link {
136 &.btn-link {
133 &:extend(.btn-link);
137 &:extend(.btn-link);
134 color: @rcblue;
138 color: @rcblue;
135 }
139 }
136
140
137 &:disabled {
141 &:disabled {
138 color: @rcblue;
142 color: @rcblue;
139 background-color: white;
143 background-color: white;
140 }
144 }
141 }
145 }
142
146
143 .btn-warning,
147 .btn-warning,
144 .btn-danger,
148 .btn-danger,
145 .revoke_perm,
149 .revoke_perm,
146 .btn-x,
150 .btn-x,
147 .form .action_button.btn-x {
151 .form .action_button.btn-x {
148 .border ( @border-thickness, @alert2 );
152 .border ( @border-thickness, @alert2 );
149 background-color: white;
153 background-color: white;
150 color: @alert2;
154 color: @alert2;
151
155
152 a {
156 a {
153 color: @alert2;
157 color: @alert2;
154 }
158 }
155
159
156 &:hover,
160 &:hover,
157 &.active {
161 &.active {
158 .border ( @border-thickness, @alert2 );
162 .border ( @border-thickness, @alert2 );
159 color: white;
163 color: white;
160 background-color: @alert2;
164 background-color: @alert2;
161
165
162 a {
166 a {
163 color: white;
167 color: white;
164 }
168 }
165 }
169 }
166
170
167 i {
171 i {
168 display:none;
172 display:none;
169 }
173 }
170
174
171 &:disabled {
175 &:disabled {
172 background-color: white;
176 background-color: white;
173 color: @alert2;
177 color: @alert2;
174 }
178 }
175 }
179 }
176
180
177 .btn-approved-status {
181 .btn-approved-status {
178 .border ( @border-thickness, @alert1 );
182 .border ( @border-thickness, @alert1 );
179 background-color: white;
183 background-color: white;
180 color: @alert1;
184 color: @alert1;
181
185
182 }
186 }
183
187
184 .btn-rejected-status {
188 .btn-rejected-status {
185 .border ( @border-thickness, @alert2 );
189 .border ( @border-thickness, @alert2 );
186 background-color: white;
190 background-color: white;
187 color: @alert2;
191 color: @alert2;
188 }
192 }
189
193
190 .btn-sm,
194 .btn-sm,
191 .btn-mini,
195 .btn-mini,
192 .field-sm .btn {
196 .field-sm .btn {
193 padding: @padding/3;
197 padding: @padding/3;
194 }
198 }
195
199
196 .btn-xs {
200 .btn-xs {
197 padding: @padding/4;
201 padding: @padding/4;
198 }
202 }
199
203
200 .btn-lg {
204 .btn-lg {
201 padding: @padding * 1.2;
205 padding: @padding * 1.2;
202 }
206 }
203
207
204 .btn-group {
208 .btn-group {
205 display: inline-block;
209 display: inline-block;
206 .btn {
210 .btn {
207 float: left;
211 float: left;
208 margin: 0 0 0 -1px;
212 margin: 0 0 0 -1px;
209 }
213 }
210 }
214 }
211
215
212 .btn-link {
216 .btn-link {
213 background: transparent;
217 background: transparent;
214 border: none;
218 border: none;
215 padding: 0;
219 padding: 0;
216 color: @rcblue;
220 color: @rcblue;
217
221
218 &:hover {
222 &:hover {
219 background: transparent;
223 background: transparent;
220 border: none;
224 border: none;
221 color: @rcdarkblue;
225 color: @rcdarkblue;
222 }
226 }
223
227
224 //disabled buttons
228 //disabled buttons
225 //last; overrides any other styles
229 //last; overrides any other styles
226 &:disabled {
230 &:disabled {
227 opacity: .7;
231 opacity: .7;
228 cursor: auto;
232 cursor: auto;
229 background-color: white;
233 background-color: white;
230 color: @grey4;
234 color: @grey4;
231 text-shadow: none;
235 text-shadow: none;
232 }
236 }
233
237
234 // TODO: johbo: Check if we can avoid this, indicates that the structure
238 // TODO: johbo: Check if we can avoid this, indicates that the structure
235 // is not yet good.
239 // is not yet good.
236 // lisa: The button CSS reflects the button HTML; both need a cleanup.
240 // lisa: The button CSS reflects the button HTML; both need a cleanup.
237 &.btn-danger {
241 &.btn-danger {
238 color: @alert2;
242 color: @alert2;
239
243
240 &:hover {
244 &:hover {
241 color: darken(@alert2,30%);
245 color: darken(@alert2,30%);
242 }
246 }
243
247
244 &:disabled {
248 &:disabled {
245 color: @alert2;
249 color: @alert2;
246 }
250 }
247 }
251 }
248 }
252 }
249
253
250 .btn-social {
254 .btn-social {
251 &:extend(.btn-default);
255 &:extend(.btn-default);
252 margin: 5px 5px 5px 0px;
256 margin: 5px 5px 5px 0px;
253 min-width: 150px;
257 min-width: 150px;
254 }
258 }
255
259
256 // TODO: johbo: check these exceptions
260 // TODO: johbo: check these exceptions
257
261
258 .links {
262 .links {
259
263
260 .btn + .btn {
264 .btn + .btn {
261 margin-top: @padding;
265 margin-top: @padding;
262 }
266 }
263 }
267 }
264
268
265
269
266 .action_button {
270 .action_button {
267 display:inline;
271 display:inline;
268 margin: 0;
272 margin: 0;
269 padding: 0 1em 0 0;
273 padding: 0 1em 0 0;
270 font-size: inherit;
274 font-size: inherit;
271 color: @rcblue;
275 color: @rcblue;
272 border: none;
276 border: none;
273 .border-radius (0);
277 border-radius: 0;
274 background-color: transparent;
278 background-color: transparent;
275
279
280 &.last-item {
281 border: none;
282 padding: 0 0 0 0;
283 }
284
276 &:last-child {
285 &:last-child {
277 border: none;
286 border: none;
287 padding: 0 0 0 0;
278 }
288 }
279
289
280 &:hover {
290 &:hover {
281 color: @rcdarkblue;
291 color: @rcdarkblue;
282 background-color: transparent;
292 background-color: transparent;
283 border: none;
293 border: none;
284 }
294 }
285 }
295 }
286 .grid_delete {
296 .grid_delete {
287 .action_button {
297 .action_button {
288 border: none;
298 border: none;
289 }
299 }
290 }
300 }
291
301
292
302
293 // TODO: johbo: Form button tweaks, check if we can use the classes instead
303 // TODO: johbo: Form button tweaks, check if we can use the classes instead
294 input[type="submit"] {
304 input[type="submit"] {
295 &:extend(.btn-primary);
305 &:extend(.btn-primary);
296
306
297 &:focus {
307 &:focus {
298 outline: 0;
308 outline: 0;
299 }
309 }
300
310
301 &:hover {
311 &:hover {
302 &:extend(.btn-primary:hover);
312 &:extend(.btn-primary:hover);
303 }
313 }
304
314
305 &.btn-link {
315 &.btn-link {
306 &:extend(.btn-link);
316 &:extend(.btn-link);
307 color: @rcblue;
317 color: @rcblue;
308
318
309 &:disabled {
319 &:disabled {
310 color: @rcblue;
320 color: @rcblue;
311 background-color: transparent;
321 background-color: transparent;
312 }
322 }
313 }
323 }
314
324
315 &:disabled {
325 &:disabled {
316 .border ( @border-thickness-buttons, @rcblue );
326 .border ( @border-thickness-buttons, @rcblue );
317 background-color: @rcblue;
327 background-color: @rcblue;
318 color: white;
328 color: white;
319 }
329 }
320 }
330 }
321
331
322 input[type="reset"] {
332 input[type="reset"] {
323 &:extend(.btn-default);
333 &:extend(.btn-default);
324
334
325 // TODO: johbo: Check if this tweak can be avoided.
335 // TODO: johbo: Check if this tweak can be avoided.
326 background: transparent;
336 background: transparent;
327
337
328 &:focus {
338 &:focus {
329 outline: 0;
339 outline: 0;
330 }
340 }
331
341
332 &:hover {
342 &:hover {
333 &:extend(.btn-default:hover);
343 &:extend(.btn-default:hover);
334 }
344 }
335
345
336 &.btn-link {
346 &.btn-link {
337 &:extend(.btn-link);
347 &:extend(.btn-link);
338 color: @rcblue;
348 color: @rcblue;
339
349
340 &:disabled {
350 &:disabled {
341 border: none;
351 border: none;
342 }
352 }
343 }
353 }
344
354
345 &:disabled {
355 &:disabled {
346 .border ( @border-thickness-buttons, @rcblue );
356 .border ( @border-thickness-buttons, @rcblue );
347 background-color: white;
357 background-color: white;
348 color: @rcblue;
358 color: @rcblue;
349 }
359 }
350 }
360 }
351
361
352 input[type="submit"],
362 input[type="submit"],
353 input[type="reset"] {
363 input[type="reset"] {
354 &.btn-danger {
364 &.btn-danger {
355 &:extend(.btn-danger);
365 &:extend(.btn-danger);
356
366
357 &:focus {
367 &:focus {
358 outline: 0;
368 outline: 0;
359 }
369 }
360
370
361 &:hover {
371 &:hover {
362 &:extend(.btn-danger:hover);
372 &:extend(.btn-danger:hover);
363 }
373 }
364
374
365 &.btn-link {
375 &.btn-link {
366 &:extend(.btn-link);
376 &:extend(.btn-link);
367 color: @alert2;
377 color: @alert2;
368
378
369 &:hover {
379 &:hover {
370 color: darken(@alert2,30%);
380 color: darken(@alert2,30%);
371 }
381 }
372 }
382 }
373
383
374 &:disabled {
384 &:disabled {
375 color: @alert2;
385 color: @alert2;
376 background-color: white;
386 background-color: white;
377 }
387 }
378 }
388 }
379 &.btn-danger-action {
389 &.btn-danger-action {
380 .border ( @border-thickness, @alert2 );
390 .border ( @border-thickness, @alert2 );
381 background-color: @alert2;
391 background-color: @alert2;
382 color: white;
392 color: white;
383
393
384 a {
394 a {
385 color: white;
395 color: white;
386 }
396 }
387
397
388 &:hover {
398 &:hover {
389 background-color: darken(@alert2,20%);
399 background-color: darken(@alert2,20%);
390 }
400 }
391
401
392 &.active {
402 &.active {
393 .border ( @border-thickness, @alert2 );
403 .border ( @border-thickness, @alert2 );
394 color: white;
404 color: white;
395 background-color: @alert2;
405 background-color: @alert2;
396
406
397 a {
407 a {
398 color: white;
408 color: white;
399 }
409 }
400 }
410 }
401
411
402 &:disabled {
412 &:disabled {
403 background-color: white;
413 background-color: white;
404 color: @alert2;
414 color: @alert2;
405 }
415 }
406 }
416 }
407 }
417 }
408
418
@@ -1,139 +1,139 b''
1 .deform {
1 .deform {
2
2
3 * {
3 * {
4 box-sizing: border-box;
4 box-sizing: border-box;
5 }
5 }
6
6
7 .required:after {
7 .required:after {
8 color: #e32;
8 color: #e32;
9 content: '*';
9 content: '*';
10 display:inline;
10 display:inline;
11 }
11 }
12
12
13 .control-label {
13 .control-label {
14 width: 200px;
14 width: 200px;
15 padding: 10px;
15 padding: 10px 0px;
16 float: left;
16 float: left;
17 }
17 }
18 .control-inputs {
18 .control-inputs {
19 width: 400px;
19 width: 400px;
20 float: left;
20 float: left;
21 }
21 }
22 .form-group .radio, .form-group .checkbox {
22 .form-group .radio, .form-group .checkbox {
23 position: relative;
23 position: relative;
24 display: block;
24 display: block;
25 /* margin-bottom: 10px; */
25 /* margin-bottom: 10px; */
26 }
26 }
27
27
28 .form-group {
28 .form-group {
29 clear: left;
29 clear: left;
30 margin-bottom: 20px;
30 margin-bottom: 20px;
31
31
32 &:after { /* clear fix */
32 &:after { /* clear fix */
33 content: " ";
33 content: " ";
34 display: block;
34 display: block;
35 clear: left;
35 clear: left;
36 }
36 }
37 }
37 }
38
38
39 .form-control {
39 .form-control {
40 width: 100%;
40 width: 100%;
41 padding: 0.9em;
41 padding: 0.9em;
42 border: 1px solid #979797;
42 border: 1px solid #979797;
43 border-radius: 2px;
43 border-radius: 2px;
44 }
44 }
45 .form-control.select2-container {
45 .form-control.select2-container {
46 padding: 0; /* padding already applied in .drop-menu a */
46 padding: 0; /* padding already applied in .drop-menu a */
47 }
47 }
48
48
49 .form-control.readonly {
49 .form-control.readonly {
50 background: #eeeeee;
50 background: #eeeeee;
51 cursor: not-allowed;
51 cursor: not-allowed;
52 }
52 }
53
53
54 .error-block {
54 .error-block {
55 color: red;
55 color: red;
56 margin: 0;
56 margin: 0;
57 }
57 }
58
58
59 .help-block {
59 .help-block {
60 margin: 0;
60 margin: 0;
61 }
61 }
62
62
63 .deform-seq-container .control-inputs {
63 .deform-seq-container .control-inputs {
64 width: 100%;
64 width: 100%;
65 }
65 }
66
66
67 .deform-seq-container .deform-seq-item-handle {
67 .deform-seq-container .deform-seq-item-handle {
68 width: 8.3%;
68 width: 8.3%;
69 float: left;
69 float: left;
70 }
70 }
71
71
72 .deform-seq-container .deform-seq-item-group {
72 .deform-seq-container .deform-seq-item-group {
73 width: 91.6%;
73 width: 91.6%;
74 float: left;
74 float: left;
75 }
75 }
76
76
77 .form-control {
77 .form-control {
78 input {
78 input {
79 height: 40px;
79 height: 40px;
80 }
80 }
81 input[type=checkbox], input[type=radio] {
81 input[type=checkbox], input[type=radio] {
82 height: auto;
82 height: auto;
83 }
83 }
84 select {
84 select {
85 height: 40px;
85 height: 40px;
86 }
86 }
87 }
87 }
88
88
89 .form-control.select2-container {
89 .form-control.select2-container {
90 height: 40px;
90 height: 40px;
91 }
91 }
92
92
93 .deform-full-field-sequence.control-inputs {
93 .deform-full-field-sequence.control-inputs {
94 width: 100%;
94 width: 100%;
95 }
95 }
96
96
97 .deform-table-sequence {
97 .deform-table-sequence {
98 .deform-seq-container {
98 .deform-seq-container {
99 .deform-seq-item {
99 .deform-seq-item {
100 margin: 0;
100 margin: 0;
101 label {
101 label {
102 display: none;
102 display: none;
103 }
103 }
104 .panel-heading {
104 .panel-heading {
105 display: none;
105 display: none;
106 }
106 }
107 .deform-seq-item-group > .panel {
107 .deform-seq-item-group > .panel {
108 padding: 0;
108 padding: 0;
109 margin: 5px 0;
109 margin: 5px 0;
110 border: none;
110 border: none;
111 &> .panel-body {
111 &> .panel-body {
112 padding: 0;
112 padding: 0;
113 }
113 }
114 }
114 }
115 &:first-child label {
115 &:first-child label {
116 display: block;
116 display: block;
117 }
117 }
118 }
118 }
119 }
119 }
120 }
120 }
121 .deform-table-2-sequence {
121 .deform-table-2-sequence {
122 .deform-seq-container {
122 .deform-seq-container {
123 .deform-seq-item {
123 .deform-seq-item {
124 .form-group {
124 .form-group {
125 width: 45% !important; padding: 0 2px; float: left; clear: none;
125 width: 45% !important; padding: 0 2px; float: left; clear: none;
126 }
126 }
127 }
127 }
128 }
128 }
129 }
129 }
130 .deform-table-3-sequence {
130 .deform-table-3-sequence {
131 .deform-seq-container {
131 .deform-seq-container {
132 .deform-seq-item {
132 .deform-seq-item {
133 .form-group {
133 .form-group {
134 width: 30% !important; padding: 0 2px; float: left; clear: none;
134 width: 30% !important; padding: 0 2px; float: left; clear: none;
135 }
135 }
136 }
136 }
137 }
137 }
138 }
138 }
139 }
139 }
@@ -1,2347 +1,2385 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'readme-box';
14 @import 'readme-box';
15 @import 'progress-bar';
15 @import 'progress-bar';
16
16
17 @import 'type';
17 @import 'type';
18 @import 'alerts';
18 @import 'alerts';
19 @import 'buttons';
19 @import 'buttons';
20 @import 'tags';
20 @import 'tags';
21 @import 'code-block';
21 @import 'code-block';
22 @import 'examples';
22 @import 'examples';
23 @import 'login';
23 @import 'login';
24 @import 'main-content';
24 @import 'main-content';
25 @import 'select2';
25 @import 'select2';
26 @import 'comments';
26 @import 'comments';
27 @import 'panels-bootstrap';
27 @import 'panels-bootstrap';
28 @import 'panels';
28 @import 'panels';
29 @import 'deform';
29 @import 'deform';
30
30
31 //--- BASE ------------------//
31 //--- BASE ------------------//
32 .noscript-error {
32 .noscript-error {
33 top: 0;
33 top: 0;
34 left: 0;
34 left: 0;
35 width: 100%;
35 width: 100%;
36 z-index: 101;
36 z-index: 101;
37 text-align: center;
37 text-align: center;
38 font-family: @text-semibold;
38 font-family: @text-semibold;
39 font-size: 120%;
39 font-size: 120%;
40 color: white;
40 color: white;
41 background-color: @alert2;
41 background-color: @alert2;
42 padding: 5px 0 5px 0;
42 padding: 5px 0 5px 0;
43 }
43 }
44
44
45 html {
45 html {
46 display: table;
46 display: table;
47 height: 100%;
47 height: 100%;
48 width: 100%;
48 width: 100%;
49 }
49 }
50
50
51 body {
51 body {
52 display: table-cell;
52 display: table-cell;
53 width: 100%;
53 width: 100%;
54 }
54 }
55
55
56 //--- LAYOUT ------------------//
56 //--- LAYOUT ------------------//
57
57
58 .hidden{
58 .hidden{
59 display: none !important;
59 display: none !important;
60 }
60 }
61
61
62 .box{
62 .box{
63 float: left;
63 float: left;
64 width: 100%;
64 width: 100%;
65 }
65 }
66
66
67 .browser-header {
67 .browser-header {
68 clear: both;
68 clear: both;
69 }
69 }
70 .main {
70 .main {
71 clear: both;
71 clear: both;
72 padding:0 0 @pagepadding;
72 padding:0 0 @pagepadding;
73 height: auto;
73 height: auto;
74
74
75 &:after { //clearfix
75 &:after { //clearfix
76 content:"";
76 content:"";
77 clear:both;
77 clear:both;
78 width:100%;
78 width:100%;
79 display:block;
79 display:block;
80 }
80 }
81 }
81 }
82
82
83 .action-link{
83 .action-link{
84 margin-left: @padding;
84 margin-left: @padding;
85 padding-left: @padding;
85 padding-left: @padding;
86 border-left: @border-thickness solid @border-default-color;
86 border-left: @border-thickness solid @border-default-color;
87 }
87 }
88
88
89 input + .action-link, .action-link.first{
89 input + .action-link, .action-link.first{
90 border-left: none;
90 border-left: none;
91 }
91 }
92
92
93 .action-link.last{
93 .action-link.last{
94 margin-right: @padding;
94 margin-right: @padding;
95 padding-right: @padding;
95 padding-right: @padding;
96 }
96 }
97
97
98 .action-link.active,
98 .action-link.active,
99 .action-link.active a{
99 .action-link.active a{
100 color: @grey4;
100 color: @grey4;
101 }
101 }
102
102
103 ul.simple-list{
103 ul.simple-list{
104 list-style: none;
104 list-style: none;
105 margin: 0;
105 margin: 0;
106 padding: 0;
106 padding: 0;
107 }
107 }
108
108
109 .main-content {
109 .main-content {
110 padding-bottom: @pagepadding;
110 padding-bottom: @pagepadding;
111 }
111 }
112
112
113 .wide-mode-wrapper {
113 .wide-mode-wrapper {
114 max-width:4000px !important;
114 max-width:4000px !important;
115 }
115 }
116
116
117 .wrapper {
117 .wrapper {
118 position: relative;
118 position: relative;
119 max-width: @wrapper-maxwidth;
119 max-width: @wrapper-maxwidth;
120 margin: 0 auto;
120 margin: 0 auto;
121 }
121 }
122
122
123 #content {
123 #content {
124 clear: both;
124 clear: both;
125 padding: 0 @contentpadding;
125 padding: 0 @contentpadding;
126 }
126 }
127
127
128 .advanced-settings-fields{
128 .advanced-settings-fields{
129 input{
129 input{
130 margin-left: @textmargin;
130 margin-left: @textmargin;
131 margin-right: @padding/2;
131 margin-right: @padding/2;
132 }
132 }
133 }
133 }
134
134
135 .cs_files_title {
135 .cs_files_title {
136 margin: @pagepadding 0 0;
136 margin: @pagepadding 0 0;
137 }
137 }
138
138
139 input.inline[type="file"] {
139 input.inline[type="file"] {
140 display: inline;
140 display: inline;
141 }
141 }
142
142
143 .error_page {
143 .error_page {
144 margin: 10% auto;
144 margin: 10% auto;
145
145
146 h1 {
146 h1 {
147 color: @grey2;
147 color: @grey2;
148 }
148 }
149
149
150 .alert {
150 .alert {
151 margin: @padding 0;
151 margin: @padding 0;
152 }
152 }
153
153
154 .error-branding {
154 .error-branding {
155 font-family: @text-semibold;
155 font-family: @text-semibold;
156 color: @grey4;
156 color: @grey4;
157 }
157 }
158
158
159 .error_message {
159 .error_message {
160 font-family: @text-regular;
160 font-family: @text-regular;
161 }
161 }
162
162
163 .sidebar {
163 .sidebar {
164 min-height: 275px;
164 min-height: 275px;
165 margin: 0;
165 margin: 0;
166 padding: 0 0 @sidebarpadding @sidebarpadding;
166 padding: 0 0 @sidebarpadding @sidebarpadding;
167 border: none;
167 border: none;
168 }
168 }
169
169
170 .main-content {
170 .main-content {
171 position: relative;
171 position: relative;
172 margin: 0 @sidebarpadding @sidebarpadding;
172 margin: 0 @sidebarpadding @sidebarpadding;
173 padding: 0 0 0 @sidebarpadding;
173 padding: 0 0 0 @sidebarpadding;
174 border-left: @border-thickness solid @grey5;
174 border-left: @border-thickness solid @grey5;
175
175
176 @media (max-width:767px) {
176 @media (max-width:767px) {
177 clear: both;
177 clear: both;
178 width: 100%;
178 width: 100%;
179 margin: 0;
179 margin: 0;
180 border: none;
180 border: none;
181 }
181 }
182 }
182 }
183
183
184 .inner-column {
184 .inner-column {
185 float: left;
185 float: left;
186 width: 29.75%;
186 width: 29.75%;
187 min-height: 150px;
187 min-height: 150px;
188 margin: @sidebarpadding 2% 0 0;
188 margin: @sidebarpadding 2% 0 0;
189 padding: 0 2% 0 0;
189 padding: 0 2% 0 0;
190 border-right: @border-thickness solid @grey5;
190 border-right: @border-thickness solid @grey5;
191
191
192 @media (max-width:767px) {
192 @media (max-width:767px) {
193 clear: both;
193 clear: both;
194 width: 100%;
194 width: 100%;
195 border: none;
195 border: none;
196 }
196 }
197
197
198 ul {
198 ul {
199 padding-left: 1.25em;
199 padding-left: 1.25em;
200 }
200 }
201
201
202 &:last-child {
202 &:last-child {
203 margin: @sidebarpadding 0 0;
203 margin: @sidebarpadding 0 0;
204 border: none;
204 border: none;
205 }
205 }
206
206
207 h4 {
207 h4 {
208 margin: 0 0 @padding;
208 margin: 0 0 @padding;
209 font-family: @text-semibold;
209 font-family: @text-semibold;
210 }
210 }
211 }
211 }
212 }
212 }
213 .error-page-logo {
213 .error-page-logo {
214 width: 130px;
214 width: 130px;
215 height: 160px;
215 height: 160px;
216 }
216 }
217
217
218 // HEADER
218 // HEADER
219 .header {
219 .header {
220
220
221 // TODO: johbo: Fix login pages, so that they work without a min-height
221 // TODO: johbo: Fix login pages, so that they work without a min-height
222 // for the header and then remove the min-height. I chose a smaller value
222 // for the header and then remove the min-height. I chose a smaller value
223 // intentionally here to avoid rendering issues in the main navigation.
223 // intentionally here to avoid rendering issues in the main navigation.
224 min-height: 49px;
224 min-height: 49px;
225
225
226 position: relative;
226 position: relative;
227 vertical-align: bottom;
227 vertical-align: bottom;
228 padding: 0 @header-padding;
228 padding: 0 @header-padding;
229 background-color: @grey2;
229 background-color: @grey2;
230 color: @grey5;
230 color: @grey5;
231
231
232 .title {
232 .title {
233 overflow: visible;
233 overflow: visible;
234 }
234 }
235
235
236 &:before,
236 &:before,
237 &:after {
237 &:after {
238 content: "";
238 content: "";
239 clear: both;
239 clear: both;
240 width: 100%;
240 width: 100%;
241 }
241 }
242
242
243 // TODO: johbo: Avoids breaking "Repositories" chooser
243 // TODO: johbo: Avoids breaking "Repositories" chooser
244 .select2-container .select2-choice .select2-arrow {
244 .select2-container .select2-choice .select2-arrow {
245 display: none;
245 display: none;
246 }
246 }
247 }
247 }
248
248
249 #header-inner {
249 #header-inner {
250 &.title {
250 &.title {
251 margin: 0;
251 margin: 0;
252 }
252 }
253 &:before,
253 &:before,
254 &:after {
254 &:after {
255 content: "";
255 content: "";
256 clear: both;
256 clear: both;
257 }
257 }
258 }
258 }
259
259
260 // Gists
260 // Gists
261 #files_data {
261 #files_data {
262 clear: both; //for firefox
262 clear: both; //for firefox
263 }
263 }
264 #gistid {
264 #gistid {
265 margin-right: @padding;
265 margin-right: @padding;
266 }
266 }
267
267
268 // Global Settings Editor
268 // Global Settings Editor
269 .textarea.editor {
269 .textarea.editor {
270 float: left;
270 float: left;
271 position: relative;
271 position: relative;
272 max-width: @texteditor-width;
272 max-width: @texteditor-width;
273
273
274 select {
274 select {
275 position: absolute;
275 position: absolute;
276 top:10px;
276 top:10px;
277 right:0;
277 right:0;
278 }
278 }
279
279
280 .CodeMirror {
280 .CodeMirror {
281 margin: 0;
281 margin: 0;
282 }
282 }
283
283
284 .help-block {
284 .help-block {
285 margin: 0 0 @padding;
285 margin: 0 0 @padding;
286 padding:.5em;
286 padding:.5em;
287 background-color: @grey6;
287 background-color: @grey6;
288 }
288 }
289 }
289 }
290
290
291 ul.auth_plugins {
291 ul.auth_plugins {
292 margin: @padding 0 @padding @legend-width;
292 margin: @padding 0 @padding @legend-width;
293 padding: 0;
293 padding: 0;
294
294
295 li {
295 li {
296 margin-bottom: @padding;
296 margin-bottom: @padding;
297 line-height: 1em;
297 line-height: 1em;
298 list-style-type: none;
298 list-style-type: none;
299
299
300 .auth_buttons .btn {
300 .auth_buttons .btn {
301 margin-right: @padding;
301 margin-right: @padding;
302 }
302 }
303
303
304 &:before { content: none; }
304 &:before { content: none; }
305 }
305 }
306 }
306 }
307
307
308
308
309 // My Account PR list
309 // My Account PR list
310
310
311 #show_closed {
311 #show_closed {
312 margin: 0 1em 0 0;
312 margin: 0 1em 0 0;
313 }
313 }
314
314
315 .pullrequestlist {
315 .pullrequestlist {
316 .closed {
316 .closed {
317 background-color: @grey6;
317 background-color: @grey6;
318 }
318 }
319 .td-status {
319 .td-status {
320 padding-left: .5em;
320 padding-left: .5em;
321 }
321 }
322 .log-container .truncate {
322 .log-container .truncate {
323 height: 2.75em;
323 height: 2.75em;
324 white-space: pre-line;
324 white-space: pre-line;
325 }
325 }
326 table.rctable .user {
326 table.rctable .user {
327 padding-left: 0;
327 padding-left: 0;
328 }
328 }
329 table.rctable {
329 table.rctable {
330 td.td-description,
330 td.td-description,
331 .rc-user {
331 .rc-user {
332 min-width: auto;
332 min-width: auto;
333 }
333 }
334 }
334 }
335 }
335 }
336
336
337 // Pull Requests
337 // Pull Requests
338
338
339 .pullrequests_section_head {
339 .pullrequests_section_head {
340 display: block;
340 display: block;
341 clear: both;
341 clear: both;
342 margin: @padding 0;
342 margin: @padding 0;
343 font-family: @text-bold;
343 font-family: @text-bold;
344 }
344 }
345
345
346 .pr-origininfo, .pr-targetinfo {
346 .pr-origininfo, .pr-targetinfo {
347 position: relative;
347 position: relative;
348
348
349 .tag {
349 .tag {
350 display: inline-block;
350 display: inline-block;
351 margin: 0 1em .5em 0;
351 margin: 0 1em .5em 0;
352 }
352 }
353
353
354 .clone-url {
354 .clone-url {
355 display: inline-block;
355 display: inline-block;
356 margin: 0 0 .5em 0;
356 margin: 0 0 .5em 0;
357 padding: 0;
357 padding: 0;
358 line-height: 1.2em;
358 line-height: 1.2em;
359 }
359 }
360 }
360 }
361
361
362 .pr-mergeinfo {
363 clear: both;
364 margin: .5em 0;
365
366 input {
367 min-width: 100% !important;
368 padding: 0 !important;
369 border: 0;
370 }
371 }
372
362 .pr-pullinfo {
373 .pr-pullinfo {
363 clear: both;
374 clear: both;
364 margin: .5em 0;
375 margin: .5em 0;
376
377 input {
378 min-width: 100% !important;
379 padding: 0 !important;
380 border: 0;
381 }
365 }
382 }
366
383
367 #pr-title-input {
384 #pr-title-input {
368 width: 72%;
385 width: 72%;
369 font-size: 1em;
386 font-size: 1em;
370 font-family: @text-bold;
387 font-family: @text-bold;
371 margin: 0;
388 margin: 0;
372 padding: 0 0 0 @padding/4;
389 padding: 0 0 0 @padding/4;
373 line-height: 1.7em;
390 line-height: 1.7em;
374 color: @text-color;
391 color: @text-color;
375 letter-spacing: .02em;
392 letter-spacing: .02em;
376 }
393 }
377
394
378 #pullrequest_title {
395 #pullrequest_title {
379 width: 100%;
396 width: 100%;
380 box-sizing: border-box;
397 box-sizing: border-box;
381 }
398 }
382
399
383 #pr_open_message {
400 #pr_open_message {
384 border: @border-thickness solid #fff;
401 border: @border-thickness solid #fff;
385 border-radius: @border-radius;
402 border-radius: @border-radius;
386 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
403 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
387 text-align: left;
404 text-align: left;
388 overflow: hidden;
405 overflow: hidden;
389 }
406 }
390
407
391 .pr-submit-button {
408 .pr-submit-button {
392 float: right;
409 float: right;
393 margin: 0 0 0 5px;
410 margin: 0 0 0 5px;
394 }
411 }
395
412
396 .pr-spacing-container {
413 .pr-spacing-container {
397 padding: 20px;
414 padding: 20px;
398 clear: both
415 clear: both
399 }
416 }
400
417
401 #pr-description-input {
418 #pr-description-input {
402 margin-bottom: 0;
419 margin-bottom: 0;
403 }
420 }
404
421
405 .pr-description-label {
422 .pr-description-label {
406 vertical-align: top;
423 vertical-align: top;
407 }
424 }
408
425
409 .perms_section_head {
426 .perms_section_head {
410 min-width: 625px;
427 min-width: 625px;
411
428
412 h2 {
429 h2 {
413 margin-bottom: 0;
430 margin-bottom: 0;
414 }
431 }
415
432
416 .label-checkbox {
433 .label-checkbox {
417 float: left;
434 float: left;
418 }
435 }
419
436
420 &.field {
437 &.field {
421 margin: @space 0 @padding;
438 margin: @space 0 @padding;
422 }
439 }
423
440
424 &:first-child.field {
441 &:first-child.field {
425 margin-top: 0;
442 margin-top: 0;
426
443
427 .label {
444 .label {
428 margin-top: 0;
445 margin-top: 0;
429 padding-top: 0;
446 padding-top: 0;
430 }
447 }
431
448
432 .radios {
449 .radios {
433 padding-top: 0;
450 padding-top: 0;
434 }
451 }
435 }
452 }
436
453
437 .radios {
454 .radios {
438 float: right;
455 float: right;
439 position: relative;
456 position: relative;
440 width: 405px;
457 width: 405px;
441 }
458 }
442 }
459 }
443
460
444 //--- MODULES ------------------//
461 //--- MODULES ------------------//
445
462
446
463
447 // Server Announcement
464 // Server Announcement
448 #server-announcement {
465 #server-announcement {
449 width: 95%;
466 width: 95%;
450 margin: @padding auto;
467 margin: @padding auto;
451 padding: @padding;
468 padding: @padding;
452 border-width: 2px;
469 border-width: 2px;
453 border-style: solid;
470 border-style: solid;
454 .border-radius(2px);
471 .border-radius(2px);
455 font-family: @text-bold;
472 font-family: @text-bold;
456
473
457 &.info { border-color: @alert4; background-color: @alert4-inner; }
474 &.info { border-color: @alert4; background-color: @alert4-inner; }
458 &.warning { border-color: @alert3; background-color: @alert3-inner; }
475 &.warning { border-color: @alert3; background-color: @alert3-inner; }
459 &.error { border-color: @alert2; background-color: @alert2-inner; }
476 &.error { border-color: @alert2; background-color: @alert2-inner; }
460 &.success { border-color: @alert1; background-color: @alert1-inner; }
477 &.success { border-color: @alert1; background-color: @alert1-inner; }
461 &.neutral { border-color: @grey3; background-color: @grey6; }
478 &.neutral { border-color: @grey3; background-color: @grey6; }
462 }
479 }
463
480
464 // Fixed Sidebar Column
481 // Fixed Sidebar Column
465 .sidebar-col-wrapper {
482 .sidebar-col-wrapper {
466 padding-left: @sidebar-all-width;
483 padding-left: @sidebar-all-width;
467
484
468 .sidebar {
485 .sidebar {
469 width: @sidebar-width;
486 width: @sidebar-width;
470 margin-left: -@sidebar-all-width;
487 margin-left: -@sidebar-all-width;
471 }
488 }
472 }
489 }
473
490
474 .sidebar-col-wrapper.scw-small {
491 .sidebar-col-wrapper.scw-small {
475 padding-left: @sidebar-small-all-width;
492 padding-left: @sidebar-small-all-width;
476
493
477 .sidebar {
494 .sidebar {
478 width: @sidebar-small-width;
495 width: @sidebar-small-width;
479 margin-left: -@sidebar-small-all-width;
496 margin-left: -@sidebar-small-all-width;
480 }
497 }
481 }
498 }
482
499
483
500
484 // FOOTER
501 // FOOTER
485 #footer {
502 #footer {
486 padding: 0;
503 padding: 0;
487 text-align: center;
504 text-align: center;
488 vertical-align: middle;
505 vertical-align: middle;
489 color: @grey2;
506 color: @grey2;
490 background-color: @grey6;
507 background-color: @grey6;
491
508
492 p {
509 p {
493 margin: 0;
510 margin: 0;
494 padding: 1em;
511 padding: 1em;
495 line-height: 1em;
512 line-height: 1em;
496 }
513 }
497
514
498 .server-instance { //server instance
515 .server-instance { //server instance
499 display: none;
516 display: none;
500 }
517 }
501
518
502 .title {
519 .title {
503 float: none;
520 float: none;
504 margin: 0 auto;
521 margin: 0 auto;
505 }
522 }
506 }
523 }
507
524
508 button.close {
525 button.close {
509 padding: 0;
526 padding: 0;
510 cursor: pointer;
527 cursor: pointer;
511 background: transparent;
528 background: transparent;
512 border: 0;
529 border: 0;
513 .box-shadow(none);
530 .box-shadow(none);
514 -webkit-appearance: none;
531 -webkit-appearance: none;
515 }
532 }
516
533
517 .close {
534 .close {
518 float: right;
535 float: right;
519 font-size: 21px;
536 font-size: 21px;
520 font-family: @text-bootstrap;
537 font-family: @text-bootstrap;
521 line-height: 1em;
538 line-height: 1em;
522 font-weight: bold;
539 font-weight: bold;
523 color: @grey2;
540 color: @grey2;
524
541
525 &:hover,
542 &:hover,
526 &:focus {
543 &:focus {
527 color: @grey1;
544 color: @grey1;
528 text-decoration: none;
545 text-decoration: none;
529 cursor: pointer;
546 cursor: pointer;
530 }
547 }
531 }
548 }
532
549
533 // GRID
550 // GRID
534 .sorting,
551 .sorting,
535 .sorting_desc,
552 .sorting_desc,
536 .sorting_asc {
553 .sorting_asc {
537 cursor: pointer;
554 cursor: pointer;
538 }
555 }
539 .sorting_desc:after {
556 .sorting_desc:after {
540 content: "\00A0\25B2";
557 content: "\00A0\25B2";
541 font-size: .75em;
558 font-size: .75em;
542 }
559 }
543 .sorting_asc:after {
560 .sorting_asc:after {
544 content: "\00A0\25BC";
561 content: "\00A0\25BC";
545 font-size: .68em;
562 font-size: .68em;
546 }
563 }
547
564
548
565
549 .user_auth_tokens {
566 .user_auth_tokens {
550
567
551 &.truncate {
568 &.truncate {
552 white-space: nowrap;
569 white-space: nowrap;
553 overflow: hidden;
570 overflow: hidden;
554 text-overflow: ellipsis;
571 text-overflow: ellipsis;
555 }
572 }
556
573
557 .fields .field .input {
574 .fields .field .input {
558 margin: 0;
575 margin: 0;
559 }
576 }
560
577
561 input#description {
578 input#description {
562 width: 100px;
579 width: 100px;
563 margin: 0;
580 margin: 0;
564 }
581 }
565
582
566 .drop-menu {
583 .drop-menu {
567 // TODO: johbo: Remove this, should work out of the box when
584 // TODO: johbo: Remove this, should work out of the box when
568 // having multiple inputs inline
585 // having multiple inputs inline
569 margin: 0 0 0 5px;
586 margin: 0 0 0 5px;
570 }
587 }
571 }
588 }
572 #user_list_table {
589 #user_list_table {
573 .closed {
590 .closed {
574 background-color: @grey6;
591 background-color: @grey6;
575 }
592 }
576 }
593 }
577
594
578
595
579 input {
596 input {
580 &.disabled {
597 &.disabled {
581 opacity: .5;
598 opacity: .5;
582 }
599 }
583 }
600 }
584
601
585 // remove extra padding in firefox
602 // remove extra padding in firefox
586 input::-moz-focus-inner { border:0; padding:0 }
603 input::-moz-focus-inner { border:0; padding:0 }
587
604
588 .adjacent input {
605 .adjacent input {
589 margin-bottom: @padding;
606 margin-bottom: @padding;
590 }
607 }
591
608
592 .permissions_boxes {
609 .permissions_boxes {
593 display: block;
610 display: block;
594 }
611 }
595
612
596 //TODO: lisa: this should be in tables
613 //TODO: lisa: this should be in tables
597 .show_more_col {
614 .show_more_col {
598 width: 20px;
615 width: 20px;
599 }
616 }
600
617
601 //FORMS
618 //FORMS
602
619
603 .medium-inline,
620 .medium-inline,
604 input#description.medium-inline {
621 input#description.medium-inline {
605 display: inline;
622 display: inline;
606 width: @medium-inline-input-width;
623 width: @medium-inline-input-width;
607 min-width: 100px;
624 min-width: 100px;
608 }
625 }
609
626
610 select {
627 select {
611 //reset
628 //reset
612 -webkit-appearance: none;
629 -webkit-appearance: none;
613 -moz-appearance: none;
630 -moz-appearance: none;
614
631
615 display: inline-block;
632 display: inline-block;
616 height: 28px;
633 height: 28px;
617 width: auto;
634 width: auto;
618 margin: 0 @padding @padding 0;
635 margin: 0 @padding @padding 0;
619 padding: 0 18px 0 8px;
636 padding: 0 18px 0 8px;
620 line-height:1em;
637 line-height:1em;
621 font-size: @basefontsize;
638 font-size: @basefontsize;
622 border: @border-thickness solid @rcblue;
639 border: @border-thickness solid @rcblue;
623 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
640 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
624 color: @rcblue;
641 color: @rcblue;
625
642
626 &:after {
643 &:after {
627 content: "\00A0\25BE";
644 content: "\00A0\25BE";
628 }
645 }
629
646
630 &:focus {
647 &:focus {
631 outline: none;
648 outline: none;
632 }
649 }
633 }
650 }
634
651
635 option {
652 option {
636 &:focus {
653 &:focus {
637 outline: none;
654 outline: none;
638 }
655 }
639 }
656 }
640
657
641 input,
658 input,
642 textarea {
659 textarea {
643 padding: @input-padding;
660 padding: @input-padding;
644 border: @input-border-thickness solid @border-highlight-color;
661 border: @input-border-thickness solid @border-highlight-color;
645 .border-radius (@border-radius);
662 .border-radius (@border-radius);
646 font-family: @text-light;
663 font-family: @text-light;
647 font-size: @basefontsize;
664 font-size: @basefontsize;
648
665
649 &.input-sm {
666 &.input-sm {
650 padding: 5px;
667 padding: 5px;
651 }
668 }
652
669
653 &#description {
670 &#description {
654 min-width: @input-description-minwidth;
671 min-width: @input-description-minwidth;
655 min-height: 1em;
672 min-height: 1em;
656 padding: 10px;
673 padding: 10px;
657 }
674 }
658 }
675 }
659
676
660 .field-sm {
677 .field-sm {
661 input,
678 input,
662 textarea {
679 textarea {
663 padding: 5px;
680 padding: 5px;
664 }
681 }
665 }
682 }
666
683
667 textarea {
684 textarea {
668 display: block;
685 display: block;
669 clear: both;
686 clear: both;
670 width: 100%;
687 width: 100%;
671 min-height: 100px;
688 min-height: 100px;
672 margin-bottom: @padding;
689 margin-bottom: @padding;
673 .box-sizing(border-box);
690 .box-sizing(border-box);
674 overflow: auto;
691 overflow: auto;
675 }
692 }
676
693
677 label {
694 label {
678 font-family: @text-light;
695 font-family: @text-light;
679 }
696 }
680
697
681 // GRAVATARS
698 // GRAVATARS
682 // centers gravatar on username to the right
699 // centers gravatar on username to the right
683
700
684 .gravatar {
701 .gravatar {
685 display: inline;
702 display: inline;
686 min-width: 16px;
703 min-width: 16px;
687 min-height: 16px;
704 min-height: 16px;
688 margin: -5px 0;
705 margin: -5px 0;
689 padding: 0;
706 padding: 0;
690 line-height: 1em;
707 line-height: 1em;
691 border: 1px solid @grey4;
708 border: 1px solid @grey4;
692 box-sizing: content-box;
709 box-sizing: content-box;
693
710
694 &.gravatar-large {
711 &.gravatar-large {
695 margin: -0.5em .25em -0.5em 0;
712 margin: -0.5em .25em -0.5em 0;
696 }
713 }
697
714
698 & + .user {
715 & + .user {
699 display: inline;
716 display: inline;
700 margin: 0;
717 margin: 0;
701 padding: 0 0 0 .17em;
718 padding: 0 0 0 .17em;
702 line-height: 1em;
719 line-height: 1em;
703 }
720 }
704 }
721 }
705
722
706 .user-inline-data {
723 .user-inline-data {
707 display: inline-block;
724 display: inline-block;
708 float: left;
725 float: left;
709 padding-left: .5em;
726 padding-left: .5em;
710 line-height: 1.3em;
727 line-height: 1.3em;
711 }
728 }
712
729
713 .rc-user { // gravatar + user wrapper
730 .rc-user { // gravatar + user wrapper
714 float: left;
731 float: left;
715 position: relative;
732 position: relative;
716 min-width: 100px;
733 min-width: 100px;
717 max-width: 200px;
734 max-width: 200px;
718 min-height: (@gravatar-size + @border-thickness * 2); // account for border
735 min-height: (@gravatar-size + @border-thickness * 2); // account for border
719 display: block;
736 display: block;
720 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
737 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
721
738
722
739
723 .gravatar {
740 .gravatar {
724 display: block;
741 display: block;
725 position: absolute;
742 position: absolute;
726 top: 0;
743 top: 0;
727 left: 0;
744 left: 0;
728 min-width: @gravatar-size;
745 min-width: @gravatar-size;
729 min-height: @gravatar-size;
746 min-height: @gravatar-size;
730 margin: 0;
747 margin: 0;
731 }
748 }
732
749
733 .user {
750 .user {
734 display: block;
751 display: block;
735 max-width: 175px;
752 max-width: 175px;
736 padding-top: 2px;
753 padding-top: 2px;
737 overflow: hidden;
754 overflow: hidden;
738 text-overflow: ellipsis;
755 text-overflow: ellipsis;
739 }
756 }
740 }
757 }
741
758
742 .gist-gravatar,
759 .gist-gravatar,
743 .journal_container {
760 .journal_container {
744 .gravatar-large {
761 .gravatar-large {
745 margin: 0 .5em -10px 0;
762 margin: 0 .5em -10px 0;
746 }
763 }
747 }
764 }
748
765
749
766
750 // ADMIN SETTINGS
767 // ADMIN SETTINGS
751
768
752 // Tag Patterns
769 // Tag Patterns
753 .tag_patterns {
770 .tag_patterns {
754 .tag_input {
771 .tag_input {
755 margin-bottom: @padding;
772 margin-bottom: @padding;
756 }
773 }
757 }
774 }
758
775
759 .locked_input {
776 .locked_input {
760 position: relative;
777 position: relative;
761
778
762 input {
779 input {
763 display: inline;
780 display: inline;
764 margin: 3px 5px 0px 0px;
781 margin: 3px 5px 0px 0px;
765 }
782 }
766
783
767 br {
784 br {
768 display: none;
785 display: none;
769 }
786 }
770
787
771 .error-message {
788 .error-message {
772 float: left;
789 float: left;
773 width: 100%;
790 width: 100%;
774 }
791 }
775
792
776 .lock_input_button {
793 .lock_input_button {
777 display: inline;
794 display: inline;
778 }
795 }
779
796
780 .help-block {
797 .help-block {
781 clear: both;
798 clear: both;
782 }
799 }
783 }
800 }
784
801
785 // Notifications
802 // Notifications
786
803
787 .notifications_buttons {
804 .notifications_buttons {
788 margin: 0 0 @space 0;
805 margin: 0 0 @space 0;
789 padding: 0;
806 padding: 0;
790
807
791 .btn {
808 .btn {
792 display: inline-block;
809 display: inline-block;
793 }
810 }
794 }
811 }
795
812
796 .notification-list {
813 .notification-list {
797
814
798 div {
815 div {
799 display: inline-block;
816 display: inline-block;
800 vertical-align: middle;
817 vertical-align: middle;
801 }
818 }
802
819
803 .container {
820 .container {
804 display: block;
821 display: block;
805 margin: 0 0 @padding 0;
822 margin: 0 0 @padding 0;
806 }
823 }
807
824
808 .delete-notifications {
825 .delete-notifications {
809 margin-left: @padding;
826 margin-left: @padding;
810 text-align: right;
827 text-align: right;
811 cursor: pointer;
828 cursor: pointer;
812 }
829 }
813
830
814 .read-notifications {
831 .read-notifications {
815 margin-left: @padding/2;
832 margin-left: @padding/2;
816 text-align: right;
833 text-align: right;
817 width: 35px;
834 width: 35px;
818 cursor: pointer;
835 cursor: pointer;
819 }
836 }
820
837
821 .icon-minus-sign {
838 .icon-minus-sign {
822 color: @alert2;
839 color: @alert2;
823 }
840 }
824
841
825 .icon-ok-sign {
842 .icon-ok-sign {
826 color: @alert1;
843 color: @alert1;
827 }
844 }
828 }
845 }
829
846
830 .user_settings {
847 .user_settings {
831 float: left;
848 float: left;
832 clear: both;
849 clear: both;
833 display: block;
850 display: block;
834 width: 100%;
851 width: 100%;
835
852
836 .gravatar_box {
853 .gravatar_box {
837 margin-bottom: @padding;
854 margin-bottom: @padding;
838
855
839 &:after {
856 &:after {
840 content: " ";
857 content: " ";
841 clear: both;
858 clear: both;
842 width: 100%;
859 width: 100%;
843 }
860 }
844 }
861 }
845
862
846 .fields .field {
863 .fields .field {
847 clear: both;
864 clear: both;
848 }
865 }
849 }
866 }
850
867
851 .advanced_settings {
868 .advanced_settings {
852 margin-bottom: @space;
869 margin-bottom: @space;
853
870
854 .help-block {
871 .help-block {
855 margin-left: 0;
872 margin-left: 0;
856 }
873 }
857
874
858 button + .help-block {
875 button + .help-block {
859 margin-top: @padding;
876 margin-top: @padding;
860 }
877 }
861 }
878 }
862
879
863 // admin settings radio buttons and labels
880 // admin settings radio buttons and labels
864 .label-2 {
881 .label-2 {
865 float: left;
882 float: left;
866 width: @label2-width;
883 width: @label2-width;
867
884
868 label {
885 label {
869 color: @grey1;
886 color: @grey1;
870 }
887 }
871 }
888 }
872 .checkboxes {
889 .checkboxes {
873 float: left;
890 float: left;
874 width: @checkboxes-width;
891 width: @checkboxes-width;
875 margin-bottom: @padding;
892 margin-bottom: @padding;
876
893
877 .checkbox {
894 .checkbox {
878 width: 100%;
895 width: 100%;
879
896
880 label {
897 label {
881 margin: 0;
898 margin: 0;
882 padding: 0;
899 padding: 0;
883 }
900 }
884 }
901 }
885
902
886 .checkbox + .checkbox {
903 .checkbox + .checkbox {
887 display: inline-block;
904 display: inline-block;
888 }
905 }
889
906
890 label {
907 label {
891 margin-right: 1em;
908 margin-right: 1em;
892 }
909 }
893 }
910 }
894
911
895 // CHANGELOG
912 // CHANGELOG
896 .container_header {
913 .container_header {
897 float: left;
914 float: left;
898 display: block;
915 display: block;
899 width: 100%;
916 width: 100%;
900 margin: @padding 0 @padding;
917 margin: @padding 0 @padding;
901
918
902 #filter_changelog {
919 #filter_changelog {
903 float: left;
920 float: left;
904 margin-right: @padding;
921 margin-right: @padding;
905 }
922 }
906
923
907 .breadcrumbs_light {
924 .breadcrumbs_light {
908 display: inline-block;
925 display: inline-block;
909 }
926 }
910 }
927 }
911
928
912 .info_box {
929 .info_box {
913 float: right;
930 float: right;
914 }
931 }
915
932
916
933
917 #graph_nodes {
934 #graph_nodes {
918 padding-top: 43px;
935 padding-top: 43px;
919 }
936 }
920
937
921 #graph_content{
938 #graph_content{
922
939
923 // adjust for table headers so that graph renders properly
940 // adjust for table headers so that graph renders properly
924 // #graph_nodes padding - table cell padding
941 // #graph_nodes padding - table cell padding
925 padding-top: (@space - (@basefontsize * 2.4));
942 padding-top: (@space - (@basefontsize * 2.4));
926
943
927 &.graph_full_width {
944 &.graph_full_width {
928 width: 100%;
945 width: 100%;
929 max-width: 100%;
946 max-width: 100%;
930 }
947 }
931 }
948 }
932
949
933 #graph {
950 #graph {
934 .flag_status {
951 .flag_status {
935 margin: 0;
952 margin: 0;
936 }
953 }
937
954
938 .pagination-left {
955 .pagination-left {
939 float: left;
956 float: left;
940 clear: both;
957 clear: both;
941 }
958 }
942
959
943 .log-container {
960 .log-container {
944 max-width: 345px;
961 max-width: 345px;
945
962
946 .message{
963 .message{
947 max-width: 340px;
964 max-width: 340px;
948 }
965 }
949 }
966 }
950
967
951 .graph-col-wrapper {
968 .graph-col-wrapper {
952 padding-left: 110px;
969 padding-left: 110px;
953
970
954 #graph_nodes {
971 #graph_nodes {
955 width: 100px;
972 width: 100px;
956 margin-left: -110px;
973 margin-left: -110px;
957 float: left;
974 float: left;
958 clear: left;
975 clear: left;
959 }
976 }
960 }
977 }
961
978
962 .load-more-commits {
979 .load-more-commits {
963 text-align: center;
980 text-align: center;
964 }
981 }
965 .load-more-commits:hover {
982 .load-more-commits:hover {
966 background-color: @grey7;
983 background-color: @grey7;
967 }
984 }
968 .load-more-commits {
985 .load-more-commits {
969 a {
986 a {
970 display: block;
987 display: block;
971 }
988 }
972 }
989 }
973 }
990 }
974
991
975 #filter_changelog {
992 #filter_changelog {
976 float: left;
993 float: left;
977 }
994 }
978
995
979
996
980 //--- THEME ------------------//
997 //--- THEME ------------------//
981
998
982 #logo {
999 #logo {
983 float: left;
1000 float: left;
984 margin: 9px 0 0 0;
1001 margin: 9px 0 0 0;
985
1002
986 .header {
1003 .header {
987 background-color: transparent;
1004 background-color: transparent;
988 }
1005 }
989
1006
990 a {
1007 a {
991 display: inline-block;
1008 display: inline-block;
992 }
1009 }
993
1010
994 img {
1011 img {
995 height:30px;
1012 height:30px;
996 }
1013 }
997 }
1014 }
998
1015
999 .logo-wrapper {
1016 .logo-wrapper {
1000 float:left;
1017 float:left;
1001 }
1018 }
1002
1019
1003 .branding{
1020 .branding{
1004 float: left;
1021 float: left;
1005 padding: 9px 2px;
1022 padding: 9px 2px;
1006 line-height: 1em;
1023 line-height: 1em;
1007 font-size: @navigation-fontsize;
1024 font-size: @navigation-fontsize;
1008 }
1025 }
1009
1026
1010 img {
1027 img {
1011 border: none;
1028 border: none;
1012 outline: none;
1029 outline: none;
1013 }
1030 }
1014 user-profile-header
1031 user-profile-header
1015 label {
1032 label {
1016
1033
1017 input[type="checkbox"] {
1034 input[type="checkbox"] {
1018 margin-right: 1em;
1035 margin-right: 1em;
1019 }
1036 }
1020 input[type="radio"] {
1037 input[type="radio"] {
1021 margin-right: 1em;
1038 margin-right: 1em;
1022 }
1039 }
1023 }
1040 }
1024
1041
1025 .flag_status {
1042 .flag_status {
1026 margin: 2px 8px 6px 2px;
1043 margin: 2px 8px 6px 2px;
1027 &.under_review {
1044 &.under_review {
1028 .circle(5px, @alert3);
1045 .circle(5px, @alert3);
1029 }
1046 }
1030 &.approved {
1047 &.approved {
1031 .circle(5px, @alert1);
1048 .circle(5px, @alert1);
1032 }
1049 }
1033 &.rejected,
1050 &.rejected,
1034 &.forced_closed{
1051 &.forced_closed{
1035 .circle(5px, @alert2);
1052 .circle(5px, @alert2);
1036 }
1053 }
1037 &.not_reviewed {
1054 &.not_reviewed {
1038 .circle(5px, @grey5);
1055 .circle(5px, @grey5);
1039 }
1056 }
1040 }
1057 }
1041
1058
1042 .flag_status_comment_box {
1059 .flag_status_comment_box {
1043 margin: 5px 6px 0px 2px;
1060 margin: 5px 6px 0px 2px;
1044 }
1061 }
1045 .test_pattern_preview {
1062 .test_pattern_preview {
1046 margin: @space 0;
1063 margin: @space 0;
1047
1064
1048 p {
1065 p {
1049 margin-bottom: 0;
1066 margin-bottom: 0;
1050 border-bottom: @border-thickness solid @border-default-color;
1067 border-bottom: @border-thickness solid @border-default-color;
1051 color: @grey3;
1068 color: @grey3;
1052 }
1069 }
1053
1070
1054 .btn {
1071 .btn {
1055 margin-bottom: @padding;
1072 margin-bottom: @padding;
1056 }
1073 }
1057 }
1074 }
1058 #test_pattern_result {
1075 #test_pattern_result {
1059 display: none;
1076 display: none;
1060 &:extend(pre);
1077 &:extend(pre);
1061 padding: .9em;
1078 padding: .9em;
1062 color: @grey3;
1079 color: @grey3;
1063 background-color: @grey7;
1080 background-color: @grey7;
1064 border-right: @border-thickness solid @border-default-color;
1081 border-right: @border-thickness solid @border-default-color;
1065 border-bottom: @border-thickness solid @border-default-color;
1082 border-bottom: @border-thickness solid @border-default-color;
1066 border-left: @border-thickness solid @border-default-color;
1083 border-left: @border-thickness solid @border-default-color;
1067 }
1084 }
1068
1085
1069 #repo_vcs_settings {
1086 #repo_vcs_settings {
1070 #inherit_overlay_vcs_default {
1087 #inherit_overlay_vcs_default {
1071 display: none;
1088 display: none;
1072 }
1089 }
1073 #inherit_overlay_vcs_custom {
1090 #inherit_overlay_vcs_custom {
1074 display: custom;
1091 display: custom;
1075 }
1092 }
1076 &.inherited {
1093 &.inherited {
1077 #inherit_overlay_vcs_default {
1094 #inherit_overlay_vcs_default {
1078 display: block;
1095 display: block;
1079 }
1096 }
1080 #inherit_overlay_vcs_custom {
1097 #inherit_overlay_vcs_custom {
1081 display: none;
1098 display: none;
1082 }
1099 }
1083 }
1100 }
1084 }
1101 }
1085
1102
1086 .issue-tracker-link {
1103 .issue-tracker-link {
1087 color: @rcblue;
1104 color: @rcblue;
1088 }
1105 }
1089
1106
1090 // Issue Tracker Table Show/Hide
1107 // Issue Tracker Table Show/Hide
1091 #repo_issue_tracker {
1108 #repo_issue_tracker {
1092 #inherit_overlay {
1109 #inherit_overlay {
1093 display: none;
1110 display: none;
1094 }
1111 }
1095 #custom_overlay {
1112 #custom_overlay {
1096 display: custom;
1113 display: custom;
1097 }
1114 }
1098 &.inherited {
1115 &.inherited {
1099 #inherit_overlay {
1116 #inherit_overlay {
1100 display: block;
1117 display: block;
1101 }
1118 }
1102 #custom_overlay {
1119 #custom_overlay {
1103 display: none;
1120 display: none;
1104 }
1121 }
1105 }
1122 }
1106 }
1123 }
1107 table.issuetracker {
1124 table.issuetracker {
1108 &.readonly {
1125 &.readonly {
1109 tr, td {
1126 tr, td {
1110 color: @grey3;
1127 color: @grey3;
1111 }
1128 }
1112 }
1129 }
1113 .edit {
1130 .edit {
1114 display: none;
1131 display: none;
1115 }
1132 }
1116 .editopen {
1133 .editopen {
1117 .edit {
1134 .edit {
1118 display: inline;
1135 display: inline;
1119 }
1136 }
1120 .entry {
1137 .entry {
1121 display: none;
1138 display: none;
1122 }
1139 }
1123 }
1140 }
1124 tr td.td-action {
1141 tr td.td-action {
1125 min-width: 117px;
1142 min-width: 117px;
1126 }
1143 }
1127 td input {
1144 td input {
1128 max-width: none;
1145 max-width: none;
1129 min-width: 30px;
1146 min-width: 30px;
1130 width: 80%;
1147 width: 80%;
1131 }
1148 }
1132 .issuetracker_pref input {
1149 .issuetracker_pref input {
1133 width: 40%;
1150 width: 40%;
1134 }
1151 }
1135 input.edit_issuetracker_update {
1152 input.edit_issuetracker_update {
1136 margin-right: 0;
1153 margin-right: 0;
1137 width: auto;
1154 width: auto;
1138 }
1155 }
1139 }
1156 }
1140
1157
1141 table.integrations {
1158 table.integrations {
1142 .td-icon {
1159 .td-icon {
1143 width: 20px;
1160 width: 20px;
1144 .integration-icon {
1161 .integration-icon {
1145 height: 20px;
1162 height: 20px;
1146 width: 20px;
1163 width: 20px;
1147 }
1164 }
1148 }
1165 }
1149 }
1166 }
1150
1167
1151 .integrations {
1168 .integrations {
1152 a.integration-box {
1169 a.integration-box {
1153 color: @text-color;
1170 color: @text-color;
1154 &:hover {
1171 &:hover {
1155 .panel {
1172 .panel {
1156 background: #fbfbfb;
1173 background: #fbfbfb;
1157 }
1174 }
1158 }
1175 }
1159 .integration-icon {
1176 .integration-icon {
1160 width: 30px;
1177 width: 30px;
1161 height: 30px;
1178 height: 30px;
1162 margin-right: 20px;
1179 margin-right: 20px;
1163 float: left;
1180 float: left;
1164 }
1181 }
1165
1182
1166 .panel-body {
1183 .panel-body {
1167 padding: 10px;
1184 padding: 10px;
1168 }
1185 }
1169 .panel {
1186 .panel {
1170 margin-bottom: 10px;
1187 margin-bottom: 10px;
1171 }
1188 }
1172 h2 {
1189 h2 {
1173 display: inline-block;
1190 display: inline-block;
1174 margin: 0;
1191 margin: 0;
1175 min-width: 140px;
1192 min-width: 140px;
1176 }
1193 }
1177 }
1194 }
1178 }
1195 }
1179
1196
1180 //Permissions Settings
1197 //Permissions Settings
1181 #add_perm {
1198 #add_perm {
1182 margin: 0 0 @padding;
1199 margin: 0 0 @padding;
1183 cursor: pointer;
1200 cursor: pointer;
1184 }
1201 }
1185
1202
1186 .perm_ac {
1203 .perm_ac {
1187 input {
1204 input {
1188 width: 95%;
1205 width: 95%;
1189 }
1206 }
1190 }
1207 }
1191
1208
1192 .autocomplete-suggestions {
1209 .autocomplete-suggestions {
1193 width: auto !important; // overrides autocomplete.js
1210 width: auto !important; // overrides autocomplete.js
1194 margin: 0;
1211 margin: 0;
1195 border: @border-thickness solid @rcblue;
1212 border: @border-thickness solid @rcblue;
1196 border-radius: @border-radius;
1213 border-radius: @border-radius;
1197 color: @rcblue;
1214 color: @rcblue;
1198 background-color: white;
1215 background-color: white;
1199 }
1216 }
1200 .autocomplete-selected {
1217 .autocomplete-selected {
1201 background: #F0F0F0;
1218 background: #F0F0F0;
1202 }
1219 }
1203 .ac-container-wrap {
1220 .ac-container-wrap {
1204 margin: 0;
1221 margin: 0;
1205 padding: 8px;
1222 padding: 8px;
1206 border-bottom: @border-thickness solid @rclightblue;
1223 border-bottom: @border-thickness solid @rclightblue;
1207 list-style-type: none;
1224 list-style-type: none;
1208 cursor: pointer;
1225 cursor: pointer;
1209
1226
1210 &:hover {
1227 &:hover {
1211 background-color: @rclightblue;
1228 background-color: @rclightblue;
1212 }
1229 }
1213
1230
1214 img {
1231 img {
1215 height: @gravatar-size;
1232 height: @gravatar-size;
1216 width: @gravatar-size;
1233 width: @gravatar-size;
1217 margin-right: 1em;
1234 margin-right: 1em;
1218 }
1235 }
1219
1236
1220 strong {
1237 strong {
1221 font-weight: normal;
1238 font-weight: normal;
1222 }
1239 }
1223 }
1240 }
1224
1241
1225 // Settings Dropdown
1242 // Settings Dropdown
1226 .user-menu .container {
1243 .user-menu .container {
1227 padding: 0 4px;
1244 padding: 0 4px;
1228 margin: 0;
1245 margin: 0;
1229 }
1246 }
1230
1247
1231 .user-menu .gravatar {
1248 .user-menu .gravatar {
1232 cursor: pointer;
1249 cursor: pointer;
1233 }
1250 }
1234
1251
1235 .codeblock {
1252 .codeblock {
1236 margin-bottom: @padding;
1253 margin-bottom: @padding;
1237 clear: both;
1254 clear: both;
1238
1255
1239 .stats{
1256 .stats{
1240 overflow: hidden;
1257 overflow: hidden;
1241 }
1258 }
1242
1259
1243 .message{
1260 .message{
1244 textarea{
1261 textarea{
1245 margin: 0;
1262 margin: 0;
1246 }
1263 }
1247 }
1264 }
1248
1265
1249 .code-header {
1266 .code-header {
1250 .stats {
1267 .stats {
1251 line-height: 2em;
1268 line-height: 2em;
1252
1269
1253 .revision_id {
1270 .revision_id {
1254 margin-left: 0;
1271 margin-left: 0;
1255 }
1272 }
1256 .buttons {
1273 .buttons {
1257 padding-right: 0;
1274 padding-right: 0;
1258 }
1275 }
1259 }
1276 }
1260
1277
1261 .item{
1278 .item{
1262 margin-right: 0.5em;
1279 margin-right: 0.5em;
1263 }
1280 }
1264 }
1281 }
1265
1282
1266 #editor_container{
1283 #editor_container{
1267 position: relative;
1284 position: relative;
1268 margin: @padding;
1285 margin: @padding;
1269 }
1286 }
1270 }
1287 }
1271
1288
1272 #file_history_container {
1289 #file_history_container {
1273 display: none;
1290 display: none;
1274 }
1291 }
1275
1292
1276 .file-history-inner {
1293 .file-history-inner {
1277 margin-bottom: 10px;
1294 margin-bottom: 10px;
1278 }
1295 }
1279
1296
1280 // Pull Requests
1297 // Pull Requests
1281 .summary-details {
1298 .summary-details {
1282 width: 72%;
1299 width: 72%;
1283 }
1300 }
1284 .pr-summary {
1301 .pr-summary {
1285 border-bottom: @border-thickness solid @grey5;
1302 border-bottom: @border-thickness solid @grey5;
1286 margin-bottom: @space;
1303 margin-bottom: @space;
1287 }
1304 }
1288 .reviewers-title {
1305 .reviewers-title {
1289 width: 25%;
1306 width: 25%;
1290 min-width: 200px;
1307 min-width: 200px;
1291 }
1308 }
1292 .reviewers {
1309 .reviewers {
1293 width: 25%;
1310 width: 25%;
1294 min-width: 200px;
1311 min-width: 200px;
1295 }
1312 }
1296 .reviewers ul li {
1313 .reviewers ul li {
1297 position: relative;
1314 position: relative;
1298 width: 100%;
1315 width: 100%;
1299 margin-bottom: 8px;
1316 margin-bottom: 8px;
1300 }
1317 }
1318
1319 .reviewer_entry {
1320 min-height: 55px;
1321 }
1322
1301 .reviewers_member {
1323 .reviewers_member {
1302 width: 100%;
1324 width: 100%;
1303 overflow: auto;
1325 overflow: auto;
1304 }
1326 }
1305 .reviewer_reason {
1327 .reviewer_reason {
1306 padding-left: 20px;
1328 padding-left: 20px;
1307 }
1329 }
1308 .reviewer_status {
1330 .reviewer_status {
1309 display: inline-block;
1331 display: inline-block;
1310 vertical-align: top;
1332 vertical-align: top;
1311 width: 7%;
1333 width: 7%;
1312 min-width: 20px;
1334 min-width: 20px;
1313 height: 1.2em;
1335 height: 1.2em;
1314 margin-top: 3px;
1336 margin-top: 3px;
1315 line-height: 1em;
1337 line-height: 1em;
1316 }
1338 }
1317
1339
1318 .reviewer_name {
1340 .reviewer_name {
1319 display: inline-block;
1341 display: inline-block;
1320 max-width: 83%;
1342 max-width: 83%;
1321 padding-right: 20px;
1343 padding-right: 20px;
1322 vertical-align: middle;
1344 vertical-align: middle;
1323 line-height: 1;
1345 line-height: 1;
1324
1346
1325 .rc-user {
1347 .rc-user {
1326 min-width: 0;
1348 min-width: 0;
1327 margin: -2px 1em 0 0;
1349 margin: -2px 1em 0 0;
1328 }
1350 }
1329
1351
1330 .reviewer {
1352 .reviewer {
1331 float: left;
1353 float: left;
1332 }
1354 }
1333 }
1355 }
1334
1356
1357 .reviewer_member_mandatory,
1358 .reviewer_member_mandatory_remove,
1335 .reviewer_member_remove {
1359 .reviewer_member_remove {
1336 position: absolute;
1360 position: absolute;
1337 right: 0;
1361 right: 0;
1338 top: 0;
1362 top: 0;
1339 width: 16px;
1363 width: 16px;
1340 margin-bottom: 10px;
1364 margin-bottom: 10px;
1341 padding: 0;
1365 padding: 0;
1342 color: black;
1366 color: black;
1343 }
1367 }
1368
1369 .reviewer_member_mandatory_remove {
1370 color: @grey4;
1371 }
1372
1373 .reviewer_member_mandatory {
1374 padding-top:20px;
1375 }
1376
1344 .reviewer_member_status {
1377 .reviewer_member_status {
1345 margin-top: 5px;
1378 margin-top: 5px;
1346 }
1379 }
1347 .pr-summary #summary{
1380 .pr-summary #summary{
1348 width: 100%;
1381 width: 100%;
1349 }
1382 }
1350 .pr-summary .action_button:hover {
1383 .pr-summary .action_button:hover {
1351 border: 0;
1384 border: 0;
1352 cursor: pointer;
1385 cursor: pointer;
1353 }
1386 }
1354 .pr-details-title {
1387 .pr-details-title {
1355 padding-bottom: 8px;
1388 padding-bottom: 8px;
1356 border-bottom: @border-thickness solid @grey5;
1389 border-bottom: @border-thickness solid @grey5;
1357
1390
1358 .action_button.disabled {
1391 .action_button.disabled {
1359 color: @grey4;
1392 color: @grey4;
1360 cursor: inherit;
1393 cursor: inherit;
1361 }
1394 }
1362 .action_button {
1395 .action_button {
1363 color: @rcblue;
1396 color: @rcblue;
1364 }
1397 }
1365 }
1398 }
1366 .pr-details-content {
1399 .pr-details-content {
1367 margin-top: @textmargin;
1400 margin-top: @textmargin;
1368 margin-bottom: @textmargin;
1401 margin-bottom: @textmargin;
1369 }
1402 }
1370 .pr-description {
1403 .pr-description {
1371 white-space:pre-wrap;
1404 white-space:pre-wrap;
1372 }
1405 }
1406
1407 .pr-reviewer-rules {
1408 padding: 10px 0px 20px 0px;
1409 }
1410
1373 .group_members {
1411 .group_members {
1374 margin-top: 0;
1412 margin-top: 0;
1375 padding: 0;
1413 padding: 0;
1376 list-style: outside none none;
1414 list-style: outside none none;
1377
1415
1378 img {
1416 img {
1379 height: @gravatar-size;
1417 height: @gravatar-size;
1380 width: @gravatar-size;
1418 width: @gravatar-size;
1381 margin-right: .5em;
1419 margin-right: .5em;
1382 margin-left: 3px;
1420 margin-left: 3px;
1383 }
1421 }
1384
1422
1385 .to-delete {
1423 .to-delete {
1386 .user {
1424 .user {
1387 text-decoration: line-through;
1425 text-decoration: line-through;
1388 }
1426 }
1389 }
1427 }
1390 }
1428 }
1391
1429
1392 .compare_view_commits_title {
1430 .compare_view_commits_title {
1393 .disabled {
1431 .disabled {
1394 cursor: inherit;
1432 cursor: inherit;
1395 &:hover{
1433 &:hover{
1396 background-color: inherit;
1434 background-color: inherit;
1397 color: inherit;
1435 color: inherit;
1398 }
1436 }
1399 }
1437 }
1400 }
1438 }
1401
1439
1402 .subtitle-compare {
1440 .subtitle-compare {
1403 margin: -15px 0px 0px 0px;
1441 margin: -15px 0px 0px 0px;
1404 }
1442 }
1405
1443
1406 .comments-summary-td {
1444 .comments-summary-td {
1407 border-top: 1px dashed @grey5;
1445 border-top: 1px dashed @grey5;
1408 }
1446 }
1409
1447
1410 // new entry in group_members
1448 // new entry in group_members
1411 .td-author-new-entry {
1449 .td-author-new-entry {
1412 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1450 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1413 }
1451 }
1414
1452
1415 .usergroup_member_remove {
1453 .usergroup_member_remove {
1416 width: 16px;
1454 width: 16px;
1417 margin-bottom: 10px;
1455 margin-bottom: 10px;
1418 padding: 0;
1456 padding: 0;
1419 color: black !important;
1457 color: black !important;
1420 cursor: pointer;
1458 cursor: pointer;
1421 }
1459 }
1422
1460
1423 .reviewer_ac .ac-input {
1461 .reviewer_ac .ac-input {
1424 width: 92%;
1462 width: 92%;
1425 margin-bottom: 1em;
1463 margin-bottom: 1em;
1426 }
1464 }
1427
1465
1428 .compare_view_commits tr{
1466 .compare_view_commits tr{
1429 height: 20px;
1467 height: 20px;
1430 }
1468 }
1431 .compare_view_commits td {
1469 .compare_view_commits td {
1432 vertical-align: top;
1470 vertical-align: top;
1433 padding-top: 10px;
1471 padding-top: 10px;
1434 }
1472 }
1435 .compare_view_commits .author {
1473 .compare_view_commits .author {
1436 margin-left: 5px;
1474 margin-left: 5px;
1437 }
1475 }
1438
1476
1439 .compare_view_commits {
1477 .compare_view_commits {
1440 .color-a {
1478 .color-a {
1441 color: @alert1;
1479 color: @alert1;
1442 }
1480 }
1443
1481
1444 .color-c {
1482 .color-c {
1445 color: @color3;
1483 color: @color3;
1446 }
1484 }
1447
1485
1448 .color-r {
1486 .color-r {
1449 color: @color5;
1487 color: @color5;
1450 }
1488 }
1451
1489
1452 .color-a-bg {
1490 .color-a-bg {
1453 background-color: @alert1;
1491 background-color: @alert1;
1454 }
1492 }
1455
1493
1456 .color-c-bg {
1494 .color-c-bg {
1457 background-color: @alert3;
1495 background-color: @alert3;
1458 }
1496 }
1459
1497
1460 .color-r-bg {
1498 .color-r-bg {
1461 background-color: @alert2;
1499 background-color: @alert2;
1462 }
1500 }
1463
1501
1464 .color-a-border {
1502 .color-a-border {
1465 border: 1px solid @alert1;
1503 border: 1px solid @alert1;
1466 }
1504 }
1467
1505
1468 .color-c-border {
1506 .color-c-border {
1469 border: 1px solid @alert3;
1507 border: 1px solid @alert3;
1470 }
1508 }
1471
1509
1472 .color-r-border {
1510 .color-r-border {
1473 border: 1px solid @alert2;
1511 border: 1px solid @alert2;
1474 }
1512 }
1475
1513
1476 .commit-change-indicator {
1514 .commit-change-indicator {
1477 width: 15px;
1515 width: 15px;
1478 height: 15px;
1516 height: 15px;
1479 position: relative;
1517 position: relative;
1480 left: 15px;
1518 left: 15px;
1481 }
1519 }
1482
1520
1483 .commit-change-content {
1521 .commit-change-content {
1484 text-align: center;
1522 text-align: center;
1485 vertical-align: middle;
1523 vertical-align: middle;
1486 line-height: 15px;
1524 line-height: 15px;
1487 }
1525 }
1488 }
1526 }
1489
1527
1490 .compare_view_files {
1528 .compare_view_files {
1491 width: 100%;
1529 width: 100%;
1492
1530
1493 td {
1531 td {
1494 vertical-align: middle;
1532 vertical-align: middle;
1495 }
1533 }
1496 }
1534 }
1497
1535
1498 .compare_view_filepath {
1536 .compare_view_filepath {
1499 color: @grey1;
1537 color: @grey1;
1500 }
1538 }
1501
1539
1502 .show_more {
1540 .show_more {
1503 display: inline-block;
1541 display: inline-block;
1504 position: relative;
1542 position: relative;
1505 vertical-align: middle;
1543 vertical-align: middle;
1506 width: 4px;
1544 width: 4px;
1507 height: @basefontsize;
1545 height: @basefontsize;
1508
1546
1509 &:after {
1547 &:after {
1510 content: "\00A0\25BE";
1548 content: "\00A0\25BE";
1511 display: inline-block;
1549 display: inline-block;
1512 width:10px;
1550 width:10px;
1513 line-height: 5px;
1551 line-height: 5px;
1514 font-size: 12px;
1552 font-size: 12px;
1515 cursor: pointer;
1553 cursor: pointer;
1516 }
1554 }
1517 }
1555 }
1518
1556
1519 .journal_more .show_more {
1557 .journal_more .show_more {
1520 display: inline;
1558 display: inline;
1521
1559
1522 &:after {
1560 &:after {
1523 content: none;
1561 content: none;
1524 }
1562 }
1525 }
1563 }
1526
1564
1527 .open .show_more:after,
1565 .open .show_more:after,
1528 .select2-dropdown-open .show_more:after {
1566 .select2-dropdown-open .show_more:after {
1529 .rotate(180deg);
1567 .rotate(180deg);
1530 margin-left: 4px;
1568 margin-left: 4px;
1531 }
1569 }
1532
1570
1533
1571
1534 .compare_view_commits .collapse_commit:after {
1572 .compare_view_commits .collapse_commit:after {
1535 cursor: pointer;
1573 cursor: pointer;
1536 content: "\00A0\25B4";
1574 content: "\00A0\25B4";
1537 margin-left: -3px;
1575 margin-left: -3px;
1538 font-size: 17px;
1576 font-size: 17px;
1539 color: @grey4;
1577 color: @grey4;
1540 }
1578 }
1541
1579
1542 .diff_links {
1580 .diff_links {
1543 margin-left: 8px;
1581 margin-left: 8px;
1544 }
1582 }
1545
1583
1546 div.ancestor {
1584 div.ancestor {
1547 margin: -30px 0px;
1585 margin: -30px 0px;
1548 }
1586 }
1549
1587
1550 .cs_icon_td input[type="checkbox"] {
1588 .cs_icon_td input[type="checkbox"] {
1551 display: none;
1589 display: none;
1552 }
1590 }
1553
1591
1554 .cs_icon_td .expand_file_icon:after {
1592 .cs_icon_td .expand_file_icon:after {
1555 cursor: pointer;
1593 cursor: pointer;
1556 content: "\00A0\25B6";
1594 content: "\00A0\25B6";
1557 font-size: 12px;
1595 font-size: 12px;
1558 color: @grey4;
1596 color: @grey4;
1559 }
1597 }
1560
1598
1561 .cs_icon_td .collapse_file_icon:after {
1599 .cs_icon_td .collapse_file_icon:after {
1562 cursor: pointer;
1600 cursor: pointer;
1563 content: "\00A0\25BC";
1601 content: "\00A0\25BC";
1564 font-size: 12px;
1602 font-size: 12px;
1565 color: @grey4;
1603 color: @grey4;
1566 }
1604 }
1567
1605
1568 /*new binary
1606 /*new binary
1569 NEW_FILENODE = 1
1607 NEW_FILENODE = 1
1570 DEL_FILENODE = 2
1608 DEL_FILENODE = 2
1571 MOD_FILENODE = 3
1609 MOD_FILENODE = 3
1572 RENAMED_FILENODE = 4
1610 RENAMED_FILENODE = 4
1573 COPIED_FILENODE = 5
1611 COPIED_FILENODE = 5
1574 CHMOD_FILENODE = 6
1612 CHMOD_FILENODE = 6
1575 BIN_FILENODE = 7
1613 BIN_FILENODE = 7
1576 */
1614 */
1577 .cs_files_expand {
1615 .cs_files_expand {
1578 font-size: @basefontsize + 5px;
1616 font-size: @basefontsize + 5px;
1579 line-height: 1.8em;
1617 line-height: 1.8em;
1580 float: right;
1618 float: right;
1581 }
1619 }
1582
1620
1583 .cs_files_expand span{
1621 .cs_files_expand span{
1584 color: @rcblue;
1622 color: @rcblue;
1585 cursor: pointer;
1623 cursor: pointer;
1586 }
1624 }
1587 .cs_files {
1625 .cs_files {
1588 clear: both;
1626 clear: both;
1589 padding-bottom: @padding;
1627 padding-bottom: @padding;
1590
1628
1591 .cur_cs {
1629 .cur_cs {
1592 margin: 10px 2px;
1630 margin: 10px 2px;
1593 font-weight: bold;
1631 font-weight: bold;
1594 }
1632 }
1595
1633
1596 .node {
1634 .node {
1597 float: left;
1635 float: left;
1598 }
1636 }
1599
1637
1600 .changes {
1638 .changes {
1601 float: right;
1639 float: right;
1602 color: white;
1640 color: white;
1603 font-size: @basefontsize - 4px;
1641 font-size: @basefontsize - 4px;
1604 margin-top: 4px;
1642 margin-top: 4px;
1605 opacity: 0.6;
1643 opacity: 0.6;
1606 filter: Alpha(opacity=60); /* IE8 and earlier */
1644 filter: Alpha(opacity=60); /* IE8 and earlier */
1607
1645
1608 .added {
1646 .added {
1609 background-color: @alert1;
1647 background-color: @alert1;
1610 float: left;
1648 float: left;
1611 text-align: center;
1649 text-align: center;
1612 }
1650 }
1613
1651
1614 .deleted {
1652 .deleted {
1615 background-color: @alert2;
1653 background-color: @alert2;
1616 float: left;
1654 float: left;
1617 text-align: center;
1655 text-align: center;
1618 }
1656 }
1619
1657
1620 .bin {
1658 .bin {
1621 background-color: @alert1;
1659 background-color: @alert1;
1622 text-align: center;
1660 text-align: center;
1623 }
1661 }
1624
1662
1625 /*new binary*/
1663 /*new binary*/
1626 .bin.bin1 {
1664 .bin.bin1 {
1627 background-color: @alert1;
1665 background-color: @alert1;
1628 text-align: center;
1666 text-align: center;
1629 }
1667 }
1630
1668
1631 /*deleted binary*/
1669 /*deleted binary*/
1632 .bin.bin2 {
1670 .bin.bin2 {
1633 background-color: @alert2;
1671 background-color: @alert2;
1634 text-align: center;
1672 text-align: center;
1635 }
1673 }
1636
1674
1637 /*mod binary*/
1675 /*mod binary*/
1638 .bin.bin3 {
1676 .bin.bin3 {
1639 background-color: @grey2;
1677 background-color: @grey2;
1640 text-align: center;
1678 text-align: center;
1641 }
1679 }
1642
1680
1643 /*rename file*/
1681 /*rename file*/
1644 .bin.bin4 {
1682 .bin.bin4 {
1645 background-color: @alert4;
1683 background-color: @alert4;
1646 text-align: center;
1684 text-align: center;
1647 }
1685 }
1648
1686
1649 /*copied file*/
1687 /*copied file*/
1650 .bin.bin5 {
1688 .bin.bin5 {
1651 background-color: @alert4;
1689 background-color: @alert4;
1652 text-align: center;
1690 text-align: center;
1653 }
1691 }
1654
1692
1655 /*chmod file*/
1693 /*chmod file*/
1656 .bin.bin6 {
1694 .bin.bin6 {
1657 background-color: @grey2;
1695 background-color: @grey2;
1658 text-align: center;
1696 text-align: center;
1659 }
1697 }
1660 }
1698 }
1661 }
1699 }
1662
1700
1663 .cs_files .cs_added, .cs_files .cs_A,
1701 .cs_files .cs_added, .cs_files .cs_A,
1664 .cs_files .cs_added, .cs_files .cs_M,
1702 .cs_files .cs_added, .cs_files .cs_M,
1665 .cs_files .cs_added, .cs_files .cs_D {
1703 .cs_files .cs_added, .cs_files .cs_D {
1666 height: 16px;
1704 height: 16px;
1667 padding-right: 10px;
1705 padding-right: 10px;
1668 margin-top: 7px;
1706 margin-top: 7px;
1669 text-align: left;
1707 text-align: left;
1670 }
1708 }
1671
1709
1672 .cs_icon_td {
1710 .cs_icon_td {
1673 min-width: 16px;
1711 min-width: 16px;
1674 width: 16px;
1712 width: 16px;
1675 }
1713 }
1676
1714
1677 .pull-request-merge {
1715 .pull-request-merge {
1678 border: 1px solid @grey5;
1716 border: 1px solid @grey5;
1679 padding: 10px 0px 20px;
1717 padding: 10px 0px 20px;
1680 margin-top: 10px;
1718 margin-top: 10px;
1681 margin-bottom: 20px;
1719 margin-bottom: 20px;
1682 }
1720 }
1683
1721
1684 .pull-request-merge ul {
1722 .pull-request-merge ul {
1685 padding: 0px 0px;
1723 padding: 0px 0px;
1686 }
1724 }
1687
1725
1688 .pull-request-merge li:before{
1726 .pull-request-merge li:before{
1689 content:none;
1727 content:none;
1690 }
1728 }
1691
1729
1692 .pull-request-merge .pull-request-wrap {
1730 .pull-request-merge .pull-request-wrap {
1693 height: auto;
1731 height: auto;
1694 padding: 0px 0px;
1732 padding: 0px 0px;
1695 text-align: right;
1733 text-align: right;
1696 }
1734 }
1697
1735
1698 .pull-request-merge span {
1736 .pull-request-merge span {
1699 margin-right: 5px;
1737 margin-right: 5px;
1700 }
1738 }
1701
1739
1702 .pull-request-merge-actions {
1740 .pull-request-merge-actions {
1703 height: 30px;
1741 height: 30px;
1704 padding: 0px 0px;
1742 padding: 0px 0px;
1705 }
1743 }
1706
1744
1707 .merge-status {
1745 .merge-status {
1708 margin-right: 5px;
1746 margin-right: 5px;
1709 }
1747 }
1710
1748
1711 .merge-message {
1749 .merge-message {
1712 font-size: 1.2em
1750 font-size: 1.2em
1713 }
1751 }
1714
1752
1715 .merge-message.success i,
1753 .merge-message.success i,
1716 .merge-icon.success i {
1754 .merge-icon.success i {
1717 color:@alert1;
1755 color:@alert1;
1718 }
1756 }
1719
1757
1720 .merge-message.warning i,
1758 .merge-message.warning i,
1721 .merge-icon.warning i {
1759 .merge-icon.warning i {
1722 color: @alert3;
1760 color: @alert3;
1723 }
1761 }
1724
1762
1725 .merge-message.error i,
1763 .merge-message.error i,
1726 .merge-icon.error i {
1764 .merge-icon.error i {
1727 color:@alert2;
1765 color:@alert2;
1728 }
1766 }
1729
1767
1730 .pr-versions {
1768 .pr-versions {
1731 font-size: 1.1em;
1769 font-size: 1.1em;
1732
1770
1733 table {
1771 table {
1734 padding: 0px 5px;
1772 padding: 0px 5px;
1735 }
1773 }
1736
1774
1737 td {
1775 td {
1738 line-height: 15px;
1776 line-height: 15px;
1739 }
1777 }
1740
1778
1741 .flag_status {
1779 .flag_status {
1742 margin: 0;
1780 margin: 0;
1743 }
1781 }
1744
1782
1745 .compare-radio-button {
1783 .compare-radio-button {
1746 position: relative;
1784 position: relative;
1747 top: -3px;
1785 top: -3px;
1748 }
1786 }
1749 }
1787 }
1750
1788
1751
1789
1752 #close_pull_request {
1790 #close_pull_request {
1753 margin-right: 0px;
1791 margin-right: 0px;
1754 }
1792 }
1755
1793
1756 .empty_data {
1794 .empty_data {
1757 color: @grey4;
1795 color: @grey4;
1758 }
1796 }
1759
1797
1760 #changeset_compare_view_content {
1798 #changeset_compare_view_content {
1761 margin-bottom: @space;
1799 margin-bottom: @space;
1762 clear: both;
1800 clear: both;
1763 width: 100%;
1801 width: 100%;
1764 box-sizing: border-box;
1802 box-sizing: border-box;
1765 .border-radius(@border-radius);
1803 .border-radius(@border-radius);
1766
1804
1767 .help-block {
1805 .help-block {
1768 margin: @padding 0;
1806 margin: @padding 0;
1769 color: @text-color;
1807 color: @text-color;
1770 }
1808 }
1771
1809
1772 .empty_data {
1810 .empty_data {
1773 margin: @padding 0;
1811 margin: @padding 0;
1774 }
1812 }
1775
1813
1776 .alert {
1814 .alert {
1777 margin-bottom: @space;
1815 margin-bottom: @space;
1778 }
1816 }
1779 }
1817 }
1780
1818
1781 .table_disp {
1819 .table_disp {
1782 .status {
1820 .status {
1783 width: auto;
1821 width: auto;
1784
1822
1785 .flag_status {
1823 .flag_status {
1786 float: left;
1824 float: left;
1787 }
1825 }
1788 }
1826 }
1789 }
1827 }
1790
1828
1791 .status_box_menu {
1829 .status_box_menu {
1792 margin: 0;
1830 margin: 0;
1793 }
1831 }
1794
1832
1795 .notification-table{
1833 .notification-table{
1796 margin-bottom: @space;
1834 margin-bottom: @space;
1797 display: table;
1835 display: table;
1798 width: 100%;
1836 width: 100%;
1799
1837
1800 .container{
1838 .container{
1801 display: table-row;
1839 display: table-row;
1802
1840
1803 .notification-header{
1841 .notification-header{
1804 border-bottom: @border-thickness solid @border-default-color;
1842 border-bottom: @border-thickness solid @border-default-color;
1805 }
1843 }
1806
1844
1807 .notification-subject{
1845 .notification-subject{
1808 display: table-cell;
1846 display: table-cell;
1809 }
1847 }
1810 }
1848 }
1811 }
1849 }
1812
1850
1813 // Notifications
1851 // Notifications
1814 .notification-header{
1852 .notification-header{
1815 display: table;
1853 display: table;
1816 width: 100%;
1854 width: 100%;
1817 padding: floor(@basefontsize/2) 0;
1855 padding: floor(@basefontsize/2) 0;
1818 line-height: 1em;
1856 line-height: 1em;
1819
1857
1820 .desc, .delete-notifications, .read-notifications{
1858 .desc, .delete-notifications, .read-notifications{
1821 display: table-cell;
1859 display: table-cell;
1822 text-align: left;
1860 text-align: left;
1823 }
1861 }
1824
1862
1825 .desc{
1863 .desc{
1826 width: 1163px;
1864 width: 1163px;
1827 }
1865 }
1828
1866
1829 .delete-notifications, .read-notifications{
1867 .delete-notifications, .read-notifications{
1830 width: 35px;
1868 width: 35px;
1831 min-width: 35px; //fixes when only one button is displayed
1869 min-width: 35px; //fixes when only one button is displayed
1832 }
1870 }
1833 }
1871 }
1834
1872
1835 .notification-body {
1873 .notification-body {
1836 .markdown-block,
1874 .markdown-block,
1837 .rst-block {
1875 .rst-block {
1838 padding: @padding 0;
1876 padding: @padding 0;
1839 }
1877 }
1840
1878
1841 .notification-subject {
1879 .notification-subject {
1842 padding: @textmargin 0;
1880 padding: @textmargin 0;
1843 border-bottom: @border-thickness solid @border-default-color;
1881 border-bottom: @border-thickness solid @border-default-color;
1844 }
1882 }
1845 }
1883 }
1846
1884
1847
1885
1848 .notifications_buttons{
1886 .notifications_buttons{
1849 float: right;
1887 float: right;
1850 }
1888 }
1851
1889
1852 #notification-status{
1890 #notification-status{
1853 display: inline;
1891 display: inline;
1854 }
1892 }
1855
1893
1856 // Repositories
1894 // Repositories
1857
1895
1858 #summary.fields{
1896 #summary.fields{
1859 display: table;
1897 display: table;
1860
1898
1861 .field{
1899 .field{
1862 display: table-row;
1900 display: table-row;
1863
1901
1864 .label-summary{
1902 .label-summary{
1865 display: table-cell;
1903 display: table-cell;
1866 min-width: @label-summary-minwidth;
1904 min-width: @label-summary-minwidth;
1867 padding-top: @padding/2;
1905 padding-top: @padding/2;
1868 padding-bottom: @padding/2;
1906 padding-bottom: @padding/2;
1869 padding-right: @padding/2;
1907 padding-right: @padding/2;
1870 }
1908 }
1871
1909
1872 .input{
1910 .input{
1873 display: table-cell;
1911 display: table-cell;
1874 padding: @padding/2;
1912 padding: @padding/2;
1875
1913
1876 input{
1914 input{
1877 min-width: 29em;
1915 min-width: 29em;
1878 padding: @padding/4;
1916 padding: @padding/4;
1879 }
1917 }
1880 }
1918 }
1881 .statistics, .downloads{
1919 .statistics, .downloads{
1882 .disabled{
1920 .disabled{
1883 color: @grey4;
1921 color: @grey4;
1884 }
1922 }
1885 }
1923 }
1886 }
1924 }
1887 }
1925 }
1888
1926
1889 #summary{
1927 #summary{
1890 width: 70%;
1928 width: 70%;
1891 }
1929 }
1892
1930
1893
1931
1894 // Journal
1932 // Journal
1895 .journal.title {
1933 .journal.title {
1896 h5 {
1934 h5 {
1897 float: left;
1935 float: left;
1898 margin: 0;
1936 margin: 0;
1899 width: 70%;
1937 width: 70%;
1900 }
1938 }
1901
1939
1902 ul {
1940 ul {
1903 float: right;
1941 float: right;
1904 display: inline-block;
1942 display: inline-block;
1905 margin: 0;
1943 margin: 0;
1906 width: 30%;
1944 width: 30%;
1907 text-align: right;
1945 text-align: right;
1908
1946
1909 li {
1947 li {
1910 display: inline;
1948 display: inline;
1911 font-size: @journal-fontsize;
1949 font-size: @journal-fontsize;
1912 line-height: 1em;
1950 line-height: 1em;
1913
1951
1914 &:before { content: none; }
1952 &:before { content: none; }
1915 }
1953 }
1916 }
1954 }
1917 }
1955 }
1918
1956
1919 .filterexample {
1957 .filterexample {
1920 position: absolute;
1958 position: absolute;
1921 top: 95px;
1959 top: 95px;
1922 left: @contentpadding;
1960 left: @contentpadding;
1923 color: @rcblue;
1961 color: @rcblue;
1924 font-size: 11px;
1962 font-size: 11px;
1925 font-family: @text-regular;
1963 font-family: @text-regular;
1926 cursor: help;
1964 cursor: help;
1927
1965
1928 &:hover {
1966 &:hover {
1929 color: @rcdarkblue;
1967 color: @rcdarkblue;
1930 }
1968 }
1931
1969
1932 @media (max-width:768px) {
1970 @media (max-width:768px) {
1933 position: relative;
1971 position: relative;
1934 top: auto;
1972 top: auto;
1935 left: auto;
1973 left: auto;
1936 display: block;
1974 display: block;
1937 }
1975 }
1938 }
1976 }
1939
1977
1940
1978
1941 #journal{
1979 #journal{
1942 margin-bottom: @space;
1980 margin-bottom: @space;
1943
1981
1944 .journal_day{
1982 .journal_day{
1945 margin-bottom: @textmargin/2;
1983 margin-bottom: @textmargin/2;
1946 padding-bottom: @textmargin/2;
1984 padding-bottom: @textmargin/2;
1947 font-size: @journal-fontsize;
1985 font-size: @journal-fontsize;
1948 border-bottom: @border-thickness solid @border-default-color;
1986 border-bottom: @border-thickness solid @border-default-color;
1949 }
1987 }
1950
1988
1951 .journal_container{
1989 .journal_container{
1952 margin-bottom: @space;
1990 margin-bottom: @space;
1953
1991
1954 .journal_user{
1992 .journal_user{
1955 display: inline-block;
1993 display: inline-block;
1956 }
1994 }
1957 .journal_action_container{
1995 .journal_action_container{
1958 display: block;
1996 display: block;
1959 margin-top: @textmargin;
1997 margin-top: @textmargin;
1960
1998
1961 div{
1999 div{
1962 display: inline;
2000 display: inline;
1963 }
2001 }
1964
2002
1965 div.journal_action_params{
2003 div.journal_action_params{
1966 display: block;
2004 display: block;
1967 }
2005 }
1968
2006
1969 div.journal_repo:after{
2007 div.journal_repo:after{
1970 content: "\A";
2008 content: "\A";
1971 white-space: pre;
2009 white-space: pre;
1972 }
2010 }
1973
2011
1974 div.date{
2012 div.date{
1975 display: block;
2013 display: block;
1976 margin-bottom: @textmargin;
2014 margin-bottom: @textmargin;
1977 }
2015 }
1978 }
2016 }
1979 }
2017 }
1980 }
2018 }
1981
2019
1982 // Files
2020 // Files
1983 .edit-file-title {
2021 .edit-file-title {
1984 border-bottom: @border-thickness solid @border-default-color;
2022 border-bottom: @border-thickness solid @border-default-color;
1985
2023
1986 .breadcrumbs {
2024 .breadcrumbs {
1987 margin-bottom: 0;
2025 margin-bottom: 0;
1988 }
2026 }
1989 }
2027 }
1990
2028
1991 .edit-file-fieldset {
2029 .edit-file-fieldset {
1992 margin-top: @sidebarpadding;
2030 margin-top: @sidebarpadding;
1993
2031
1994 .fieldset {
2032 .fieldset {
1995 .left-label {
2033 .left-label {
1996 width: 13%;
2034 width: 13%;
1997 }
2035 }
1998 .right-content {
2036 .right-content {
1999 width: 87%;
2037 width: 87%;
2000 max-width: 100%;
2038 max-width: 100%;
2001 }
2039 }
2002 .filename-label {
2040 .filename-label {
2003 margin-top: 13px;
2041 margin-top: 13px;
2004 }
2042 }
2005 .commit-message-label {
2043 .commit-message-label {
2006 margin-top: 4px;
2044 margin-top: 4px;
2007 }
2045 }
2008 .file-upload-input {
2046 .file-upload-input {
2009 input {
2047 input {
2010 display: none;
2048 display: none;
2011 }
2049 }
2012 margin-top: 10px;
2050 margin-top: 10px;
2013 }
2051 }
2014 .file-upload-label {
2052 .file-upload-label {
2015 margin-top: 10px;
2053 margin-top: 10px;
2016 }
2054 }
2017 p {
2055 p {
2018 margin-top: 5px;
2056 margin-top: 5px;
2019 }
2057 }
2020
2058
2021 }
2059 }
2022 .custom-path-link {
2060 .custom-path-link {
2023 margin-left: 5px;
2061 margin-left: 5px;
2024 }
2062 }
2025 #commit {
2063 #commit {
2026 resize: vertical;
2064 resize: vertical;
2027 }
2065 }
2028 }
2066 }
2029
2067
2030 .delete-file-preview {
2068 .delete-file-preview {
2031 max-height: 250px;
2069 max-height: 250px;
2032 }
2070 }
2033
2071
2034 .new-file,
2072 .new-file,
2035 #filter_activate,
2073 #filter_activate,
2036 #filter_deactivate {
2074 #filter_deactivate {
2037 float: left;
2075 float: left;
2038 margin: 0 0 0 15px;
2076 margin: 0 0 0 15px;
2039 }
2077 }
2040
2078
2041 h3.files_location{
2079 h3.files_location{
2042 line-height: 2.4em;
2080 line-height: 2.4em;
2043 }
2081 }
2044
2082
2045 .browser-nav {
2083 .browser-nav {
2046 display: table;
2084 display: table;
2047 margin-bottom: @space;
2085 margin-bottom: @space;
2048
2086
2049
2087
2050 .info_box {
2088 .info_box {
2051 display: inline-table;
2089 display: inline-table;
2052 height: 2.5em;
2090 height: 2.5em;
2053
2091
2054 .browser-cur-rev, .info_box_elem {
2092 .browser-cur-rev, .info_box_elem {
2055 display: table-cell;
2093 display: table-cell;
2056 vertical-align: middle;
2094 vertical-align: middle;
2057 }
2095 }
2058
2096
2059 .info_box_elem {
2097 .info_box_elem {
2060 border-top: @border-thickness solid @rcblue;
2098 border-top: @border-thickness solid @rcblue;
2061 border-bottom: @border-thickness solid @rcblue;
2099 border-bottom: @border-thickness solid @rcblue;
2062
2100
2063 #at_rev, a {
2101 #at_rev, a {
2064 padding: 0.6em 0.9em;
2102 padding: 0.6em 0.9em;
2065 margin: 0;
2103 margin: 0;
2066 .box-shadow(none);
2104 .box-shadow(none);
2067 border: 0;
2105 border: 0;
2068 height: 12px;
2106 height: 12px;
2069 }
2107 }
2070
2108
2071 input#at_rev {
2109 input#at_rev {
2072 max-width: 50px;
2110 max-width: 50px;
2073 text-align: right;
2111 text-align: right;
2074 }
2112 }
2075
2113
2076 &.previous {
2114 &.previous {
2077 border: @border-thickness solid @rcblue;
2115 border: @border-thickness solid @rcblue;
2078 .disabled {
2116 .disabled {
2079 color: @grey4;
2117 color: @grey4;
2080 cursor: not-allowed;
2118 cursor: not-allowed;
2081 }
2119 }
2082 }
2120 }
2083
2121
2084 &.next {
2122 &.next {
2085 border: @border-thickness solid @rcblue;
2123 border: @border-thickness solid @rcblue;
2086 .disabled {
2124 .disabled {
2087 color: @grey4;
2125 color: @grey4;
2088 cursor: not-allowed;
2126 cursor: not-allowed;
2089 }
2127 }
2090 }
2128 }
2091 }
2129 }
2092
2130
2093 .browser-cur-rev {
2131 .browser-cur-rev {
2094
2132
2095 span{
2133 span{
2096 margin: 0;
2134 margin: 0;
2097 color: @rcblue;
2135 color: @rcblue;
2098 height: 12px;
2136 height: 12px;
2099 display: inline-block;
2137 display: inline-block;
2100 padding: 0.7em 1em ;
2138 padding: 0.7em 1em ;
2101 border: @border-thickness solid @rcblue;
2139 border: @border-thickness solid @rcblue;
2102 margin-right: @padding;
2140 margin-right: @padding;
2103 }
2141 }
2104 }
2142 }
2105 }
2143 }
2106
2144
2107 .search_activate {
2145 .search_activate {
2108 display: table-cell;
2146 display: table-cell;
2109 vertical-align: middle;
2147 vertical-align: middle;
2110
2148
2111 input, label{
2149 input, label{
2112 margin: 0;
2150 margin: 0;
2113 padding: 0;
2151 padding: 0;
2114 }
2152 }
2115
2153
2116 input{
2154 input{
2117 margin-left: @textmargin;
2155 margin-left: @textmargin;
2118 }
2156 }
2119
2157
2120 }
2158 }
2121 }
2159 }
2122
2160
2123 .browser-cur-rev{
2161 .browser-cur-rev{
2124 margin-bottom: @textmargin;
2162 margin-bottom: @textmargin;
2125 }
2163 }
2126
2164
2127 #node_filter_box_loading{
2165 #node_filter_box_loading{
2128 .info_text;
2166 .info_text;
2129 }
2167 }
2130
2168
2131 .browser-search {
2169 .browser-search {
2132 margin: -25px 0px 5px 0px;
2170 margin: -25px 0px 5px 0px;
2133 }
2171 }
2134
2172
2135 .node-filter {
2173 .node-filter {
2136 font-size: @repo-title-fontsize;
2174 font-size: @repo-title-fontsize;
2137 padding: 4px 0px 0px 0px;
2175 padding: 4px 0px 0px 0px;
2138
2176
2139 .node-filter-path {
2177 .node-filter-path {
2140 float: left;
2178 float: left;
2141 color: @grey4;
2179 color: @grey4;
2142 }
2180 }
2143 .node-filter-input {
2181 .node-filter-input {
2144 float: left;
2182 float: left;
2145 margin: -2px 0px 0px 2px;
2183 margin: -2px 0px 0px 2px;
2146 input {
2184 input {
2147 padding: 2px;
2185 padding: 2px;
2148 border: none;
2186 border: none;
2149 font-size: @repo-title-fontsize;
2187 font-size: @repo-title-fontsize;
2150 }
2188 }
2151 }
2189 }
2152 }
2190 }
2153
2191
2154
2192
2155 .browser-result{
2193 .browser-result{
2156 td a{
2194 td a{
2157 margin-left: 0.5em;
2195 margin-left: 0.5em;
2158 display: inline-block;
2196 display: inline-block;
2159
2197
2160 em{
2198 em{
2161 font-family: @text-bold;
2199 font-family: @text-bold;
2162 }
2200 }
2163 }
2201 }
2164 }
2202 }
2165
2203
2166 .browser-highlight{
2204 .browser-highlight{
2167 background-color: @grey5-alpha;
2205 background-color: @grey5-alpha;
2168 }
2206 }
2169
2207
2170
2208
2171 // Search
2209 // Search
2172
2210
2173 .search-form{
2211 .search-form{
2174 #q {
2212 #q {
2175 width: @search-form-width;
2213 width: @search-form-width;
2176 }
2214 }
2177 .fields{
2215 .fields{
2178 margin: 0 0 @space;
2216 margin: 0 0 @space;
2179 }
2217 }
2180
2218
2181 label{
2219 label{
2182 display: inline-block;
2220 display: inline-block;
2183 margin-right: @textmargin;
2221 margin-right: @textmargin;
2184 padding-top: 0.25em;
2222 padding-top: 0.25em;
2185 }
2223 }
2186
2224
2187
2225
2188 .results{
2226 .results{
2189 clear: both;
2227 clear: both;
2190 margin: 0 0 @padding;
2228 margin: 0 0 @padding;
2191 }
2229 }
2192 }
2230 }
2193
2231
2194 div.search-feedback-items {
2232 div.search-feedback-items {
2195 display: inline-block;
2233 display: inline-block;
2196 padding:0px 0px 0px 96px;
2234 padding:0px 0px 0px 96px;
2197 }
2235 }
2198
2236
2199 div.search-code-body {
2237 div.search-code-body {
2200 background-color: #ffffff; padding: 5px 0 5px 10px;
2238 background-color: #ffffff; padding: 5px 0 5px 10px;
2201 pre {
2239 pre {
2202 .match { background-color: #faffa6;}
2240 .match { background-color: #faffa6;}
2203 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2241 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2204 }
2242 }
2205 }
2243 }
2206
2244
2207 .expand_commit.search {
2245 .expand_commit.search {
2208 .show_more.open {
2246 .show_more.open {
2209 height: auto;
2247 height: auto;
2210 max-height: none;
2248 max-height: none;
2211 }
2249 }
2212 }
2250 }
2213
2251
2214 .search-results {
2252 .search-results {
2215
2253
2216 h2 {
2254 h2 {
2217 margin-bottom: 0;
2255 margin-bottom: 0;
2218 }
2256 }
2219 .codeblock {
2257 .codeblock {
2220 border: none;
2258 border: none;
2221 background: transparent;
2259 background: transparent;
2222 }
2260 }
2223
2261
2224 .codeblock-header {
2262 .codeblock-header {
2225 border: none;
2263 border: none;
2226 background: transparent;
2264 background: transparent;
2227 }
2265 }
2228
2266
2229 .code-body {
2267 .code-body {
2230 border: @border-thickness solid @border-default-color;
2268 border: @border-thickness solid @border-default-color;
2231 .border-radius(@border-radius);
2269 .border-radius(@border-radius);
2232 }
2270 }
2233
2271
2234 .td-commit {
2272 .td-commit {
2235 &:extend(pre);
2273 &:extend(pre);
2236 border-bottom: @border-thickness solid @border-default-color;
2274 border-bottom: @border-thickness solid @border-default-color;
2237 }
2275 }
2238
2276
2239 .message {
2277 .message {
2240 height: auto;
2278 height: auto;
2241 max-width: 350px;
2279 max-width: 350px;
2242 white-space: normal;
2280 white-space: normal;
2243 text-overflow: initial;
2281 text-overflow: initial;
2244 overflow: visible;
2282 overflow: visible;
2245
2283
2246 .match { background-color: #faffa6;}
2284 .match { background-color: #faffa6;}
2247 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2285 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2248 }
2286 }
2249
2287
2250 }
2288 }
2251
2289
2252 table.rctable td.td-search-results div {
2290 table.rctable td.td-search-results div {
2253 max-width: 100%;
2291 max-width: 100%;
2254 }
2292 }
2255
2293
2256 #tip-box, .tip-box{
2294 #tip-box, .tip-box{
2257 padding: @menupadding/2;
2295 padding: @menupadding/2;
2258 display: block;
2296 display: block;
2259 border: @border-thickness solid @border-highlight-color;
2297 border: @border-thickness solid @border-highlight-color;
2260 .border-radius(@border-radius);
2298 .border-radius(@border-radius);
2261 background-color: white;
2299 background-color: white;
2262 z-index: 99;
2300 z-index: 99;
2263 white-space: pre-wrap;
2301 white-space: pre-wrap;
2264 }
2302 }
2265
2303
2266 #linktt {
2304 #linktt {
2267 width: 79px;
2305 width: 79px;
2268 }
2306 }
2269
2307
2270 #help_kb .modal-content{
2308 #help_kb .modal-content{
2271 max-width: 750px;
2309 max-width: 750px;
2272 margin: 10% auto;
2310 margin: 10% auto;
2273
2311
2274 table{
2312 table{
2275 td,th{
2313 td,th{
2276 border-bottom: none;
2314 border-bottom: none;
2277 line-height: 2.5em;
2315 line-height: 2.5em;
2278 }
2316 }
2279 th{
2317 th{
2280 padding-bottom: @textmargin/2;
2318 padding-bottom: @textmargin/2;
2281 }
2319 }
2282 td.keys{
2320 td.keys{
2283 text-align: center;
2321 text-align: center;
2284 }
2322 }
2285 }
2323 }
2286
2324
2287 .block-left{
2325 .block-left{
2288 width: 45%;
2326 width: 45%;
2289 margin-right: 5%;
2327 margin-right: 5%;
2290 }
2328 }
2291 .modal-footer{
2329 .modal-footer{
2292 clear: both;
2330 clear: both;
2293 }
2331 }
2294 .key.tag{
2332 .key.tag{
2295 padding: 0.5em;
2333 padding: 0.5em;
2296 background-color: @rcblue;
2334 background-color: @rcblue;
2297 color: white;
2335 color: white;
2298 border-color: @rcblue;
2336 border-color: @rcblue;
2299 .box-shadow(none);
2337 .box-shadow(none);
2300 }
2338 }
2301 }
2339 }
2302
2340
2303
2341
2304
2342
2305 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2343 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2306
2344
2307 @import 'statistics-graph';
2345 @import 'statistics-graph';
2308 @import 'tables';
2346 @import 'tables';
2309 @import 'forms';
2347 @import 'forms';
2310 @import 'diff';
2348 @import 'diff';
2311 @import 'summary';
2349 @import 'summary';
2312 @import 'navigation';
2350 @import 'navigation';
2313
2351
2314 //--- SHOW/HIDE SECTIONS --//
2352 //--- SHOW/HIDE SECTIONS --//
2315
2353
2316 .btn-collapse {
2354 .btn-collapse {
2317 float: right;
2355 float: right;
2318 text-align: right;
2356 text-align: right;
2319 font-family: @text-light;
2357 font-family: @text-light;
2320 font-size: @basefontsize;
2358 font-size: @basefontsize;
2321 cursor: pointer;
2359 cursor: pointer;
2322 border: none;
2360 border: none;
2323 color: @rcblue;
2361 color: @rcblue;
2324 }
2362 }
2325
2363
2326 table.rctable,
2364 table.rctable,
2327 table.dataTable {
2365 table.dataTable {
2328 .btn-collapse {
2366 .btn-collapse {
2329 float: right;
2367 float: right;
2330 text-align: right;
2368 text-align: right;
2331 }
2369 }
2332 }
2370 }
2333
2371
2334
2372
2335 // TODO: johbo: Fix for IE10, this avoids that we see a border
2373 // TODO: johbo: Fix for IE10, this avoids that we see a border
2336 // and padding around checkboxes and radio boxes. Move to the right place,
2374 // and padding around checkboxes and radio boxes. Move to the right place,
2337 // or better: Remove this once we did the form refactoring.
2375 // or better: Remove this once we did the form refactoring.
2338 input[type=checkbox],
2376 input[type=checkbox],
2339 input[type=radio] {
2377 input[type=radio] {
2340 padding: 0;
2378 padding: 0;
2341 border: none;
2379 border: none;
2342 }
2380 }
2343
2381
2344 .toggle-ajax-spinner{
2382 .toggle-ajax-spinner{
2345 height: 16px;
2383 height: 16px;
2346 width: 16px;
2384 width: 16px;
2347 }
2385 }
@@ -1,355 +1,600 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
20 var prButtonLockChecks = {
21 'compare': false,
22 'reviewers': false
23 };
24
19 /**
25 /**
20 * Pull request reviewers
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
28 * @param lockEnabled
29 * @param msg
30 * @param scope
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
45 }
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
48 }
49
50 if (msg) {
51 $('#pr_open_message').html(msg);
52 }
53 };
54
55
56 /**
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
61 - commitN
62 - commitN+1
63 ...
64
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
67
68 * @param sourceRef
69 * @param elements
70 * @param limit
71 * @returns {*[]}
21 */
72 */
22 var removeReviewMember = function(reviewer_id, mark_delete){
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
75 var desc = '';
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
85 else {
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
89
90 return [title, desc]
91 };
92
93
94
95 ReviewersController = function () {
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
102
103 this.defaultForbidReviewUsers = function() {
104 return [
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
107 ];
108 };
109
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
112 };
113
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
116 };
117
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
121 };
122
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
127 // reset state of review rules
128 self.$rulesList.html('');
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
135 );
136 return self.forbidReviewUsers
137 }
138
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
144 )
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
150
151 } else {
152 self.$rulesList.append(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
156 }
157 }
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
162 )
163 }
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
169 )
170 }
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
177 }
178 return self.forbidReviewUsers
179 };
180
181 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
182
183 if (self.currentRequest) {
184 // make sure we cleanup old running requests before triggering this
185 // again
186 self.currentRequest.abort();
187 }
188
189 $('.calculate-reviewers').show();
190 // reset reviewer members
191 self.$reviewMembers.empty();
192
193 prButtonLock(true, null, 'reviewers');
194 $('#user').hide(); // hide user autocomplete before load
195
196 var url = pyroutes.url('repo_default_reviewers_data',
197 {
198 'repo_name': templateContext.repo_name,
199 'source_repo': sourceRepo,
200 'source_ref': sourceRef[2],
201 'target_repo': targetRepo,
202 'target_ref': targetRef[2]
203 });
204
205 self.currentRequest = $.get(url)
206 .done(function(data) {
207 self.currentRequest = null;
208
209 // review rules
210 self.loadReviewRules(data);
211
212 for (var i = 0; i < data.reviewers.length; i++) {
213 var reviewer = data.reviewers[i];
214 self.addReviewMember(
215 reviewer.user_id, reviewer.firstname,
216 reviewer.lastname, reviewer.username,
217 reviewer.gravatar_link, reviewer.reasons,
218 reviewer.mandatory);
219 }
220 $('.calculate-reviewers').hide();
221 prButtonLock(false, null, 'reviewers');
222 $('#user').show(); // show user autocomplete after load
223 });
224 };
225
226 // check those, refactor
227 this.removeReviewMember = function(reviewer_id, mark_delete) {
23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
228 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24
229
25 if(typeof(mark_delete) === undefined){
230 if(typeof(mark_delete) === undefined){
26 mark_delete = false;
231 mark_delete = false;
27 }
232 }
28
233
29 if(mark_delete === true){
234 if(mark_delete === true){
30 if (reviewer){
235 if (reviewer){
31 // now delete the input
236 // now delete the input
32 $('#reviewer_{0} input'.format(reviewer_id)).remove();
237 $('#reviewer_{0} input'.format(reviewer_id)).remove();
33 // mark as to-delete
238 // mark as to-delete
34 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
239 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
35 obj.addClass('to-delete');
240 obj.addClass('to-delete');
36 obj.css({"text-decoration":"line-through", "opacity": 0.5});
241 obj.css({"text-decoration":"line-through", "opacity": 0.5});
37 }
242 }
38 }
243 }
39 else{
244 else{
40 $('#reviewer_{0}'.format(reviewer_id)).remove();
245 $('#reviewer_{0}'.format(reviewer_id)).remove();
41 }
246 }
42 };
247 };
43
248
44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
249 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
45 var members = $('#review_members').get(0);
250 var members = self.$reviewMembers.get(0);
46 var reasons_html = '';
251 var reasons_html = '';
47 var reasons_inputs = '';
252 var reasons_inputs = '';
48 var reasons = reasons || [];
253 var reasons = reasons || [];
254 var mandatory = mandatory || false;
255
49 if (reasons) {
256 if (reasons) {
50 for (var i = 0; i < reasons.length; i++) {
257 for (var i = 0; i < reasons.length; i++) {
51 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
258 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
52 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
259 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
53 }
260 }
54 }
261 }
55 var tmpl = '<li id="reviewer_{2}">'+
262 var tmpl = '' +
263 '<li id="reviewer_{2}" class="reviewer_entry">'+
56 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
264 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
57 '<div class="reviewer_status">'+
265 '<div class="reviewer_status">'+
58 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
266 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
59 '</div>'+
267 '</div>'+
60 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
268 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
61 '<span class="reviewer_name user">{1}</span>'+
269 '<span class="reviewer_name user">{1}</span>'+
62 reasons_html +
270 reasons_html +
63 '<input type="hidden" name="user_id" value="{2}">'+
271 '<input type="hidden" name="user_id" value="{2}">'+
64 '<input type="hidden" name="__start__" value="reasons:sequence">'+
272 '<input type="hidden" name="__start__" value="reasons:sequence">'+
65 '{3}'+
273 '{3}'+
66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
274 '<input type="hidden" name="__end__" value="reasons:sequence">';
67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
275
276 if (mandatory) {
277 tmpl += ''+
278 '<div class="reviewer_member_mandatory_remove">' +
68 '<i class="icon-remove-sign"></i>'+
279 '<i class="icon-remove-sign"></i>'+
69 '</div>'+
280 '</div>' +
70 '</div>'+
281 '<input type="hidden" name="mandatory" value="true">'+
282 '<div class="reviewer_member_mandatory">' +
283 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
284 '</div>';
285
286 } else {
287 tmpl += ''+
288 '<input type="hidden" name="mandatory" value="false">'+
289 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
290 '<i class="icon-remove-sign"></i>'+
291 '</div>';
292 }
293 // continue template
294 tmpl += ''+
71 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
295 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
72 '</li>' ;
296 '</li>' ;
73
297
74 var displayname = "{0} ({1} {2})".format(
298 var displayname = "{0} ({1} {2})".format(
75 nname, escapeHtml(fname), escapeHtml(lname));
299 nname, escapeHtml(fname), escapeHtml(lname));
76 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
300 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
77 // check if we don't have this ID already in
301 // check if we don't have this ID already in
78 var ids = [];
302 var ids = [];
79 var _els = $('#review_members li').toArray();
303 var _els = self.$reviewMembers.find('li').toArray();
80 for (el in _els){
304 for (el in _els){
81 ids.push(_els[el].id)
305 ids.push(_els[el].id)
82 }
306 }
83 if(ids.indexOf('reviewer_'+id) == -1){
307
308 var userAllowedReview = function(userId) {
309 var allowed = true;
310 $.each(self.forbidReviewUsers, function(index, member_data) {
311 if (parseInt(userId) === member_data['user_id']) {
312 allowed = false;
313 return false // breaks the loop
314 }
315 });
316 return allowed
317 };
318
319 var userAllowed = userAllowedReview(id);
320 if (!userAllowed){
321 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
322 }
323 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
324
325 if(shouldAdd) {
84 // only add if it's not there
326 // only add if it's not there
85 members.innerHTML += element;
327 members.innerHTML += element;
86 }
328 }
87
329
88 };
330 };
89
331
332 this.updateReviewers = function(repo_name, pull_request_id){
333 var postData = '_method=put&' + $('#reviewers input').serialize();
334 _updatePullRequest(repo_name, pull_request_id, postData);
335 };
336
337 };
338
339
90 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
340 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
91 var url = pyroutes.url(
341 var url = pyroutes.url(
92 'pullrequest_update',
342 'pullrequest_update',
93 {"repo_name": repo_name, "pull_request_id": pull_request_id});
343 {"repo_name": repo_name, "pull_request_id": pull_request_id});
94 if (typeof postData === 'string' ) {
344 if (typeof postData === 'string' ) {
95 postData += '&csrf_token=' + CSRF_TOKEN;
345 postData += '&csrf_token=' + CSRF_TOKEN;
96 } else {
346 } else {
97 postData.csrf_token = CSRF_TOKEN;
347 postData.csrf_token = CSRF_TOKEN;
98 }
348 }
99 var success = function(o) {
349 var success = function(o) {
100 window.location.reload();
350 window.location.reload();
101 };
351 };
102 ajaxPOST(url, postData, success);
352 ajaxPOST(url, postData, success);
103 };
353 };
104
354
105 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
106 if (reviewers_ids === undefined){
107 var postData = '_method=put&' + $('#reviewers input').serialize();
108 _updatePullRequest(repo_name, pull_request_id, postData);
109 }
110 };
111
112 /**
355 /**
113 * PULL REQUEST reject & close
356 * PULL REQUEST reject & close
114 */
357 */
115 var closePullRequest = function(repo_name, pull_request_id) {
358 var closePullRequest = function(repo_name, pull_request_id) {
116 var postData = {
359 var postData = {
117 '_method': 'put',
360 '_method': 'put',
118 'close_pull_request': true};
361 'close_pull_request': true};
119 _updatePullRequest(repo_name, pull_request_id, postData);
362 _updatePullRequest(repo_name, pull_request_id, postData);
120 };
363 };
121
364
122 /**
365 /**
123 * PULL REQUEST update commits
366 * PULL REQUEST update commits
124 */
367 */
125 var updateCommits = function(repo_name, pull_request_id) {
368 var updateCommits = function(repo_name, pull_request_id) {
126 var postData = {
369 var postData = {
127 '_method': 'put',
370 '_method': 'put',
128 'update_commits': true};
371 'update_commits': true};
129 _updatePullRequest(repo_name, pull_request_id, postData);
372 _updatePullRequest(repo_name, pull_request_id, postData);
130 };
373 };
131
374
132
375
133 /**
376 /**
134 * PULL REQUEST edit info
377 * PULL REQUEST edit info
135 */
378 */
136 var editPullRequest = function(repo_name, pull_request_id, title, description) {
379 var editPullRequest = function(repo_name, pull_request_id, title, description) {
137 var url = pyroutes.url(
380 var url = pyroutes.url(
138 'pullrequest_update',
381 'pullrequest_update',
139 {"repo_name": repo_name, "pull_request_id": pull_request_id});
382 {"repo_name": repo_name, "pull_request_id": pull_request_id});
140
383
141 var postData = {
384 var postData = {
142 '_method': 'put',
385 '_method': 'put',
143 'title': title,
386 'title': title,
144 'description': description,
387 'description': description,
145 'edit_pull_request': true,
388 'edit_pull_request': true,
146 'csrf_token': CSRF_TOKEN
389 'csrf_token': CSRF_TOKEN
147 };
390 };
148 var success = function(o) {
391 var success = function(o) {
149 window.location.reload();
392 window.location.reload();
150 };
393 };
151 ajaxPOST(url, postData, success);
394 ajaxPOST(url, postData, success);
152 };
395 };
153
396
154 var initPullRequestsCodeMirror = function (textAreaId) {
397 var initPullRequestsCodeMirror = function (textAreaId) {
155 var ta = $(textAreaId).get(0);
398 var ta = $(textAreaId).get(0);
156 var initialHeight = '100px';
399 var initialHeight = '100px';
157
400
158 // default options
401 // default options
159 var codeMirrorOptions = {
402 var codeMirrorOptions = {
160 mode: "text",
403 mode: "text",
161 lineNumbers: false,
404 lineNumbers: false,
162 indentUnit: 4,
405 indentUnit: 4,
163 theme: 'rc-input'
406 theme: 'rc-input'
164 };
407 };
165
408
166 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
409 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
167 // marker for manually set description
410 // marker for manually set description
168 codeMirrorInstance._userDefinedDesc = false;
411 codeMirrorInstance._userDefinedDesc = false;
169 codeMirrorInstance.setSize(null, initialHeight);
412 codeMirrorInstance.setSize(null, initialHeight);
170 codeMirrorInstance.on("change", function(instance, changeObj) {
413 codeMirrorInstance.on("change", function(instance, changeObj) {
171 var height = initialHeight;
414 var height = initialHeight;
172 var lines = instance.lineCount();
415 var lines = instance.lineCount();
173 if (lines > 6 && lines < 20) {
416 if (lines > 6 && lines < 20) {
174 height = "auto"
417 height = "auto"
175 }
418 }
176 else if (lines >= 20) {
419 else if (lines >= 20) {
177 height = 20 * 15;
420 height = 20 * 15;
178 }
421 }
179 instance.setSize(null, height);
422 instance.setSize(null, height);
180
423
181 // detect if the change was trigger by auto desc, or user input
424 // detect if the change was trigger by auto desc, or user input
182 changeOrigin = changeObj.origin;
425 changeOrigin = changeObj.origin;
183
426
184 if (changeOrigin === "setValue") {
427 if (changeOrigin === "setValue") {
185 cmLog.debug('Change triggered by setValue');
428 cmLog.debug('Change triggered by setValue');
186 }
429 }
187 else {
430 else {
188 cmLog.debug('user triggered change !');
431 cmLog.debug('user triggered change !');
189 // set special marker to indicate user has created an input.
432 // set special marker to indicate user has created an input.
190 instance._userDefinedDesc = true;
433 instance._userDefinedDesc = true;
191 }
434 }
192
435
193 });
436 });
194
437
195 return codeMirrorInstance
438 return codeMirrorInstance
196 };
439 };
197
440
198 /**
441 /**
199 * Reviewer autocomplete
442 * Reviewer autocomplete
200 */
443 */
201 var ReviewerAutoComplete = function(inputId) {
444 var ReviewerAutoComplete = function(inputId) {
202 $(inputId).autocomplete({
445 $(inputId).autocomplete({
203 serviceUrl: pyroutes.url('user_autocomplete_data'),
446 serviceUrl: pyroutes.url('user_autocomplete_data'),
204 minChars:2,
447 minChars:2,
205 maxHeight:400,
448 maxHeight:400,
206 deferRequestBy: 300, //miliseconds
449 deferRequestBy: 300, //miliseconds
207 showNoSuggestionNotice: true,
450 showNoSuggestionNotice: true,
208 tabDisabled: true,
451 tabDisabled: true,
209 autoSelectFirst: true,
452 autoSelectFirst: true,
210 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true },
453 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
211 formatResult: autocompleteFormatResult,
454 formatResult: autocompleteFormatResult,
212 lookupFilter: autocompleteFilterResult,
455 lookupFilter: autocompleteFilterResult,
213 onSelect: function(element, data) {
456 onSelect: function(element, data) {
214
457
215 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
458 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
216 if (data.value_type == 'user_group') {
459 if (data.value_type == 'user_group') {
217 reasons.push(_gettext('member of "{0}"').format(data.value_display));
460 reasons.push(_gettext('member of "{0}"').format(data.value_display));
218
461
219 $.each(data.members, function(index, member_data) {
462 $.each(data.members, function(index, member_data) {
220 addReviewMember(member_data.id, member_data.first_name, member_data.last_name,
463 reviewersController.addReviewMember(
464 member_data.id, member_data.first_name, member_data.last_name,
221 member_data.username, member_data.icon_link, reasons);
465 member_data.username, member_data.icon_link, reasons);
222 })
466 })
223
467
224 } else {
468 } else {
225 addReviewMember(data.id, data.first_name, data.last_name,
469 reviewersController.addReviewMember(
470 data.id, data.first_name, data.last_name,
226 data.username, data.icon_link, reasons);
471 data.username, data.icon_link, reasons);
227 }
472 }
228
473
229 $(inputId).val('');
474 $(inputId).val('');
230 }
475 }
231 });
476 });
232 };
477 };
233
478
234
479
235 VersionController = function () {
480 VersionController = function () {
236 var self = this;
481 var self = this;
237 this.$verSource = $('input[name=ver_source]');
482 this.$verSource = $('input[name=ver_source]');
238 this.$verTarget = $('input[name=ver_target]');
483 this.$verTarget = $('input[name=ver_target]');
239 this.$showVersionDiff = $('#show-version-diff');
484 this.$showVersionDiff = $('#show-version-diff');
240
485
241 this.adjustRadioSelectors = function (curNode) {
486 this.adjustRadioSelectors = function (curNode) {
242 var getVal = function (item) {
487 var getVal = function (item) {
243 if (item == 'latest') {
488 if (item == 'latest') {
244 return Number.MAX_SAFE_INTEGER
489 return Number.MAX_SAFE_INTEGER
245 }
490 }
246 else {
491 else {
247 return parseInt(item)
492 return parseInt(item)
248 }
493 }
249 };
494 };
250
495
251 var curVal = getVal($(curNode).val());
496 var curVal = getVal($(curNode).val());
252 var cleared = false;
497 var cleared = false;
253
498
254 $.each(self.$verSource, function (index, value) {
499 $.each(self.$verSource, function (index, value) {
255 var elVal = getVal($(value).val());
500 var elVal = getVal($(value).val());
256
501
257 if (elVal > curVal) {
502 if (elVal > curVal) {
258 if ($(value).is(':checked')) {
503 if ($(value).is(':checked')) {
259 cleared = true;
504 cleared = true;
260 }
505 }
261 $(value).attr('disabled', 'disabled');
506 $(value).attr('disabled', 'disabled');
262 $(value).removeAttr('checked');
507 $(value).removeAttr('checked');
263 $(value).css({'opacity': 0.1});
508 $(value).css({'opacity': 0.1});
264 }
509 }
265 else {
510 else {
266 $(value).css({'opacity': 1});
511 $(value).css({'opacity': 1});
267 $(value).removeAttr('disabled');
512 $(value).removeAttr('disabled');
268 }
513 }
269 });
514 });
270
515
271 if (cleared) {
516 if (cleared) {
272 // if we unchecked an active, set the next one to same loc.
517 // if we unchecked an active, set the next one to same loc.
273 $(this.$verSource).filter('[value={0}]'.format(
518 $(this.$verSource).filter('[value={0}]'.format(
274 curVal)).attr('checked', 'checked');
519 curVal)).attr('checked', 'checked');
275 }
520 }
276
521
277 self.setLockAction(false,
522 self.setLockAction(false,
278 $(curNode).data('verPos'),
523 $(curNode).data('verPos'),
279 $(this.$verSource).filter(':checked').data('verPos')
524 $(this.$verSource).filter(':checked').data('verPos')
280 );
525 );
281 };
526 };
282
527
283
528
284 this.attachVersionListener = function () {
529 this.attachVersionListener = function () {
285 self.$verTarget.change(function (e) {
530 self.$verTarget.change(function (e) {
286 self.adjustRadioSelectors(this)
531 self.adjustRadioSelectors(this)
287 });
532 });
288 self.$verSource.change(function (e) {
533 self.$verSource.change(function (e) {
289 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
534 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
290 });
535 });
291 };
536 };
292
537
293 this.init = function () {
538 this.init = function () {
294
539
295 var curNode = self.$verTarget.filter(':checked');
540 var curNode = self.$verTarget.filter(':checked');
296 self.adjustRadioSelectors(curNode);
541 self.adjustRadioSelectors(curNode);
297 self.setLockAction(true);
542 self.setLockAction(true);
298 self.attachVersionListener();
543 self.attachVersionListener();
299
544
300 };
545 };
301
546
302 this.setLockAction = function (state, selectedVersion, otherVersion) {
547 this.setLockAction = function (state, selectedVersion, otherVersion) {
303 var $showVersionDiff = this.$showVersionDiff;
548 var $showVersionDiff = this.$showVersionDiff;
304
549
305 if (state) {
550 if (state) {
306 $showVersionDiff.attr('disabled', 'disabled');
551 $showVersionDiff.attr('disabled', 'disabled');
307 $showVersionDiff.addClass('disabled');
552 $showVersionDiff.addClass('disabled');
308 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
553 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
309 }
554 }
310 else {
555 else {
311 $showVersionDiff.removeAttr('disabled');
556 $showVersionDiff.removeAttr('disabled');
312 $showVersionDiff.removeClass('disabled');
557 $showVersionDiff.removeClass('disabled');
313
558
314 if (selectedVersion == otherVersion) {
559 if (selectedVersion == otherVersion) {
315 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
560 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
316 } else {
561 } else {
317 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
562 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
318 }
563 }
319 }
564 }
320
565
321 };
566 };
322
567
323 this.showVersionDiff = function () {
568 this.showVersionDiff = function () {
324 var target = self.$verTarget.filter(':checked');
569 var target = self.$verTarget.filter(':checked');
325 var source = self.$verSource.filter(':checked');
570 var source = self.$verSource.filter(':checked');
326
571
327 if (target.val() && source.val()) {
572 if (target.val() && source.val()) {
328 var params = {
573 var params = {
329 'pull_request_id': templateContext.pull_request_data.pull_request_id,
574 'pull_request_id': templateContext.pull_request_data.pull_request_id,
330 'repo_name': templateContext.repo_name,
575 'repo_name': templateContext.repo_name,
331 'version': target.val(),
576 'version': target.val(),
332 'from_version': source.val()
577 'from_version': source.val()
333 };
578 };
334 window.location = pyroutes.url('pullrequest_show', params)
579 window.location = pyroutes.url('pullrequest_show', params)
335 }
580 }
336
581
337 return false;
582 return false;
338 };
583 };
339
584
340 this.toggleVersionView = function (elem) {
585 this.toggleVersionView = function (elem) {
341
586
342 if (this.$showVersionDiff.is(':visible')) {
587 if (this.$showVersionDiff.is(':visible')) {
343 $('.version-pr').hide();
588 $('.version-pr').hide();
344 this.$showVersionDiff.hide();
589 this.$showVersionDiff.hide();
345 $(elem).html($(elem).data('toggleOn'))
590 $(elem).html($(elem).data('toggleOn'))
346 } else {
591 } else {
347 $('.version-pr').show();
592 $('.version-pr').show();
348 this.$showVersionDiff.show();
593 this.$showVersionDiff.show();
349 $(elem).html($(elem).data('toggleOff'))
594 $(elem).html($(elem).data('toggleOff'))
350 }
595 }
351
596
352 return false
597 return false
353 }
598 }
354
599
355 }; No newline at end of file
600 };
@@ -1,95 +1,99 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ##
2 ##
3 ## See also repo_settings.html
3 ## See also repo_settings.html
4 ##
4 ##
5 <%inherit file="/base/base.mako"/>
5 <%inherit file="/base/base.mako"/>
6
6
7 <%def name="title()">
7 <%def name="title()">
8 ${_('%s repository settings') % c.repo_info.repo_name}
8 ${_('%s repository settings') % c.repo_info.repo_name}
9 %if c.rhodecode_name:
9 %if c.rhodecode_name:
10 &middot; ${h.branding(c.rhodecode_name)}
10 &middot; ${h.branding(c.rhodecode_name)}
11 %endif
11 %endif
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15 ${_('Settings')}
15 ${_('Settings')}
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='options')}
23 ${self.repo_menu(active='options')}
24 </%def>
24 </%def>
25
25
26 <%def name="main_content()">
26 <%def name="main_content()">
27 % if hasattr(c, 'repo_edit_template'):
28 <%include file="${c.repo_edit_template}"/>
29 % else:
27 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
30 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
31 % endif
28 </%def>
32 </%def>
29
33
30
34
31 <%def name="main()">
35 <%def name="main()">
32 <div class="box">
36 <div class="box">
33 <div class="title">
37 <div class="title">
34 ${self.repo_page_title(c.rhodecode_db_repo)}
38 ${self.repo_page_title(c.rhodecode_db_repo)}
35 ${self.breadcrumbs()}
39 ${self.breadcrumbs()}
36 </div>
40 </div>
37
41
38 <div class="sidebar-col-wrapper scw-small">
42 <div class="sidebar-col-wrapper scw-small">
39 <div class="sidebar">
43 <div class="sidebar">
40 <ul class="nav nav-pills nav-stacked">
44 <ul class="nav nav-pills nav-stacked">
41 <li class="${'active' if c.active=='settings' else ''}">
45 <li class="${'active' if c.active=='settings' else ''}">
42 <a href="${h.route_path('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
46 <a href="${h.route_path('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
43 </li>
47 </li>
44 <li class="${'active' if c.active=='permissions' else ''}">
48 <li class="${'active' if c.active=='permissions' else ''}">
45 <a href="${h.route_path('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
49 <a href="${h.route_path('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
46 </li>
50 </li>
47 <li class="${'active' if c.active=='advanced' else ''}">
51 <li class="${'active' if c.active=='advanced' else ''}">
48 <a href="${h.route_path('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
52 <a href="${h.route_path('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
49 </li>
53 </li>
50 <li class="${'active' if c.active=='vcs' else ''}">
54 <li class="${'active' if c.active=='vcs' else ''}">
51 <a href="${h.url('repo_vcs_settings', repo_name=c.repo_name)}">${_('VCS')}</a>
55 <a href="${h.url('repo_vcs_settings', repo_name=c.repo_name)}">${_('VCS')}</a>
52 </li>
56 </li>
53 <li class="${'active' if c.active=='fields' else ''}">
57 <li class="${'active' if c.active=='fields' else ''}">
54 <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
58 <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
55 </li>
59 </li>
56 <li class="${'active' if c.active=='issuetracker' else ''}">
60 <li class="${'active' if c.active=='issuetracker' else ''}">
57 <a href="${h.url('repo_settings_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
61 <a href="${h.url('repo_settings_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
58 </li>
62 </li>
59 <li class="${'active' if c.active=='caches' else ''}">
63 <li class="${'active' if c.active=='caches' else ''}">
60 <a href="${h.route_path('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
64 <a href="${h.route_path('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
61 </li>
65 </li>
62 %if c.repo_info.repo_type != 'svn':
66 %if c.repo_info.repo_type != 'svn':
63 <li class="${'active' if c.active=='remote' else ''}">
67 <li class="${'active' if c.active=='remote' else ''}">
64 <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
68 <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
65 </li>
69 </li>
66 %endif
70 %endif
67 <li class="${'active' if c.active=='statistics' else ''}">
71 <li class="${'active' if c.active=='statistics' else ''}">
68 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
72 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
69 </li>
73 </li>
70 <li class="${'active' if c.active=='integrations' else ''}">
74 <li class="${'active' if c.active=='integrations' else ''}">
71 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
75 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
72 </li>
76 </li>
73 %if c.repo_info.repo_type != 'svn':
77 %if c.repo_info.repo_type != 'svn':
74 <li class="${'active' if c.active=='reviewers' else ''}">
78 <li class="${'active' if c.active=='reviewers' else ''}">
75 <a href="${h.route_path('repo_reviewers', repo_name=c.repo_name)}">${_('Reviewer Rules')}</a>
79 <a href="${h.route_path('repo_reviewers', repo_name=c.repo_name)}">${_('Reviewer Rules')}</a>
76 </li>
80 </li>
77 %endif
81 %endif
78 <li class="${'active' if c.active=='maintenance' else ''}">
82 <li class="${'active' if c.active=='maintenance' else ''}">
79 <a href="${h.route_path('repo_maintenance', repo_name=c.repo_name)}">${_('Maintenance')}</a>
83 <a href="${h.route_path('repo_maintenance', repo_name=c.repo_name)}">${_('Maintenance')}</a>
80 </li>
84 </li>
81 <li class="${'active' if c.active=='strip' else ''}">
85 <li class="${'active' if c.active=='strip' else ''}">
82 <a href="${h.route_path('strip', repo_name=c.repo_name)}">${_('Strip')}</a>
86 <a href="${h.route_path('strip', repo_name=c.repo_name)}">${_('Strip')}</a>
83 </li>
87 </li>
84
88
85 </ul>
89 </ul>
86 </div>
90 </div>
87
91
88 <div class="main-content-full-width">
92 <div class="main-content-full-width">
89 ${self.main_content()}
93 ${self.main_content()}
90 </div>
94 </div>
91
95
92 </div>
96 </div>
93 </div>
97 </div>
94
98
95 </%def> No newline at end of file
99 </%def>
@@ -1,113 +1,114 b''
1 ## Changesets table !
1 ## Changesets table !
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 %if c.ancestor:
4 %if c.ancestor:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
5 <div class="ancestor">${_('Common Ancestor Commit')}:
6 <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}">
6 <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}">
7 ${h.short_id(c.ancestor)}
7 ${h.short_id(c.ancestor)}
8 </a>. ${_('Compare was calculated based on this shared commit.')}
8 </a>. ${_('Compare was calculated based on this shared commit.')}
9 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
9 </div>
10 </div>
10 %endif
11 %endif
11
12
12 <div class="container">
13 <div class="container">
13 <input type="hidden" name="__start__" value="revisions:sequence">
14 <input type="hidden" name="__start__" value="revisions:sequence">
14 <table class="rctable compare_view_commits">
15 <table class="rctable compare_view_commits">
15 <tr>
16 <tr>
16 <th>${_('Time')}</th>
17 <th>${_('Time')}</th>
17 <th>${_('Author')}</th>
18 <th>${_('Author')}</th>
18 <th>${_('Commit')}</th>
19 <th>${_('Commit')}</th>
19 <th></th>
20 <th></th>
20 <th>${_('Description')}</th>
21 <th>${_('Description')}</th>
21 </tr>
22 </tr>
22 %for commit in c.commit_ranges:
23 %for commit in c.commit_ranges:
23 <tr id="row-${commit.raw_id}"
24 <tr id="row-${commit.raw_id}"
24 commit_id="${commit.raw_id}"
25 commit_id="${commit.raw_id}"
25 class="compare_select"
26 class="compare_select"
26 style="${'display: none' if c.collapse_all_commits else ''}"
27 style="${'display: none' if c.collapse_all_commits else ''}"
27 >
28 >
28 <td class="td-time">
29 <td class="td-time">
29 ${h.age_component(commit.date)}
30 ${h.age_component(commit.date)}
30 </td>
31 </td>
31 <td class="td-user">
32 <td class="td-user">
32 ${base.gravatar_with_user(commit.author, 16)}
33 ${base.gravatar_with_user(commit.author, 16)}
33 </td>
34 </td>
34 <td class="td-hash">
35 <td class="td-hash">
35 <code>
36 <code>
36 <a href="${h.url('changeset_home',
37 <a href="${h.url('changeset_home',
37 repo_name=c.target_repo.repo_name,
38 repo_name=c.target_repo.repo_name,
38 revision=commit.raw_id)}">
39 revision=commit.raw_id)}">
39 r${commit.revision}:${h.short_id(commit.raw_id)}
40 r${commit.revision}:${h.short_id(commit.raw_id)}
40 </a>
41 </a>
41 ${h.hidden('revisions',commit.raw_id)}
42 ${h.hidden('revisions',commit.raw_id)}
42 </code>
43 </code>
43 </td>
44 </td>
44 <td class="expand_commit"
45 <td class="expand_commit"
45 data-commit-id="${commit.raw_id}"
46 data-commit-id="${commit.raw_id}"
46 title="${_( 'Expand commit message')}"
47 title="${_( 'Expand commit message')}"
47 >
48 >
48 <div class="show_more_col">
49 <div class="show_more_col">
49 <i class="show_more"></i>
50 <i class="show_more"></i>
50 </div>
51 </div>
51 </td>
52 </td>
52 <td class="mid td-description">
53 <td class="mid td-description">
53 <div class="log-container truncate-wrap">
54 <div class="log-container truncate-wrap">
54 <div
55 <div
55 id="c-${commit.raw_id}"
56 id="c-${commit.raw_id}"
56 class="message truncate"
57 class="message truncate"
57 data-message-raw="${commit.message}"
58 data-message-raw="${commit.message}"
58 >
59 >
59 ${h.urlify_commit_message(commit.message, c.repo_name)}
60 ${h.urlify_commit_message(commit.message, c.repo_name)}
60 </div>
61 </div>
61 </div>
62 </div>
62 </td>
63 </td>
63 </tr>
64 </tr>
64 %endfor
65 %endfor
65 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
66 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
66 <td colspan="5">
67 <td colspan="5">
67 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
68 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
68 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${ungettext('show it','show them', len(c.commit_ranges))}</a>
69 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${ungettext('show it','show them', len(c.commit_ranges))}</a>
69 </td>
70 </td>
70 </tr>
71 </tr>
71 % if not c.commit_ranges:
72 % if not c.commit_ranges:
72 <tr class="compare_select">
73 <tr class="compare_select">
73 <td colspan="5">
74 <td colspan="5">
74 ${_('No commits in this compare')}
75 ${_('No commits in this compare')}
75 </td>
76 </td>
76 </tr>
77 </tr>
77 % endif
78 % endif
78 </table>
79 </table>
79 <input type="hidden" name="__end__" value="revisions:sequence">
80 <input type="hidden" name="__end__" value="revisions:sequence">
80
81
81 </div>
82 </div>
82
83
83 <script>
84 <script>
84 $('.expand_commit').on('click',function(e){
85 $('.expand_commit').on('click',function(e){
85 var target_expand = $(this);
86 var target_expand = $(this);
86 var cid = target_expand.data('commitId');
87 var cid = target_expand.data('commitId');
87
88
88 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
89 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
89 if (target_expand.hasClass('open')){
90 if (target_expand.hasClass('open')){
90 $('#c-'+cid).css({
91 $('#c-'+cid).css({
91 'height': '1.5em',
92 'height': '1.5em',
92 'white-space': 'nowrap',
93 'white-space': 'nowrap',
93 'text-overflow': 'ellipsis',
94 'text-overflow': 'ellipsis',
94 'overflow':'hidden'
95 'overflow':'hidden'
95 });
96 });
96 target_expand.removeClass('open');
97 target_expand.removeClass('open');
97 }
98 }
98 else {
99 else {
99 $('#c-'+cid).css({
100 $('#c-'+cid).css({
100 'height': 'auto',
101 'height': 'auto',
101 'white-space': 'pre-line',
102 'white-space': 'pre-line',
102 'text-overflow': 'initial',
103 'text-overflow': 'initial',
103 'overflow':'visible'
104 'overflow':'visible'
104 });
105 });
105 target_expand.addClass('open');
106 target_expand.addClass('open');
106 }
107 }
107 });
108 });
108
109
109 $('.compare_select').on('click',function(e){
110 $('.compare_select').on('click',function(e){
110 var cid = $(this).attr('commit_id');
111 var cid = $(this).attr('commit_id');
111 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
112 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
112 });
113 });
113 </script>
114 </script>
@@ -1,105 +1,105 b''
1 <div tal:define="item_tmpl item_template|field.widget.item_template;
1 <div tal:define="item_tmpl item_template|field.widget.item_template;
2 oid oid|field.oid;
2 oid oid|field.oid;
3 name name|field.name;
3 name name|field.name;
4 min_len min_len|field.widget.min_len;
4 min_len min_len|field.widget.min_len;
5 min_len min_len or 0;
5 min_len min_len or 0;
6 max_len max_len|field.widget.max_len;
6 max_len max_len|field.widget.max_len;
7 max_len max_len or 100000;
7 max_len max_len or 100000;
8 now_len len(subfields);
8 now_len len(subfields);
9 orderable orderable|field.widget.orderable;
9 orderable orderable|field.widget.orderable;
10 orderable orderable and 1 or 0;
10 orderable orderable and 1 or 0;
11 prototype field.widget.prototype(field);
11 prototype field.widget.prototype(field);
12 title title|field.title;"
12 title title|field.title;"
13 class="deform-seq"
13 class="deform-seq"
14 id="${oid}">
14 id="${oid}">
15
15
16 <style>
16 <style>
17 body.dragging, body.dragging * {
17 body.dragging, body.dragging * {
18 cursor: move !important;
18 cursor: move !important;
19 }
19 }
20
20
21 .dragged {
21 .dragged {
22 position: absolute;
22 position: absolute;
23 opacity: 0.5;
23 opacity: 0.5;
24 z-index: 2000;
24 z-index: 2000;
25 }
25 }
26 </style>
26 </style>
27
27
28 <!-- sequence -->
28 <!-- sequence -->
29 <input type="hidden" name="__start__"
29 <input type="hidden" name="__start__"
30 value="${field.name}:sequence"
30 value="${field.name}:sequence"
31 class="deform-proto"
31 class="deform-proto"
32 tal:attributes="prototype prototype"/>
32 tal:attributes="prototype prototype"/>
33
33
34 <div class="panel panel-default">
34 <div class="panel panel-default">
35 <div class="panel-heading">${title}</div>
36 <div class="panel-body">
35 <div class="panel-body">
37
36
38 <div class="deform-seq-container"
37 <div class="deform-seq-container"
39 id="${oid}-orderable">
38 id="${oid}-orderable">
40 <div tal:define="subfields [ x[1] for x in subfields ]"
39 <div tal:define="subfields [ x[1] for x in subfields ]"
41 tal:repeat="subfield subfields"
40 tal:repeat="subfield subfields"
42 tal:replace="structure subfield.render_template(item_tmpl,
41 tal:replace="structure subfield.render_template(item_tmpl,parent=field)" />
43 parent=field)" />
44 <span class="deform-insert-before"
42 <span class="deform-insert-before"
45 tal:attributes="
43 tal:attributes="
46 min_len min_len;
44 min_len min_len;
47 max_len max_len;
45 max_len max_len;
48 now_len now_len;
46 now_len now_len;
49 orderable orderable;"></span>
47 orderable orderable;">
48
49 </span>
50 </div>
50 </div>
51
51
52 <div style="clear: both"></div>
52 <div style="clear: both"></div>
53 </div>
53 </div>
54
54
55 <div class="panel-footer">
55 <div class="panel-footer">
56 <a href="#"
56 <a href="#"
57 class="btn deform-seq-add"
57 class="btn deform-seq-add"
58 id="${field.oid}-seqAdd"
58 id="${field.oid}-seqAdd"
59 onclick="javascript: return deform.appendSequenceItem(this);">
59 onclick="javascript: return deform.appendSequenceItem(this);">
60 <small id="${field.oid}-addtext">${add_subitem_text}</small>
60 <small id="${field.oid}-addtext">${add_subitem_text}</small>
61 </a>
61 </a>
62
62
63 <script type="text/javascript">
63 <script type="text/javascript">
64 deform.addCallback(
64 deform.addCallback(
65 '${field.oid}',
65 '${field.oid}',
66 function(oid) {
66 function(oid) {
67 oid_node = $('#'+ oid);
67 oid_node = $('#'+ oid);
68 deform.processSequenceButtons(oid_node, ${min_len},
68 deform.processSequenceButtons(oid_node, ${min_len},
69 ${max_len}, ${now_len},
69 ${max_len}, ${now_len},
70 ${orderable});
70 ${orderable});
71 }
71 }
72 )
72 )
73 <tal:block condition="orderable">
73 <tal:block condition="orderable">
74 $( "#${oid}-orderable" ).sortable({
74 $( "#${oid}-orderable" ).sortable({
75 handle: ".deform-order-button, .panel-heading",
75 handle: ".deform-order-button, .panel-heading",
76 containerSelector: "#${oid}-orderable",
76 containerSelector: "#${oid}-orderable",
77 itemSelector: ".deform-seq-item",
77 itemSelector: ".deform-seq-item",
78 placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>',
78 placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>',
79 onDragStart: function ($item, container, _super) {
79 onDragStart: function ($item, container, _super) {
80 var offset = $item.offset(),
80 var offset = $item.offset(),
81 pointer = container.rootGroup.pointer
81 pointer = container.rootGroup.pointer;
82
82
83 adjustment = {
83 adjustment = {
84 left: pointer.left - offset.left,
84 left: pointer.left - offset.left,
85 top: pointer.top - offset.top
85 top: pointer.top - offset.top
86 }
86 };
87
87
88 _super($item, container)
88 _super($item, container)
89 },
89 },
90 onDrag: function ($item, position) {
90 onDrag: function ($item, position) {
91 $item.css({
91 $item.css({
92 left: position.left - adjustment.left,
92 left: position.left - adjustment.left,
93 top: position.top - adjustment.top
93 top: position.top - adjustment.top
94 })
94 })
95 }
95 }
96 });
96 });
97 </tal:block>
97 </tal:block>
98 </script>
98 </script>
99
99
100 <input type="hidden" name="__end__" value="${field.name}:sequence"/>
100 <input type="hidden" name="__end__" value="${field.name}:sequence"/>
101 <!-- /sequence -->
101 <!-- /sequence -->
102 </div>
102 </div>
103
103
104 </div>
104 </div>
105 </div> No newline at end of file
105 </div>
@@ -1,35 +1,31 b''
1 <div tal:omit-tag="field.widget.hidden"
1 <div tal:omit-tag="field.widget.hidden"
2 tal:define="hidden hidden|field.widget.hidden;
2 tal:define="hidden hidden|field.widget.hidden;
3 error_class error_class|field.widget.error_class;
3 error_class error_class|field.widget.error_class;
4 description description|field.description;
4 description description|field.description;
5 title title|field.title;
5 title title|field.title;
6 oid oid|field.oid"
6 oid oid|field.oid"
7 class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}"
7 class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}"
8 i18n:domain="deform">
8 i18n:domain="deform">
9
9 <div class="deform-seq-item-group">
10 <div class="deform-seq-item-group">
10 <span tal:replace="structure field.serialize(cstruct)"/>
11 <span tal:replace="structure field.serialize(cstruct)"/>
11 <tal:errors condition="field.error and not hidden"
12 <tal:errors condition="field.error and not hidden"
12 define="errstr 'error-%s' % oid"
13 define="errstr 'error-%s' % oid"
13 repeat="msg field.error.messages()">
14 repeat="msg field.error.messages()">
14 <p tal:condition="msg"
15 <p tal:condition="msg"
15 id="${errstr if repeat.msg.index==0 else '%s-%s' % (errstr, repeat.msg.index)}"
16 id="${errstr if repeat.msg.index==0 else '%s-%s' % (errstr, repeat.msg.index)}"
16 class="${error_class} help-block error-block"
17 class="${error_class} help-block error-block"
17 i18n:translate="">${msg}</p>
18 i18n:translate="">${msg}</p>
18 </tal:errors>
19 </tal:errors>
19 </div>
20 </div>
21
20 <div class="deform-seq-item-handle" style="padding:0">
22 <div class="deform-seq-item-handle" style="padding:0">
21 <!-- sequence_item -->
22 <span class="deform-order-button close glyphicon glyphicon-resize-vertical"
23 id="${oid}-order"
24 tal:condition="not hidden"
25 title="Reorder (via drag and drop)"
26 i18n:attributes="title"></span>
27 <a class="deform-close-button close"
23 <a class="deform-close-button close"
28 id="${oid}-close"
24 id="${oid}-close"
29 tal:condition="not field.widget.hidden"
25 tal:condition="not field.widget.hidden"
30 title="Remove"
26 title="Remove"
31 i18n:attributes="title"
27 i18n:attributes="title"
32 onclick="javascript:deform.removeSequenceItem(this);">&times;</a>
28 onclick="javascript:deform.removeSequenceItem(this);">&times;</a>
33 </div>
29 </div>
34 <!-- /sequence_item -->
30
35 </div>
31 </div>
@@ -1,591 +1,505 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('New pull request')}
4 ${c.repo_name} ${_('New pull request')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('New pull request')}
8 ${_('New pull request')}
9 </%def>
9 </%def>
10
10
11 <%def name="menu_bar_nav()">
11 <%def name="menu_bar_nav()">
12 ${self.menu_items(active='repositories')}
12 ${self.menu_items(active='repositories')}
13 </%def>
13 </%def>
14
14
15 <%def name="menu_bar_subnav()">
15 <%def name="menu_bar_subnav()">
16 ${self.repo_menu(active='showpullrequest')}
16 ${self.repo_menu(active='showpullrequest')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <div class="box">
20 <div class="box">
21 <div class="title">
21 <div class="title">
22 ${self.repo_page_title(c.rhodecode_db_repo)}
22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 ${self.breadcrumbs()}
23 ${self.breadcrumbs()}
24 </div>
24 </div>
25
25
26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 <div class="box pr-summary">
27 <div class="box pr-summary">
28
28
29 <div class="summary-details block-left">
29 <div class="summary-details block-left">
30
30
31 <div class="form">
31 <div class="form">
32 <!-- fields -->
32 <!-- fields -->
33
33
34 <div class="fields" >
34 <div class="fields" >
35
35
36 <div class="field">
36 <div class="field">
37 <div class="label">
37 <div class="label">
38 <label for="pullrequest_title">${_('Title')}:</label>
38 <label for="pullrequest_title">${_('Title')}:</label>
39 </div>
39 </div>
40 <div class="input">
40 <div class="input">
41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 </div>
42 </div>
43 </div>
43 </div>
44
44
45 <div class="field">
45 <div class="field">
46 <div class="label label-textarea">
46 <div class="label label-textarea">
47 <label for="pullrequest_desc">${_('Description')}:</label>
47 <label for="pullrequest_desc">${_('Description')}:</label>
48 </div>
48 </div>
49 <div class="textarea text-area editor">
49 <div class="textarea text-area editor">
50 ${h.textarea('pullrequest_desc',size=30, )}
50 ${h.textarea('pullrequest_desc',size=30, )}
51 <span class="help-block">${_('Write a short description on this pull request')}</span>
51 <span class="help-block">${_('Write a short description on this pull request')}</span>
52 </div>
52 </div>
53 </div>
53 </div>
54
54
55 <div class="field">
55 <div class="field">
56 <div class="label label-textarea">
56 <div class="label label-textarea">
57 <label for="pullrequest_desc">${_('Commit flow')}:</label>
57 <label for="pullrequest_desc">${_('Commit flow')}:</label>
58 </div>
58 </div>
59
59
60 ## TODO: johbo: Abusing the "content" class here to get the
60 ## TODO: johbo: Abusing the "content" class here to get the
61 ## desired effect. Should be replaced by a proper solution.
61 ## desired effect. Should be replaced by a proper solution.
62
62
63 ##ORG
63 ##ORG
64 <div class="content">
64 <div class="content">
65 <strong>${_('Origin repository')}:</strong>
65 <strong>${_('Source repository')}:</strong>
66 ${c.rhodecode_db_repo.description}
66 ${c.rhodecode_db_repo.description}
67 </div>
67 </div>
68 <div class="content">
68 <div class="content">
69 ${h.hidden('source_repo')}
69 ${h.hidden('source_repo')}
70 ${h.hidden('source_ref')}
70 ${h.hidden('source_ref')}
71 </div>
71 </div>
72
72
73 ##OTHER, most Probably the PARENT OF THIS FORK
73 ##OTHER, most Probably the PARENT OF THIS FORK
74 <div class="content">
74 <div class="content">
75 ## filled with JS
75 ## filled with JS
76 <div id="target_repo_desc"></div>
76 <div id="target_repo_desc"></div>
77 </div>
77 </div>
78
78
79 <div class="content">
79 <div class="content">
80 ${h.hidden('target_repo')}
80 ${h.hidden('target_repo')}
81 ${h.hidden('target_ref')}
81 ${h.hidden('target_ref')}
82 <span id="target_ref_loading" style="display: none">
82 <span id="target_ref_loading" style="display: none">
83 ${_('Loading refs...')}
83 ${_('Loading refs...')}
84 </span>
84 </span>
85 </div>
85 </div>
86 </div>
86 </div>
87
87
88 <div class="field">
88 <div class="field">
89 <div class="label label-textarea">
89 <div class="label label-textarea">
90 <label for="pullrequest_submit"></label>
90 <label for="pullrequest_submit"></label>
91 </div>
91 </div>
92 <div class="input">
92 <div class="input">
93 <div class="pr-submit-button">
93 <div class="pr-submit-button">
94 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
94 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
95 </div>
95 </div>
96 <div id="pr_open_message"></div>
96 <div id="pr_open_message"></div>
97 </div>
97 </div>
98 </div>
98 </div>
99
99
100 <div class="pr-spacing-container"></div>
100 <div class="pr-spacing-container"></div>
101 </div>
101 </div>
102 </div>
102 </div>
103 </div>
103 </div>
104 <div>
104 <div>
105 ## REIEW RULES
106 <div id="review_rules" style="display: none" class="reviewers-title block-right">
107 <div class="pr-details-title">
108 ${_('Reviewer rules')}
109 </div>
110 <div class="pr-reviewer-rules">
111 ## review rules will be appended here, by default reviewers logic
112 </div>
113 </div>
114
115 ## REVIEWERS
105 <div class="reviewers-title block-right">
116 <div class="reviewers-title block-right">
106 <div class="pr-details-title">
117 <div class="pr-details-title">
107 ${_('Pull request reviewers')}
118 ${_('Pull request reviewers')}
108 <span class="calculate-reviewers"> - ${_('loading...')}</span>
119 <span class="calculate-reviewers"> - ${_('loading...')}</span>
109 </div>
120 </div>
110 </div>
121 </div>
111 <div id="reviewers" class="block-right pr-details-content reviewers">
122 <div id="reviewers" class="block-right pr-details-content reviewers">
112 ## members goes here, filled via JS based on initial selection !
123 ## members goes here, filled via JS based on initial selection !
113 <input type="hidden" name="__start__" value="review_members:sequence">
124 <input type="hidden" name="__start__" value="review_members:sequence">
114 <ul id="review_members" class="group_members"></ul>
125 <ul id="review_members" class="group_members"></ul>
115 <input type="hidden" name="__end__" value="review_members:sequence">
126 <input type="hidden" name="__end__" value="review_members:sequence">
116 <div id="add_reviewer_input" class='ac'>
127 <div id="add_reviewer_input" class='ac'>
117 <div class="reviewer_ac">
128 <div class="reviewer_ac">
118 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
129 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
119 <div id="reviewers_container"></div>
130 <div id="reviewers_container"></div>
120 </div>
131 </div>
121 </div>
132 </div>
122 </div>
133 </div>
123 </div>
134 </div>
124 </div>
135 </div>
125 <div class="box">
136 <div class="box">
126 <div>
137 <div>
127 ## overview pulled by ajax
138 ## overview pulled by ajax
128 <div id="pull_request_overview"></div>
139 <div id="pull_request_overview"></div>
129 </div>
140 </div>
130 </div>
141 </div>
131 ${h.end_form()}
142 ${h.end_form()}
132 </div>
143 </div>
133
144
134 <script type="text/javascript">
145 <script type="text/javascript">
135 $(function(){
146 $(function(){
136 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
147 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
137 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
148 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
138 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
149 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
139 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
150 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
140 var targetRepoName = '${c.repo_name}';
141
151
142 var $pullRequestForm = $('#pull_request_form');
152 var $pullRequestForm = $('#pull_request_form');
143 var $sourceRepo = $('#source_repo', $pullRequestForm);
153 var $sourceRepo = $('#source_repo', $pullRequestForm);
144 var $targetRepo = $('#target_repo', $pullRequestForm);
154 var $targetRepo = $('#target_repo', $pullRequestForm);
145 var $sourceRef = $('#source_ref', $pullRequestForm);
155 var $sourceRef = $('#source_ref', $pullRequestForm);
146 var $targetRef = $('#target_ref', $pullRequestForm);
156 var $targetRef = $('#target_ref', $pullRequestForm);
147
157
158 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
159 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
160
161 var targetRepo = function() { return $targetRepo.eq(0).val() };
162 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
163
148 var calculateContainerWidth = function() {
164 var calculateContainerWidth = function() {
149 var maxWidth = 0;
165 var maxWidth = 0;
150 var repoSelect2Containers = ['#source_repo', '#target_repo'];
166 var repoSelect2Containers = ['#source_repo', '#target_repo'];
151 $.each(repoSelect2Containers, function(idx, value) {
167 $.each(repoSelect2Containers, function(idx, value) {
152 $(value).select2('container').width('auto');
168 $(value).select2('container').width('auto');
153 var curWidth = $(value).select2('container').width();
169 var curWidth = $(value).select2('container').width();
154 if (maxWidth <= curWidth) {
170 if (maxWidth <= curWidth) {
155 maxWidth = curWidth;
171 maxWidth = curWidth;
156 }
172 }
157 $.each(repoSelect2Containers, function(idx, value) {
173 $.each(repoSelect2Containers, function(idx, value) {
158 $(value).select2('container').width(maxWidth + 10);
174 $(value).select2('container').width(maxWidth + 10);
159 });
175 });
160 });
176 });
161 };
177 };
162
178
163 var initRefSelection = function(selectedRef) {
179 var initRefSelection = function(selectedRef) {
164 return function(element, callback) {
180 return function(element, callback) {
165 // translate our select2 id into a text, it's a mapping to show
181 // translate our select2 id into a text, it's a mapping to show
166 // simple label when selecting by internal ID.
182 // simple label when selecting by internal ID.
167 var id, refData;
183 var id, refData;
168 if (selectedRef === undefined) {
184 if (selectedRef === undefined) {
169 id = element.val();
185 id = element.val();
170 refData = element.val().split(':');
186 refData = element.val().split(':');
171 } else {
187 } else {
172 id = selectedRef;
188 id = selectedRef;
173 refData = selectedRef.split(':');
189 refData = selectedRef.split(':');
174 }
190 }
175
191
176 var text = refData[1];
192 var text = refData[1];
177 if (refData[0] === 'rev') {
193 if (refData[0] === 'rev') {
178 text = text.substring(0, 12);
194 text = text.substring(0, 12);
179 }
195 }
180
196
181 var data = {id: id, text: text};
197 var data = {id: id, text: text};
182
198
183 callback(data);
199 callback(data);
184 };
200 };
185 };
201 };
186
202
187 var formatRefSelection = function(item) {
203 var formatRefSelection = function(item) {
188 var prefix = '';
204 var prefix = '';
189 var refData = item.id.split(':');
205 var refData = item.id.split(':');
190 if (refData[0] === 'branch') {
206 if (refData[0] === 'branch') {
191 prefix = '<i class="icon-branch"></i>';
207 prefix = '<i class="icon-branch"></i>';
192 }
208 }
193 else if (refData[0] === 'book') {
209 else if (refData[0] === 'book') {
194 prefix = '<i class="icon-bookmark"></i>';
210 prefix = '<i class="icon-bookmark"></i>';
195 }
211 }
196 else if (refData[0] === 'tag') {
212 else if (refData[0] === 'tag') {
197 prefix = '<i class="icon-tag"></i>';
213 prefix = '<i class="icon-tag"></i>';
198 }
214 }
199
215
200 var originalOption = item.element;
216 var originalOption = item.element;
201 return prefix + item.text;
217 return prefix + item.text;
202 };
218 };
203
219
204 // custom code mirror
220 // custom code mirror
205 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
221 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
206
222
223 reviewersController = new ReviewersController();
224
207 var queryTargetRepo = function(self, query) {
225 var queryTargetRepo = function(self, query) {
208 // cache ALL results if query is empty
226 // cache ALL results if query is empty
209 var cacheKey = query.term || '__';
227 var cacheKey = query.term || '__';
210 var cachedData = self.cachedDataSource[cacheKey];
228 var cachedData = self.cachedDataSource[cacheKey];
211
229
212 if (cachedData) {
230 if (cachedData) {
213 query.callback({results: cachedData.results});
231 query.callback({results: cachedData.results});
214 } else {
232 } else {
215 $.ajax({
233 $.ajax({
216 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
234 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
217 data: {query: query.term},
235 data: {query: query.term},
218 dataType: 'json',
236 dataType: 'json',
219 type: 'GET',
237 type: 'GET',
220 success: function(data) {
238 success: function(data) {
221 self.cachedDataSource[cacheKey] = data;
239 self.cachedDataSource[cacheKey] = data;
222 query.callback({results: data.results});
240 query.callback({results: data.results});
223 },
241 },
224 error: function(data, textStatus, errorThrown) {
242 error: function(data, textStatus, errorThrown) {
225 alert(
243 alert(
226 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
244 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
227 }
245 }
228 });
246 });
229 }
247 }
230 };
248 };
231
249
232 var queryTargetRefs = function(initialData, query) {
250 var queryTargetRefs = function(initialData, query) {
233 var data = {results: []};
251 var data = {results: []};
234 // filter initialData
252 // filter initialData
235 $.each(initialData, function() {
253 $.each(initialData, function() {
236 var section = this.text;
254 var section = this.text;
237 var children = [];
255 var children = [];
238 $.each(this.children, function() {
256 $.each(this.children, function() {
239 if (query.term.length === 0 ||
257 if (query.term.length === 0 ||
240 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
258 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
241 children.push({'id': this.id, 'text': this.text})
259 children.push({'id': this.id, 'text': this.text})
242 }
260 }
243 });
261 });
244 data.results.push({'text': section, 'children': children})
262 data.results.push({'text': section, 'children': children})
245 });
263 });
246 query.callback({results: data.results});
264 query.callback({results: data.results});
247 };
265 };
248
266
249
250 var prButtonLockChecks = {
251 'compare': false,
252 'reviewers': false
253 };
254
255 var prButtonLock = function(lockEnabled, msg, scope) {
256 scope = scope || 'all';
257 if (scope == 'all'){
258 prButtonLockChecks['compare'] = !lockEnabled;
259 prButtonLockChecks['reviewers'] = !lockEnabled;
260 } else if (scope == 'compare') {
261 prButtonLockChecks['compare'] = !lockEnabled;
262 } else if (scope == 'reviewers'){
263 prButtonLockChecks['reviewers'] = !lockEnabled;
264 }
265 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
266 if (lockEnabled) {
267 $('#save').attr('disabled', 'disabled');
268 }
269 else if (checksMeet) {
270 $('#save').removeAttr('disabled');
271 }
272
273 if (msg) {
274 $('#pr_open_message').html(msg);
275 }
276 };
277
278 var loadRepoRefDiffPreview = function() {
267 var loadRepoRefDiffPreview = function() {
279 var sourceRepo = $sourceRepo.eq(0).val();
280 var sourceRef = $sourceRef.eq(0).val().split(':');
281
282 var targetRepo = $targetRepo.eq(0).val();
283 var targetRef = $targetRef.eq(0).val().split(':');
284
268
285 var url_data = {
269 var url_data = {
286 'repo_name': targetRepo,
270 'repo_name': targetRepo(),
287 'target_repo': sourceRepo,
271 'target_repo': sourceRepo(),
288 'source_ref': targetRef[2],
272 'source_ref': targetRef()[2],
289 'source_ref_type': 'rev',
273 'source_ref_type': 'rev',
290 'target_ref': sourceRef[2],
274 'target_ref': sourceRef()[2],
291 'target_ref_type': 'rev',
275 'target_ref_type': 'rev',
292 'merge': true,
276 'merge': true,
293 '_': Date.now() // bypass browser caching
277 '_': Date.now() // bypass browser caching
294 }; // gather the source/target ref and repo here
278 }; // gather the source/target ref and repo here
295
279
296 if (sourceRef.length !== 3 || targetRef.length !== 3) {
280 if (sourceRef().length !== 3 || targetRef().length !== 3) {
297 prButtonLock(true, "${_('Please select origin and destination')}");
281 prButtonLock(true, "${_('Please select source and target')}");
298 return;
282 return;
299 }
283 }
300 var url = pyroutes.url('compare_url', url_data);
284 var url = pyroutes.url('compare_url', url_data);
301
285
302 // lock PR button, so we cannot send PR before it's calculated
286 // lock PR button, so we cannot send PR before it's calculated
303 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
287 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
304
288
305 if (loadRepoRefDiffPreview._currentRequest) {
289 if (loadRepoRefDiffPreview._currentRequest) {
306 loadRepoRefDiffPreview._currentRequest.abort();
290 loadRepoRefDiffPreview._currentRequest.abort();
307 }
291 }
308
292
309 loadRepoRefDiffPreview._currentRequest = $.get(url)
293 loadRepoRefDiffPreview._currentRequest = $.get(url)
310 .error(function(data, textStatus, errorThrown) {
294 .error(function(data, textStatus, errorThrown) {
311 alert(
295 alert(
312 "Error while processing request.\nError code {0} ({1}).".format(
296 "Error while processing request.\nError code {0} ({1}).".format(
313 data.status, data.statusText));
297 data.status, data.statusText));
314 })
298 })
315 .done(function(data) {
299 .done(function(data) {
316 loadRepoRefDiffPreview._currentRequest = null;
300 loadRepoRefDiffPreview._currentRequest = null;
317 $('#pull_request_overview').html(data);
301 $('#pull_request_overview').html(data);
302
318 var commitElements = $(data).find('tr[commit_id]');
303 var commitElements = $(data).find('tr[commit_id]');
319
304
320 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
305 var prTitleAndDesc = getTitleAndDescription(
321 commitElements, 5);
306 sourceRef()[1], commitElements, 5);
322
307
323 var title = prTitleAndDesc[0];
308 var title = prTitleAndDesc[0];
324 var proposedDescription = prTitleAndDesc[1];
309 var proposedDescription = prTitleAndDesc[1];
325
310
326 var useGeneratedTitle = (
311 var useGeneratedTitle = (
327 $('#pullrequest_title').hasClass('autogenerated-title') ||
312 $('#pullrequest_title').hasClass('autogenerated-title') ||
328 $('#pullrequest_title').val() === "");
313 $('#pullrequest_title').val() === "");
329
314
330 if (title && useGeneratedTitle) {
315 if (title && useGeneratedTitle) {
331 // use generated title if we haven't specified our own
316 // use generated title if we haven't specified our own
332 $('#pullrequest_title').val(title);
317 $('#pullrequest_title').val(title);
333 $('#pullrequest_title').addClass('autogenerated-title');
318 $('#pullrequest_title').addClass('autogenerated-title');
334
319
335 }
320 }
336
321
337 var useGeneratedDescription = (
322 var useGeneratedDescription = (
338 !codeMirrorInstance._userDefinedDesc ||
323 !codeMirrorInstance._userDefinedDesc ||
339 codeMirrorInstance.getValue() === "");
324 codeMirrorInstance.getValue() === "");
340
325
341 if (proposedDescription && useGeneratedDescription) {
326 if (proposedDescription && useGeneratedDescription) {
342 // set proposed content, if we haven't defined our own,
327 // set proposed content, if we haven't defined our own,
343 // or we don't have description written
328 // or we don't have description written
344 codeMirrorInstance._userDefinedDesc = false; // reset state
329 codeMirrorInstance._userDefinedDesc = false; // reset state
345 codeMirrorInstance.setValue(proposedDescription);
330 codeMirrorInstance.setValue(proposedDescription);
346 }
331 }
347
332
348 var msg = '';
333 var msg = '';
349 if (commitElements.length === 1) {
334 if (commitElements.length === 1) {
350 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
335 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
351 } else {
336 } else {
352 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
337 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
353 }
338 }
354
339
355 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
340 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
356
341
357 if (commitElements.length) {
342 if (commitElements.length) {
358 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
343 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
359 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
344 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
360 }
345 }
361 else {
346 else {
362 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
347 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
363 }
348 }
364
349
365
350
366 });
351 });
367 };
352 };
368
353
369 /**
370 Generate Title and Description for a PullRequest.
371 In case of 1 commits, the title and description is that one commit
372 in case of multiple commits, we iterate on them with max N number of commits,
373 and build description in a form
374 - commitN
375 - commitN+1
376 ...
377
378 Title is then constructed from branch names, or other references,
379 replacing '-' and '_' into spaces
380
381 * @param sourceRef
382 * @param elements
383 * @param limit
384 * @returns {*[]}
385 */
386 var getTitleAndDescription = function(sourceRef, elements, limit) {
387 var title = '';
388 var desc = '';
389
390 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
391 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
392 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
393 });
394 // only 1 commit, use commit message as title
395 if (elements.length == 1) {
396 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
397 }
398 else {
399 // use reference name
400 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
401 }
402
403 return [title, desc]
404 };
405
406 var Select2Box = function(element, overrides) {
354 var Select2Box = function(element, overrides) {
407 var globalDefaults = {
355 var globalDefaults = {
408 dropdownAutoWidth: true,
356 dropdownAutoWidth: true,
409 containerCssClass: "drop-menu",
357 containerCssClass: "drop-menu",
410 dropdownCssClass: "drop-menu-dropdown"
358 dropdownCssClass: "drop-menu-dropdown"
411 };
359 };
412
360
413 var initSelect2 = function(defaultOptions) {
361 var initSelect2 = function(defaultOptions) {
414 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
362 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
415 element.select2(options);
363 element.select2(options);
416 };
364 };
417
365
418 return {
366 return {
419 initRef: function() {
367 initRef: function() {
420 var defaultOptions = {
368 var defaultOptions = {
421 minimumResultsForSearch: 5,
369 minimumResultsForSearch: 5,
422 formatSelection: formatRefSelection
370 formatSelection: formatRefSelection
423 };
371 };
424
372
425 initSelect2(defaultOptions);
373 initSelect2(defaultOptions);
426 },
374 },
427
375
428 initRepo: function(defaultValue, readOnly) {
376 initRepo: function(defaultValue, readOnly) {
429 var defaultOptions = {
377 var defaultOptions = {
430 initSelection : function (element, callback) {
378 initSelection : function (element, callback) {
431 var data = {id: defaultValue, text: defaultValue};
379 var data = {id: defaultValue, text: defaultValue};
432 callback(data);
380 callback(data);
433 }
381 }
434 };
382 };
435
383
436 initSelect2(defaultOptions);
384 initSelect2(defaultOptions);
437
385
438 element.select2('val', defaultSourceRepo);
386 element.select2('val', defaultSourceRepo);
439 if (readOnly === true) {
387 if (readOnly === true) {
440 element.select2('readonly', true);
388 element.select2('readonly', true);
441 }
389 }
442 }
390 }
443 };
391 };
444 };
392 };
445
393
446 var initTargetRefs = function(refsData, selectedRef){
394 var initTargetRefs = function(refsData, selectedRef){
447 Select2Box($targetRef, {
395 Select2Box($targetRef, {
448 query: function(query) {
396 query: function(query) {
449 queryTargetRefs(refsData, query);
397 queryTargetRefs(refsData, query);
450 },
398 },
451 initSelection : initRefSelection(selectedRef)
399 initSelection : initRefSelection(selectedRef)
452 }).initRef();
400 }).initRef();
453
401
454 if (!(selectedRef === undefined)) {
402 if (!(selectedRef === undefined)) {
455 $targetRef.select2('val', selectedRef);
403 $targetRef.select2('val', selectedRef);
456 }
404 }
457 };
405 };
458
406
459 var targetRepoChanged = function(repoData) {
407 var targetRepoChanged = function(repoData) {
460 // generate new DESC of target repo displayed next to select
408 // generate new DESC of target repo displayed next to select
461 $('#target_repo_desc').html(
409 $('#target_repo_desc').html(
462 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
410 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
463 );
411 );
464
412
465 // generate dynamic select2 for refs.
413 // generate dynamic select2 for refs.
466 initTargetRefs(repoData['refs']['select2_refs'],
414 initTargetRefs(repoData['refs']['select2_refs'],
467 repoData['refs']['selected_ref']);
415 repoData['refs']['selected_ref']);
468
416
469 };
417 };
470
418
471 var sourceRefSelect2 = Select2Box(
419 var sourceRefSelect2 = Select2Box($sourceRef, {
472 $sourceRef, {
473 placeholder: "${_('Select commit reference')}",
420 placeholder: "${_('Select commit reference')}",
474 query: function(query) {
421 query: function(query) {
475 var initialData = defaultSourceRepoData['refs']['select2_refs'];
422 var initialData = defaultSourceRepoData['refs']['select2_refs'];
476 queryTargetRefs(initialData, query)
423 queryTargetRefs(initialData, query)
477 },
424 },
478 initSelection: initRefSelection()
425 initSelection: initRefSelection()
479 }
426 }
480 );
427 );
481
428
482 var sourceRepoSelect2 = Select2Box($sourceRepo, {
429 var sourceRepoSelect2 = Select2Box($sourceRepo, {
483 query: function(query) {}
430 query: function(query) {}
484 });
431 });
485
432
486 var targetRepoSelect2 = Select2Box($targetRepo, {
433 var targetRepoSelect2 = Select2Box($targetRepo, {
487 cachedDataSource: {},
434 cachedDataSource: {},
488 query: $.debounce(250, function(query) {
435 query: $.debounce(250, function(query) {
489 queryTargetRepo(this, query);
436 queryTargetRepo(this, query);
490 }),
437 }),
491 formatResult: formatResult
438 formatResult: formatResult
492 });
439 });
493
440
494 sourceRefSelect2.initRef();
441 sourceRefSelect2.initRef();
495
442
496 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
443 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
497
444
498 targetRepoSelect2.initRepo(defaultTargetRepo, false);
445 targetRepoSelect2.initRepo(defaultTargetRepo, false);
499
446
500 $sourceRef.on('change', function(e){
447 $sourceRef.on('change', function(e){
501 loadRepoRefDiffPreview();
448 loadRepoRefDiffPreview();
502 loadDefaultReviewers();
449 reviewersController.loadDefaultReviewers(
450 sourceRepo(), sourceRef(), targetRepo(), targetRef());
503 });
451 });
504
452
505 $targetRef.on('change', function(e){
453 $targetRef.on('change', function(e){
506 loadRepoRefDiffPreview();
454 loadRepoRefDiffPreview();
507 loadDefaultReviewers();
455 reviewersController.loadDefaultReviewers(
456 sourceRepo(), sourceRef(), targetRepo(), targetRef());
508 });
457 });
509
458
510 $targetRepo.on('change', function(e){
459 $targetRepo.on('change', function(e){
511 var repoName = $(this).val();
460 var repoName = $(this).val();
512 calculateContainerWidth();
461 calculateContainerWidth();
513 $targetRef.select2('destroy');
462 $targetRef.select2('destroy');
514 $('#target_ref_loading').show();
463 $('#target_ref_loading').show();
515
464
516 $.ajax({
465 $.ajax({
517 url: pyroutes.url('pullrequest_repo_refs',
466 url: pyroutes.url('pullrequest_repo_refs',
518 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
467 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
519 data: {},
468 data: {},
520 dataType: 'json',
469 dataType: 'json',
521 type: 'GET',
470 type: 'GET',
522 success: function(data) {
471 success: function(data) {
523 $('#target_ref_loading').hide();
472 $('#target_ref_loading').hide();
524 targetRepoChanged(data);
473 targetRepoChanged(data);
525 loadRepoRefDiffPreview();
474 loadRepoRefDiffPreview();
526 },
475 },
527 error: function(data, textStatus, errorThrown) {
476 error: function(data, textStatus, errorThrown) {
528 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
477 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
529 }
478 }
530 })
479 })
531
480
532 });
481 });
533
482
534 var loadDefaultReviewers = function() {
483 prButtonLock(true, "${_('Please select source and target')}", 'all');
535 if (loadDefaultReviewers._currentRequest) {
536 loadDefaultReviewers._currentRequest.abort();
537 }
538 $('.calculate-reviewers').show();
539 prButtonLock(true, null, 'reviewers');
540
541 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
542
543 var sourceRepo = $sourceRepo.eq(0).val();
544 var sourceRef = $sourceRef.eq(0).val().split(':');
545 var targetRepo = $targetRepo.eq(0).val();
546 var targetRef = $targetRef.eq(0).val().split(':');
547 url += '?source_repo=' + sourceRepo;
548 url += '&source_ref=' + sourceRef[2];
549 url += '&target_repo=' + targetRepo;
550 url += '&target_ref=' + targetRef[2];
551
552 loadDefaultReviewers._currentRequest = $.get(url)
553 .done(function(data) {
554 loadDefaultReviewers._currentRequest = null;
555
556 // reset && add the reviewer based on selected repo
557 $('#review_members').html('');
558 for (var i = 0; i < data.reviewers.length; i++) {
559 var reviewer = data.reviewers[i];
560 addReviewMember(
561 reviewer.user_id, reviewer.firstname,
562 reviewer.lastname, reviewer.username,
563 reviewer.gravatar_link, reviewer.reasons);
564 }
565 $('.calculate-reviewers').hide();
566 prButtonLock(false, null, 'reviewers');
567 });
568 };
569
570 prButtonLock(true, "${_('Please select origin and destination')}", 'all');
571
484
572 // auto-load on init, the target refs select2
485 // auto-load on init, the target refs select2
573 calculateContainerWidth();
486 calculateContainerWidth();
574 targetRepoChanged(defaultTargetRepoData);
487 targetRepoChanged(defaultTargetRepoData);
575
488
576 $('#pullrequest_title').on('keyup', function(e){
489 $('#pullrequest_title').on('keyup', function(e){
577 $(this).removeClass('autogenerated-title');
490 $(this).removeClass('autogenerated-title');
578 });
491 });
579
492
580 % if c.default_source_ref:
493 % if c.default_source_ref:
581 // in case we have a pre-selected value, use it now
494 // in case we have a pre-selected value, use it now
582 $sourceRef.select2('val', '${c.default_source_ref}');
495 $sourceRef.select2('val', '${c.default_source_ref}');
583 loadRepoRefDiffPreview();
496 loadRepoRefDiffPreview();
584 loadDefaultReviewers();
497 reviewersController.loadDefaultReviewers(
498 sourceRepo(), sourceRef(), targetRepo(), targetRef());
585 % endif
499 % endif
586
500
587 ReviewerAutoComplete('#user');
501 ReviewerAutoComplete('#user');
588 });
502 });
589 </script>
503 </script>
590
504
591 </%def>
505 </%def>
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now