##// 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 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -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
21 */
31 */
22 var removeReviewMember = function(reviewer_id, mark_delete){
32 var prButtonLock = function(lockEnabled, msg, scope) {
23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
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 }
24
49
25 if(typeof(mark_delete) === undefined){
50 if (msg) {
26 mark_delete = false;
51 $('#pr_open_message').html(msg);
27 }
52 }
53 };
28
54
29 if(mark_delete === true){
55
30 if (reviewer){
56 /**
31 // now delete the input
57 Generate Title and Description for a PullRequest.
32 $('#reviewer_{0} input'.format(reviewer_id)).remove();
58 In case of 1 commits, the title and description is that one commit
33 // mark as to-delete
59 in case of multiple commits, we iterate on them with max N number of commits,
34 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
60 and build description in a form
35 obj.addClass('to-delete');
61 - commitN
36 obj.css({"text-decoration":"line-through", "opacity": 0.5});
62 - commitN+1
37 }
63 ...
38 }
64
39 else{
65 Title is then constructed from branch names, or other references,
40 $('#reviewer_{0}'.format(reviewer_id)).remove();
66 replacing '-' and '_' into spaces
41 }
67
68 * @param sourceRef
69 * @param elements
70 * @param limit
71 * @returns {*[]}
72 */
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]
42 };
91 };
43
92
44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
93
45 var members = $('#review_members').get(0);
94
46 var reasons_html = '';
95 ReviewersController = function () {
47 var reasons_inputs = '';
96 var self = this;
48 var reasons = reasons || [];
97 this.$reviewRulesContainer = $('#review_rules');
49 if (reasons) {
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
50 for (var i = 0; i < reasons.length; i++) {
99 this.forbidReviewUsers = undefined;
51 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
100 this.$reviewMembers = $('#review_members');
52 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
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();
53 }
187 }
54 }
188
55 var tmpl = '<li id="reviewer_{2}">'+
189 $('.calculate-reviewers').show();
56 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
190 // reset reviewer members
57 '<div class="reviewer_status">'+
191 self.$reviewMembers.empty();
58 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
192
59 '</div>'+
193 prButtonLock(true, null, 'reviewers');
60 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
194 $('#user').hide(); // hide user autocomplete before load
61 '<span class="reviewer_name user">{1}</span>'+
195
62 reasons_html +
196 var url = pyroutes.url('repo_default_reviewers_data',
63 '<input type="hidden" name="user_id" value="{2}">'+
197 {
64 '<input type="hidden" name="__start__" value="reasons:sequence">'+
198 'repo_name': templateContext.repo_name,
65 '{3}'+
199 'source_repo': sourceRepo,
66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
200 'source_ref': sourceRef[2],
67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
201 'target_repo': targetRepo,
68 '<i class="icon-remove-sign"></i>'+
202 'target_ref': targetRef[2]
69 '</div>'+
203 });
70 '</div>'+
204
71 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
205 self.currentRequest = $.get(url)
72 '</li>' ;
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) {
228 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
229
230 if(typeof(mark_delete) === undefined){
231 mark_delete = false;
232 }
233
234 if(mark_delete === true){
235 if (reviewer){
236 // now delete the input
237 $('#reviewer_{0} input'.format(reviewer_id)).remove();
238 // mark as to-delete
239 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
240 obj.addClass('to-delete');
241 obj.css({"text-decoration":"line-through", "opacity": 0.5});
242 }
243 }
244 else{
245 $('#reviewer_{0}'.format(reviewer_id)).remove();
246 }
247 };
248
249 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
250 var members = self.$reviewMembers.get(0);
251 var reasons_html = '';
252 var reasons_inputs = '';
253 var reasons = reasons || [];
254 var mandatory = mandatory || false;
73
255
74 var displayname = "{0} ({1} {2})".format(
256 if (reasons) {
75 nname, escapeHtml(fname), escapeHtml(lname));
257 for (var i = 0; i < reasons.length; i++) {
76 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
258 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
77 // check if we don't have this ID already in
259 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
78 var ids = [];
260 }
79 var _els = $('#review_members li').toArray();
261 }
80 for (el in _els){
262 var tmpl = '' +
81 ids.push(_els[el].id)
263 '<li id="reviewer_{2}" class="reviewer_entry">'+
82 }
264 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
83 if(ids.indexOf('reviewer_'+id) == -1){
265 '<div class="reviewer_status">'+
84 // only add if it's not there
266 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
85 members.innerHTML += element;
267 '</div>'+
86 }
268 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
269 '<span class="reviewer_name user">{1}</span>'+
270 reasons_html +
271 '<input type="hidden" name="user_id" value="{2}">'+
272 '<input type="hidden" name="__start__" value="reasons:sequence">'+
273 '{3}'+
274 '<input type="hidden" name="__end__" value="reasons:sequence">';
275
276 if (mandatory) {
277 tmpl += ''+
278 '<div class="reviewer_member_mandatory_remove">' +
279 '<i class="icon-remove-sign"></i>'+
280 '</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 += ''+
295 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
296 '</li>' ;
297
298 var displayname = "{0} ({1} {2})".format(
299 nname, escapeHtml(fname), escapeHtml(lname));
300 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
301 // check if we don't have this ID already in
302 var ids = [];
303 var _els = self.$reviewMembers.find('li').toArray();
304 for (el in _els){
305 ids.push(_els[el].id)
306 }
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) {
326 // only add if it's not there
327 members.innerHTML += element;
328 }
329
330 };
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 };
87
336
88 };
337 };
89
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(
221 member_data.username, member_data.icon_link, reasons);
464 member_data.id, member_data.first_name, member_data.last_name,
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(
226 data.username, data.icon_link, reasons);
470 data.id, data.first_name, data.last_name,
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 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
27 % if hasattr(c, 'repo_edit_template'):
28 <%include file="${c.repo_edit_template}"/>
29 % else:
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