##// END OF EJS Templates
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
marcink -
r1323:41333240 default
parent child Browse files
Show More
@@ -1,209 +1,209 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.comment import ChangesetCommentsModel
23 from rhodecode.model.comment import CommentsModel
24 from rhodecode.model.db import UserLog
24 from rhodecode.model.db import UserLog
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_error, assert_ok)
28 build_data, api_call, assert_error, assert_ok)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCommentPullRequest(object):
32 class TestCommentPullRequest(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 @pytest.mark.backends("git", "hg")
41 @pytest.mark.backends("git", "hg")
42 def test_api_comment_pull_request(self, pr_util, no_notifications):
42 def test_api_comment_pull_request(self, pr_util, no_notifications):
43 pull_request = pr_util.create_pull_request()
43 pull_request = pr_util.create_pull_request()
44 pull_request_id = pull_request.pull_request_id
44 pull_request_id = pull_request.pull_request_id
45 author = pull_request.user_id
45 author = pull_request.user_id
46 repo = pull_request.target_repo.repo_id
46 repo = pull_request.target_repo.repo_id
47 id_, params = build_data(
47 id_, params = build_data(
48 self.apikey, 'comment_pull_request',
48 self.apikey, 'comment_pull_request',
49 repoid=pull_request.target_repo.repo_name,
49 repoid=pull_request.target_repo.repo_name,
50 pullrequestid=pull_request.pull_request_id,
50 pullrequestid=pull_request.pull_request_id,
51 message='test message')
51 message='test message')
52 response = api_call(self.app, params)
52 response = api_call(self.app, params)
53 pull_request = PullRequestModel().get(pull_request.pull_request_id)
53 pull_request = PullRequestModel().get(pull_request.pull_request_id)
54
54
55 comments = ChangesetCommentsModel().get_comments(
55 comments = CommentsModel().get_comments(
56 pull_request.target_repo.repo_id, pull_request=pull_request)
56 pull_request.target_repo.repo_id, pull_request=pull_request)
57
57
58 expected = {
58 expected = {
59 'pull_request_id': pull_request.pull_request_id,
59 'pull_request_id': pull_request.pull_request_id,
60 'comment_id': comments[-1].comment_id,
60 'comment_id': comments[-1].comment_id,
61 'status': {'given': None, 'was_changed': None}
61 'status': {'given': None, 'was_changed': None}
62 }
62 }
63 assert_ok(id_, expected, response.body)
63 assert_ok(id_, expected, response.body)
64
64
65 action = 'user_commented_pull_request:%d' % pull_request_id
65 action = 'user_commented_pull_request:%d' % pull_request_id
66 journal = UserLog.query()\
66 journal = UserLog.query()\
67 .filter(UserLog.user_id == author)\
67 .filter(UserLog.user_id == author)\
68 .filter(UserLog.repository_id == repo)\
68 .filter(UserLog.repository_id == repo)\
69 .filter(UserLog.action == action)\
69 .filter(UserLog.action == action)\
70 .all()
70 .all()
71 assert len(journal) == 2
71 assert len(journal) == 2
72
72
73 @pytest.mark.backends("git", "hg")
73 @pytest.mark.backends("git", "hg")
74 def test_api_comment_pull_request_change_status(
74 def test_api_comment_pull_request_change_status(
75 self, pr_util, no_notifications):
75 self, pr_util, no_notifications):
76 pull_request = pr_util.create_pull_request()
76 pull_request = pr_util.create_pull_request()
77 pull_request_id = pull_request.pull_request_id
77 pull_request_id = pull_request.pull_request_id
78 id_, params = build_data(
78 id_, params = build_data(
79 self.apikey, 'comment_pull_request',
79 self.apikey, 'comment_pull_request',
80 repoid=pull_request.target_repo.repo_name,
80 repoid=pull_request.target_repo.repo_name,
81 pullrequestid=pull_request.pull_request_id,
81 pullrequestid=pull_request.pull_request_id,
82 status='rejected')
82 status='rejected')
83 response = api_call(self.app, params)
83 response = api_call(self.app, params)
84 pull_request = PullRequestModel().get(pull_request_id)
84 pull_request = PullRequestModel().get(pull_request_id)
85
85
86 comments = ChangesetCommentsModel().get_comments(
86 comments = CommentsModel().get_comments(
87 pull_request.target_repo.repo_id, pull_request=pull_request)
87 pull_request.target_repo.repo_id, pull_request=pull_request)
88 expected = {
88 expected = {
89 'pull_request_id': pull_request.pull_request_id,
89 'pull_request_id': pull_request.pull_request_id,
90 'comment_id': comments[-1].comment_id,
90 'comment_id': comments[-1].comment_id,
91 'status': {'given': 'rejected', 'was_changed': True}
91 'status': {'given': 'rejected', 'was_changed': True}
92 }
92 }
93 assert_ok(id_, expected, response.body)
93 assert_ok(id_, expected, response.body)
94
94
95 @pytest.mark.backends("git", "hg")
95 @pytest.mark.backends("git", "hg")
96 def test_api_comment_pull_request_change_status_with_specific_commit_id(
96 def test_api_comment_pull_request_change_status_with_specific_commit_id(
97 self, pr_util, no_notifications):
97 self, pr_util, no_notifications):
98 pull_request = pr_util.create_pull_request()
98 pull_request = pr_util.create_pull_request()
99 pull_request_id = pull_request.pull_request_id
99 pull_request_id = pull_request.pull_request_id
100 latest_commit_id = 'test_commit'
100 latest_commit_id = 'test_commit'
101 # inject additional revision, to fail test the status change on
101 # inject additional revision, to fail test the status change on
102 # non-latest commit
102 # non-latest commit
103 pull_request.revisions = pull_request.revisions + ['test_commit']
103 pull_request.revisions = pull_request.revisions + ['test_commit']
104
104
105 id_, params = build_data(
105 id_, params = build_data(
106 self.apikey, 'comment_pull_request',
106 self.apikey, 'comment_pull_request',
107 repoid=pull_request.target_repo.repo_name,
107 repoid=pull_request.target_repo.repo_name,
108 pullrequestid=pull_request.pull_request_id,
108 pullrequestid=pull_request.pull_request_id,
109 status='approved', commit_id=latest_commit_id)
109 status='approved', commit_id=latest_commit_id)
110 response = api_call(self.app, params)
110 response = api_call(self.app, params)
111 pull_request = PullRequestModel().get(pull_request_id)
111 pull_request = PullRequestModel().get(pull_request_id)
112
112
113 expected = {
113 expected = {
114 'pull_request_id': pull_request.pull_request_id,
114 'pull_request_id': pull_request.pull_request_id,
115 'comment_id': None,
115 'comment_id': None,
116 'status': {'given': 'approved', 'was_changed': False}
116 'status': {'given': 'approved', 'was_changed': False}
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_comment_pull_request_change_status_with_specific_commit_id(
121 def test_api_comment_pull_request_change_status_with_specific_commit_id(
122 self, pr_util, no_notifications):
122 self, pr_util, no_notifications):
123 pull_request = pr_util.create_pull_request()
123 pull_request = pr_util.create_pull_request()
124 pull_request_id = pull_request.pull_request_id
124 pull_request_id = pull_request.pull_request_id
125 latest_commit_id = pull_request.revisions[0]
125 latest_commit_id = pull_request.revisions[0]
126
126
127 id_, params = build_data(
127 id_, params = build_data(
128 self.apikey, 'comment_pull_request',
128 self.apikey, 'comment_pull_request',
129 repoid=pull_request.target_repo.repo_name,
129 repoid=pull_request.target_repo.repo_name,
130 pullrequestid=pull_request.pull_request_id,
130 pullrequestid=pull_request.pull_request_id,
131 status='approved', commit_id=latest_commit_id)
131 status='approved', commit_id=latest_commit_id)
132 response = api_call(self.app, params)
132 response = api_call(self.app, params)
133 pull_request = PullRequestModel().get(pull_request_id)
133 pull_request = PullRequestModel().get(pull_request_id)
134
134
135 comments = ChangesetCommentsModel().get_comments(
135 comments = CommentsModel().get_comments(
136 pull_request.target_repo.repo_id, pull_request=pull_request)
136 pull_request.target_repo.repo_id, pull_request=pull_request)
137 expected = {
137 expected = {
138 'pull_request_id': pull_request.pull_request_id,
138 'pull_request_id': pull_request.pull_request_id,
139 'comment_id': comments[-1].comment_id,
139 'comment_id': comments[-1].comment_id,
140 'status': {'given': 'approved', 'was_changed': True}
140 'status': {'given': 'approved', 'was_changed': True}
141 }
141 }
142 assert_ok(id_, expected, response.body)
142 assert_ok(id_, expected, response.body)
143
143
144 @pytest.mark.backends("git", "hg")
144 @pytest.mark.backends("git", "hg")
145 def test_api_comment_pull_request_missing_params_error(self, pr_util):
145 def test_api_comment_pull_request_missing_params_error(self, pr_util):
146 pull_request = pr_util.create_pull_request()
146 pull_request = pr_util.create_pull_request()
147 pull_request_id = pull_request.pull_request_id
147 pull_request_id = pull_request.pull_request_id
148 pull_request_repo = pull_request.target_repo.repo_name
148 pull_request_repo = pull_request.target_repo.repo_name
149 id_, params = build_data(
149 id_, params = build_data(
150 self.apikey, 'comment_pull_request',
150 self.apikey, 'comment_pull_request',
151 repoid=pull_request_repo,
151 repoid=pull_request_repo,
152 pullrequestid=pull_request_id)
152 pullrequestid=pull_request_id)
153 response = api_call(self.app, params)
153 response = api_call(self.app, params)
154
154
155 expected = 'Both message and status parameters are missing. At least one is required.'
155 expected = 'Both message and status parameters are missing. At least one is required.'
156 assert_error(id_, expected, given=response.body)
156 assert_error(id_, expected, given=response.body)
157
157
158 @pytest.mark.backends("git", "hg")
158 @pytest.mark.backends("git", "hg")
159 def test_api_comment_pull_request_unknown_status_error(self, pr_util):
159 def test_api_comment_pull_request_unknown_status_error(self, pr_util):
160 pull_request = pr_util.create_pull_request()
160 pull_request = pr_util.create_pull_request()
161 pull_request_id = pull_request.pull_request_id
161 pull_request_id = pull_request.pull_request_id
162 pull_request_repo = pull_request.target_repo.repo_name
162 pull_request_repo = pull_request.target_repo.repo_name
163 id_, params = build_data(
163 id_, params = build_data(
164 self.apikey, 'comment_pull_request',
164 self.apikey, 'comment_pull_request',
165 repoid=pull_request_repo,
165 repoid=pull_request_repo,
166 pullrequestid=pull_request_id,
166 pullrequestid=pull_request_id,
167 status='42')
167 status='42')
168 response = api_call(self.app, params)
168 response = api_call(self.app, params)
169
169
170 expected = 'Unknown comment status: `42`'
170 expected = 'Unknown comment status: `42`'
171 assert_error(id_, expected, given=response.body)
171 assert_error(id_, expected, given=response.body)
172
172
173 @pytest.mark.backends("git", "hg")
173 @pytest.mark.backends("git", "hg")
174 def test_api_comment_pull_request_repo_error(self):
174 def test_api_comment_pull_request_repo_error(self):
175 id_, params = build_data(
175 id_, params = build_data(
176 self.apikey, 'comment_pull_request',
176 self.apikey, 'comment_pull_request',
177 repoid=666, pullrequestid=1)
177 repoid=666, pullrequestid=1)
178 response = api_call(self.app, params)
178 response = api_call(self.app, params)
179
179
180 expected = 'repository `666` does not exist'
180 expected = 'repository `666` does not exist'
181 assert_error(id_, expected, given=response.body)
181 assert_error(id_, expected, given=response.body)
182
182
183 @pytest.mark.backends("git", "hg")
183 @pytest.mark.backends("git", "hg")
184 def test_api_comment_pull_request_non_admin_with_userid_error(
184 def test_api_comment_pull_request_non_admin_with_userid_error(
185 self, pr_util):
185 self, pr_util):
186 pull_request = pr_util.create_pull_request()
186 pull_request = pr_util.create_pull_request()
187 id_, params = build_data(
187 id_, params = build_data(
188 self.apikey_regular, 'comment_pull_request',
188 self.apikey_regular, 'comment_pull_request',
189 repoid=pull_request.target_repo.repo_name,
189 repoid=pull_request.target_repo.repo_name,
190 pullrequestid=pull_request.pull_request_id,
190 pullrequestid=pull_request.pull_request_id,
191 userid=TEST_USER_ADMIN_LOGIN)
191 userid=TEST_USER_ADMIN_LOGIN)
192 response = api_call(self.app, params)
192 response = api_call(self.app, params)
193
193
194 expected = 'userid is not the same as your user'
194 expected = 'userid is not the same as your user'
195 assert_error(id_, expected, given=response.body)
195 assert_error(id_, expected, given=response.body)
196
196
197 @pytest.mark.backends("git", "hg")
197 @pytest.mark.backends("git", "hg")
198 def test_api_comment_pull_request_wrong_commit_id_error(self, pr_util):
198 def test_api_comment_pull_request_wrong_commit_id_error(self, pr_util):
199 pull_request = pr_util.create_pull_request()
199 pull_request = pr_util.create_pull_request()
200 id_, params = build_data(
200 id_, params = build_data(
201 self.apikey_regular, 'comment_pull_request',
201 self.apikey_regular, 'comment_pull_request',
202 repoid=pull_request.target_repo.repo_name,
202 repoid=pull_request.target_repo.repo_name,
203 status='approved',
203 status='approved',
204 pullrequestid=pull_request.pull_request_id,
204 pullrequestid=pull_request.pull_request_id,
205 commit_id='XXX')
205 commit_id='XXX')
206 response = api_call(self.app, params)
206 response = api_call(self.app, params)
207
207
208 expected = 'Invalid commit_id `XXX` for this pull request.'
208 expected = 'Invalid commit_id `XXX` for this pull request.'
209 assert_error(id_, expected, given=response.body)
209 assert_error(id_, expected, given=response.body)
@@ -1,714 +1,714 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
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 ChangesetCommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus
34 from rhodecode.model.db import Session, ChangesetStatus
35 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.pull_request import PullRequestModel
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 @jsonrpc_method()
41 @jsonrpc_method()
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 """
43 """
44 Get a pull request based on the given ID.
44 Get a pull request based on the given ID.
45
45
46 :param apiuser: This is filled automatically from the |authtoken|.
46 :param apiuser: This is filled automatically from the |authtoken|.
47 :type apiuser: AuthUser
47 :type apiuser: AuthUser
48 :param repoid: Repository name or repository ID from where the pull
48 :param repoid: Repository name or repository ID from where the pull
49 request was opened.
49 request was opened.
50 :type repoid: str or int
50 :type repoid: str or int
51 :param pullrequestid: ID of the requested pull request.
51 :param pullrequestid: ID of the requested pull request.
52 :type pullrequestid: int
52 :type pullrequestid: int
53
53
54 Example output:
54 Example output:
55
55
56 .. code-block:: bash
56 .. code-block:: bash
57
57
58 "id": <id_given_in_input>,
58 "id": <id_given_in_input>,
59 "result":
59 "result":
60 {
60 {
61 "pull_request_id": "<pull_request_id>",
61 "pull_request_id": "<pull_request_id>",
62 "url": "<url>",
62 "url": "<url>",
63 "title": "<title>",
63 "title": "<title>",
64 "description": "<description>",
64 "description": "<description>",
65 "status" : "<status>",
65 "status" : "<status>",
66 "created_on": "<date_time_created>",
66 "created_on": "<date_time_created>",
67 "updated_on": "<date_time_updated>",
67 "updated_on": "<date_time_updated>",
68 "commit_ids": [
68 "commit_ids": [
69 ...
69 ...
70 "<commit_id>",
70 "<commit_id>",
71 "<commit_id>",
71 "<commit_id>",
72 ...
72 ...
73 ],
73 ],
74 "review_status": "<review_status>",
74 "review_status": "<review_status>",
75 "mergeable": {
75 "mergeable": {
76 "status": "<bool>",
76 "status": "<bool>",
77 "message": "<message>",
77 "message": "<message>",
78 },
78 },
79 "source": {
79 "source": {
80 "clone_url": "<clone_url>",
80 "clone_url": "<clone_url>",
81 "repository": "<repository_name>",
81 "repository": "<repository_name>",
82 "reference":
82 "reference":
83 {
83 {
84 "name": "<name>",
84 "name": "<name>",
85 "type": "<type>",
85 "type": "<type>",
86 "commit_id": "<commit_id>",
86 "commit_id": "<commit_id>",
87 }
87 }
88 },
88 },
89 "target": {
89 "target": {
90 "clone_url": "<clone_url>",
90 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
91 "repository": "<repository_name>",
92 "reference":
92 "reference":
93 {
93 {
94 "name": "<name>",
94 "name": "<name>",
95 "type": "<type>",
95 "type": "<type>",
96 "commit_id": "<commit_id>",
96 "commit_id": "<commit_id>",
97 }
97 }
98 },
98 },
99 "merge": {
99 "merge": {
100 "clone_url": "<clone_url>",
100 "clone_url": "<clone_url>",
101 "reference":
101 "reference":
102 {
102 {
103 "name": "<name>",
103 "name": "<name>",
104 "type": "<type>",
104 "type": "<type>",
105 "commit_id": "<commit_id>",
105 "commit_id": "<commit_id>",
106 }
106 }
107 },
107 },
108 "author": <user_obj>,
108 "author": <user_obj>,
109 "reviewers": [
109 "reviewers": [
110 ...
110 ...
111 {
111 {
112 "user": "<user_obj>",
112 "user": "<user_obj>",
113 "review_status": "<review_status>",
113 "review_status": "<review_status>",
114 }
114 }
115 ...
115 ...
116 ]
116 ]
117 },
117 },
118 "error": null
118 "error": null
119 """
119 """
120 get_repo_or_error(repoid)
120 get_repo_or_error(repoid)
121 pull_request = get_pull_request_or_error(pullrequestid)
121 pull_request = get_pull_request_or_error(pullrequestid)
122 if not PullRequestModel().check_user_read(
122 if not PullRequestModel().check_user_read(
123 pull_request, apiuser, api=True):
123 pull_request, apiuser, api=True):
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
125 data = pull_request.get_api_data()
125 data = pull_request.get_api_data()
126 return data
126 return data
127
127
128
128
129 @jsonrpc_method()
129 @jsonrpc_method()
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
131 """
131 """
132 Get all pull requests from the repository specified in `repoid`.
132 Get all pull requests from the repository specified in `repoid`.
133
133
134 :param apiuser: This is filled automatically from the |authtoken|.
134 :param apiuser: This is filled automatically from the |authtoken|.
135 :type apiuser: AuthUser
135 :type apiuser: AuthUser
136 :param repoid: Repository name or repository ID.
136 :param repoid: Repository name or repository ID.
137 :type repoid: str or int
137 :type repoid: str or int
138 :param status: Only return pull requests with the specified status.
138 :param status: Only return pull requests with the specified status.
139 Valid options are.
139 Valid options are.
140 * ``new`` (default)
140 * ``new`` (default)
141 * ``open``
141 * ``open``
142 * ``closed``
142 * ``closed``
143 :type status: str
143 :type status: str
144
144
145 Example output:
145 Example output:
146
146
147 .. code-block:: bash
147 .. code-block:: bash
148
148
149 "id": <id_given_in_input>,
149 "id": <id_given_in_input>,
150 "result":
150 "result":
151 [
151 [
152 ...
152 ...
153 {
153 {
154 "pull_request_id": "<pull_request_id>",
154 "pull_request_id": "<pull_request_id>",
155 "url": "<url>",
155 "url": "<url>",
156 "title" : "<title>",
156 "title" : "<title>",
157 "description": "<description>",
157 "description": "<description>",
158 "status": "<status>",
158 "status": "<status>",
159 "created_on": "<date_time_created>",
159 "created_on": "<date_time_created>",
160 "updated_on": "<date_time_updated>",
160 "updated_on": "<date_time_updated>",
161 "commit_ids": [
161 "commit_ids": [
162 ...
162 ...
163 "<commit_id>",
163 "<commit_id>",
164 "<commit_id>",
164 "<commit_id>",
165 ...
165 ...
166 ],
166 ],
167 "review_status": "<review_status>",
167 "review_status": "<review_status>",
168 "mergeable": {
168 "mergeable": {
169 "status": "<bool>",
169 "status": "<bool>",
170 "message: "<message>",
170 "message: "<message>",
171 },
171 },
172 "source": {
172 "source": {
173 "clone_url": "<clone_url>",
173 "clone_url": "<clone_url>",
174 "reference":
174 "reference":
175 {
175 {
176 "name": "<name>",
176 "name": "<name>",
177 "type": "<type>",
177 "type": "<type>",
178 "commit_id": "<commit_id>",
178 "commit_id": "<commit_id>",
179 }
179 }
180 },
180 },
181 "target": {
181 "target": {
182 "clone_url": "<clone_url>",
182 "clone_url": "<clone_url>",
183 "reference":
183 "reference":
184 {
184 {
185 "name": "<name>",
185 "name": "<name>",
186 "type": "<type>",
186 "type": "<type>",
187 "commit_id": "<commit_id>",
187 "commit_id": "<commit_id>",
188 }
188 }
189 },
189 },
190 "merge": {
190 "merge": {
191 "clone_url": "<clone_url>",
191 "clone_url": "<clone_url>",
192 "reference":
192 "reference":
193 {
193 {
194 "name": "<name>",
194 "name": "<name>",
195 "type": "<type>",
195 "type": "<type>",
196 "commit_id": "<commit_id>",
196 "commit_id": "<commit_id>",
197 }
197 }
198 },
198 },
199 "author": <user_obj>,
199 "author": <user_obj>,
200 "reviewers": [
200 "reviewers": [
201 ...
201 ...
202 {
202 {
203 "user": "<user_obj>",
203 "user": "<user_obj>",
204 "review_status": "<review_status>",
204 "review_status": "<review_status>",
205 }
205 }
206 ...
206 ...
207 ]
207 ]
208 }
208 }
209 ...
209 ...
210 ],
210 ],
211 "error": null
211 "error": null
212
212
213 """
213 """
214 repo = get_repo_or_error(repoid)
214 repo = get_repo_or_error(repoid)
215 if not has_superadmin_permission(apiuser):
215 if not has_superadmin_permission(apiuser):
216 _perms = (
216 _perms = (
217 'repository.admin', 'repository.write', 'repository.read',)
217 'repository.admin', 'repository.write', 'repository.read',)
218 validate_repo_permissions(apiuser, repoid, repo, _perms)
218 validate_repo_permissions(apiuser, repoid, repo, _perms)
219
219
220 status = Optional.extract(status)
220 status = Optional.extract(status)
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
222 data = [pr.get_api_data() for pr in pull_requests]
222 data = [pr.get_api_data() for pr in pull_requests]
223 return data
223 return data
224
224
225
225
226 @jsonrpc_method()
226 @jsonrpc_method()
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
228 userid=Optional(OAttr('apiuser'))):
228 userid=Optional(OAttr('apiuser'))):
229 """
229 """
230 Merge the pull request specified by `pullrequestid` into its target
230 Merge the pull request specified by `pullrequestid` into its target
231 repository.
231 repository.
232
232
233 :param apiuser: This is filled automatically from the |authtoken|.
233 :param apiuser: This is filled automatically from the |authtoken|.
234 :type apiuser: AuthUser
234 :type apiuser: AuthUser
235 :param repoid: The Repository name or repository ID of the
235 :param repoid: The Repository name or repository ID of the
236 target repository to which the |pr| is to be merged.
236 target repository to which the |pr| is to be merged.
237 :type repoid: str or int
237 :type repoid: str or int
238 :param pullrequestid: ID of the pull request which shall be merged.
238 :param pullrequestid: ID of the pull request which shall be merged.
239 :type pullrequestid: int
239 :type pullrequestid: int
240 :param userid: Merge the pull request as this user.
240 :param userid: Merge the pull request as this user.
241 :type userid: Optional(str or int)
241 :type userid: Optional(str or int)
242
242
243 Example output:
243 Example output:
244
244
245 .. code-block:: bash
245 .. code-block:: bash
246
246
247 "id": <id_given_in_input>,
247 "id": <id_given_in_input>,
248 "result":
248 "result":
249 {
249 {
250 "executed": "<bool>",
250 "executed": "<bool>",
251 "failure_reason": "<int>",
251 "failure_reason": "<int>",
252 "merge_commit_id": "<merge_commit_id>",
252 "merge_commit_id": "<merge_commit_id>",
253 "possible": "<bool>",
253 "possible": "<bool>",
254 "merge_ref": {
254 "merge_ref": {
255 "commit_id": "<commit_id>",
255 "commit_id": "<commit_id>",
256 "type": "<type>",
256 "type": "<type>",
257 "name": "<name>"
257 "name": "<name>"
258 }
258 }
259 },
259 },
260 "error": null
260 "error": null
261
261
262 """
262 """
263 repo = get_repo_or_error(repoid)
263 repo = get_repo_or_error(repoid)
264 if not isinstance(userid, Optional):
264 if not isinstance(userid, Optional):
265 if (has_superadmin_permission(apiuser) or
265 if (has_superadmin_permission(apiuser) or
266 HasRepoPermissionAnyApi('repository.admin')(
266 HasRepoPermissionAnyApi('repository.admin')(
267 user=apiuser, repo_name=repo.repo_name)):
267 user=apiuser, repo_name=repo.repo_name)):
268 apiuser = get_user_or_error(userid)
268 apiuser = get_user_or_error(userid)
269 else:
269 else:
270 raise JSONRPCError('userid is not the same as your user')
270 raise JSONRPCError('userid is not the same as your user')
271
271
272 pull_request = get_pull_request_or_error(pullrequestid)
272 pull_request = get_pull_request_or_error(pullrequestid)
273 if not PullRequestModel().check_user_merge(
273 if not PullRequestModel().check_user_merge(
274 pull_request, apiuser, api=True):
274 pull_request, apiuser, api=True):
275 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
275 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
276 if pull_request.is_closed():
276 if pull_request.is_closed():
277 raise JSONRPCError(
277 raise JSONRPCError(
278 'pull request `%s` merge failed, pull request is closed' % (
278 'pull request `%s` merge failed, pull request is closed' % (
279 pullrequestid,))
279 pullrequestid,))
280
280
281 target_repo = pull_request.target_repo
281 target_repo = pull_request.target_repo
282 extras = vcs_operation_context(
282 extras = vcs_operation_context(
283 request.environ, repo_name=target_repo.repo_name,
283 request.environ, repo_name=target_repo.repo_name,
284 username=apiuser.username, action='push',
284 username=apiuser.username, action='push',
285 scm=target_repo.repo_type)
285 scm=target_repo.repo_type)
286 merge_response = PullRequestModel().merge(
286 merge_response = PullRequestModel().merge(
287 pull_request, apiuser, extras=extras)
287 pull_request, apiuser, extras=extras)
288 if merge_response.executed:
288 if merge_response.executed:
289 PullRequestModel().close_pull_request(
289 PullRequestModel().close_pull_request(
290 pull_request.pull_request_id, apiuser)
290 pull_request.pull_request_id, apiuser)
291
291
292 Session().commit()
292 Session().commit()
293
293
294 # In previous versions the merge response directly contained the merge
294 # In previous versions the merge response directly contained the merge
295 # commit id. It is now contained in the merge reference object. To be
295 # commit id. It is now contained in the merge reference object. To be
296 # backwards compatible we have to extract it again.
296 # backwards compatible we have to extract it again.
297 merge_response = merge_response._asdict()
297 merge_response = merge_response._asdict()
298 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
298 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
299
299
300 return merge_response
300 return merge_response
301
301
302
302
303 @jsonrpc_method()
303 @jsonrpc_method()
304 def close_pull_request(request, apiuser, repoid, pullrequestid,
304 def close_pull_request(request, apiuser, repoid, pullrequestid,
305 userid=Optional(OAttr('apiuser'))):
305 userid=Optional(OAttr('apiuser'))):
306 """
306 """
307 Close the pull request specified by `pullrequestid`.
307 Close the pull request specified by `pullrequestid`.
308
308
309 :param apiuser: This is filled automatically from the |authtoken|.
309 :param apiuser: This is filled automatically from the |authtoken|.
310 :type apiuser: AuthUser
310 :type apiuser: AuthUser
311 :param repoid: Repository name or repository ID to which the pull
311 :param repoid: Repository name or repository ID to which the pull
312 request belongs.
312 request belongs.
313 :type repoid: str or int
313 :type repoid: str or int
314 :param pullrequestid: ID of the pull request to be closed.
314 :param pullrequestid: ID of the pull request to be closed.
315 :type pullrequestid: int
315 :type pullrequestid: int
316 :param userid: Close the pull request as this user.
316 :param userid: Close the pull request as this user.
317 :type userid: Optional(str or int)
317 :type userid: Optional(str or int)
318
318
319 Example output:
319 Example output:
320
320
321 .. code-block:: bash
321 .. code-block:: bash
322
322
323 "id": <id_given_in_input>,
323 "id": <id_given_in_input>,
324 "result":
324 "result":
325 {
325 {
326 "pull_request_id": "<int>",
326 "pull_request_id": "<int>",
327 "closed": "<bool>"
327 "closed": "<bool>"
328 },
328 },
329 "error": null
329 "error": null
330
330
331 """
331 """
332 repo = get_repo_or_error(repoid)
332 repo = get_repo_or_error(repoid)
333 if not isinstance(userid, Optional):
333 if not isinstance(userid, Optional):
334 if (has_superadmin_permission(apiuser) or
334 if (has_superadmin_permission(apiuser) or
335 HasRepoPermissionAnyApi('repository.admin')(
335 HasRepoPermissionAnyApi('repository.admin')(
336 user=apiuser, repo_name=repo.repo_name)):
336 user=apiuser, repo_name=repo.repo_name)):
337 apiuser = get_user_or_error(userid)
337 apiuser = get_user_or_error(userid)
338 else:
338 else:
339 raise JSONRPCError('userid is not the same as your user')
339 raise JSONRPCError('userid is not the same as your user')
340
340
341 pull_request = get_pull_request_or_error(pullrequestid)
341 pull_request = get_pull_request_or_error(pullrequestid)
342 if not PullRequestModel().check_user_update(
342 if not PullRequestModel().check_user_update(
343 pull_request, apiuser, api=True):
343 pull_request, apiuser, api=True):
344 raise JSONRPCError(
344 raise JSONRPCError(
345 'pull request `%s` close failed, no permission to close.' % (
345 'pull request `%s` close failed, no permission to close.' % (
346 pullrequestid,))
346 pullrequestid,))
347 if pull_request.is_closed():
347 if pull_request.is_closed():
348 raise JSONRPCError(
348 raise JSONRPCError(
349 'pull request `%s` is already closed' % (pullrequestid,))
349 'pull request `%s` is already closed' % (pullrequestid,))
350
350
351 PullRequestModel().close_pull_request(
351 PullRequestModel().close_pull_request(
352 pull_request.pull_request_id, apiuser)
352 pull_request.pull_request_id, apiuser)
353 Session().commit()
353 Session().commit()
354 data = {
354 data = {
355 'pull_request_id': pull_request.pull_request_id,
355 'pull_request_id': pull_request.pull_request_id,
356 'closed': True,
356 'closed': True,
357 }
357 }
358 return data
358 return data
359
359
360
360
361 @jsonrpc_method()
361 @jsonrpc_method()
362 def comment_pull_request(request, apiuser, repoid, pullrequestid,
362 def comment_pull_request(request, apiuser, repoid, pullrequestid,
363 message=Optional(None), status=Optional(None),
363 message=Optional(None), status=Optional(None),
364 commit_id=Optional(None),
364 commit_id=Optional(None),
365 userid=Optional(OAttr('apiuser'))):
365 userid=Optional(OAttr('apiuser'))):
366 """
366 """
367 Comment on the pull request specified with the `pullrequestid`,
367 Comment on the pull request specified with the `pullrequestid`,
368 in the |repo| specified by the `repoid`, and optionally change the
368 in the |repo| specified by the `repoid`, and optionally change the
369 review status.
369 review status.
370
370
371 :param apiuser: This is filled automatically from the |authtoken|.
371 :param apiuser: This is filled automatically from the |authtoken|.
372 :type apiuser: AuthUser
372 :type apiuser: AuthUser
373 :param repoid: The repository name or repository ID.
373 :param repoid: The repository name or repository ID.
374 :type repoid: str or int
374 :type repoid: str or int
375 :param pullrequestid: The pull request ID.
375 :param pullrequestid: The pull request ID.
376 :type pullrequestid: int
376 :type pullrequestid: int
377 :param message: The text content of the comment.
377 :param message: The text content of the comment.
378 :type message: str
378 :type message: str
379 :param status: (**Optional**) Set the approval status of the pull
379 :param status: (**Optional**) Set the approval status of the pull
380 request. Valid options are:
380 request. Valid options are:
381 * not_reviewed
381 * not_reviewed
382 * approved
382 * approved
383 * rejected
383 * rejected
384 * under_review
384 * under_review
385 :type status: str
385 :type status: str
386 :param commit_id: Specify the commit_id for which to set a comment. If
386 :param commit_id: Specify the commit_id for which to set a comment. If
387 given commit_id is different than latest in the PR status
387 given commit_id is different than latest in the PR status
388 change won't be performed.
388 change won't be performed.
389 :type commit_id: str
389 :type commit_id: str
390 :param userid: Comment on the pull request as this user
390 :param userid: Comment on the pull request as this user
391 :type userid: Optional(str or int)
391 :type userid: Optional(str or int)
392
392
393 Example output:
393 Example output:
394
394
395 .. code-block:: bash
395 .. code-block:: bash
396
396
397 id : <id_given_in_input>
397 id : <id_given_in_input>
398 result :
398 result :
399 {
399 {
400 "pull_request_id": "<Integer>",
400 "pull_request_id": "<Integer>",
401 "comment_id": "<Integer>",
401 "comment_id": "<Integer>",
402 "status": {"given": <given_status>,
402 "status": {"given": <given_status>,
403 "was_changed": <bool status_was_actually_changed> },
403 "was_changed": <bool status_was_actually_changed> },
404 }
404 }
405 error : null
405 error : null
406 """
406 """
407 repo = get_repo_or_error(repoid)
407 repo = get_repo_or_error(repoid)
408 if not isinstance(userid, Optional):
408 if not isinstance(userid, Optional):
409 if (has_superadmin_permission(apiuser) or
409 if (has_superadmin_permission(apiuser) or
410 HasRepoPermissionAnyApi('repository.admin')(
410 HasRepoPermissionAnyApi('repository.admin')(
411 user=apiuser, repo_name=repo.repo_name)):
411 user=apiuser, repo_name=repo.repo_name)):
412 apiuser = get_user_or_error(userid)
412 apiuser = get_user_or_error(userid)
413 else:
413 else:
414 raise JSONRPCError('userid is not the same as your user')
414 raise JSONRPCError('userid is not the same as your user')
415
415
416 pull_request = get_pull_request_or_error(pullrequestid)
416 pull_request = get_pull_request_or_error(pullrequestid)
417 if not PullRequestModel().check_user_read(
417 if not PullRequestModel().check_user_read(
418 pull_request, apiuser, api=True):
418 pull_request, apiuser, api=True):
419 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
419 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
420 message = Optional.extract(message)
420 message = Optional.extract(message)
421 status = Optional.extract(status)
421 status = Optional.extract(status)
422 commit_id = Optional.extract(commit_id)
422 commit_id = Optional.extract(commit_id)
423
423
424 if not message and not status:
424 if not message and not status:
425 raise JSONRPCError(
425 raise JSONRPCError(
426 'Both message and status parameters are missing. '
426 'Both message and status parameters are missing. '
427 'At least one is required.')
427 'At least one is required.')
428
428
429 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
429 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
430 status is not None):
430 status is not None):
431 raise JSONRPCError('Unknown comment status: `%s`' % status)
431 raise JSONRPCError('Unknown comment status: `%s`' % status)
432
432
433 if commit_id and commit_id not in pull_request.revisions:
433 if commit_id and commit_id not in pull_request.revisions:
434 raise JSONRPCError(
434 raise JSONRPCError(
435 'Invalid commit_id `%s` for this pull request.' % commit_id)
435 'Invalid commit_id `%s` for this pull request.' % commit_id)
436
436
437 allowed_to_change_status = PullRequestModel().check_user_change_status(
437 allowed_to_change_status = PullRequestModel().check_user_change_status(
438 pull_request, apiuser)
438 pull_request, apiuser)
439
439
440 # if commit_id is passed re-validated if user is allowed to change status
440 # if commit_id is passed re-validated if user is allowed to change status
441 # based on latest commit_id from the PR
441 # based on latest commit_id from the PR
442 if commit_id:
442 if commit_id:
443 commit_idx = pull_request.revisions.index(commit_id)
443 commit_idx = pull_request.revisions.index(commit_id)
444 if commit_idx != 0:
444 if commit_idx != 0:
445 allowed_to_change_status = False
445 allowed_to_change_status = False
446
446
447 text = message
447 text = message
448 status_label = ChangesetStatus.get_status_lbl(status)
448 status_label = ChangesetStatus.get_status_lbl(status)
449 if status and allowed_to_change_status:
449 if status and allowed_to_change_status:
450 st_message = ('Status change %(transition_icon)s %(status)s'
450 st_message = ('Status change %(transition_icon)s %(status)s'
451 % {'transition_icon': '>', 'status': status_label})
451 % {'transition_icon': '>', 'status': status_label})
452 text = message or st_message
452 text = message or st_message
453
453
454 rc_config = SettingsModel().get_all_settings()
454 rc_config = SettingsModel().get_all_settings()
455 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
455 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
456
456
457 status_change = status and allowed_to_change_status
457 status_change = status and allowed_to_change_status
458 comment = ChangesetCommentsModel().create(
458 comment = CommentsModel().create(
459 text=text,
459 text=text,
460 repo=pull_request.target_repo.repo_id,
460 repo=pull_request.target_repo.repo_id,
461 user=apiuser.user_id,
461 user=apiuser.user_id,
462 pull_request=pull_request.pull_request_id,
462 pull_request=pull_request.pull_request_id,
463 f_path=None,
463 f_path=None,
464 line_no=None,
464 line_no=None,
465 status_change=(status_label if status_change else None),
465 status_change=(status_label if status_change else None),
466 status_change_type=(status if status_change else None),
466 status_change_type=(status if status_change else None),
467 closing_pr=False,
467 closing_pr=False,
468 renderer=renderer
468 renderer=renderer
469 )
469 )
470
470
471 if allowed_to_change_status and status:
471 if allowed_to_change_status and status:
472 ChangesetStatusModel().set_status(
472 ChangesetStatusModel().set_status(
473 pull_request.target_repo.repo_id,
473 pull_request.target_repo.repo_id,
474 status,
474 status,
475 apiuser.user_id,
475 apiuser.user_id,
476 comment,
476 comment,
477 pull_request=pull_request.pull_request_id
477 pull_request=pull_request.pull_request_id
478 )
478 )
479 Session().flush()
479 Session().flush()
480
480
481 Session().commit()
481 Session().commit()
482 data = {
482 data = {
483 'pull_request_id': pull_request.pull_request_id,
483 'pull_request_id': pull_request.pull_request_id,
484 'comment_id': comment.comment_id if comment else None,
484 'comment_id': comment.comment_id if comment else None,
485 'status': {'given': status, 'was_changed': status_change},
485 'status': {'given': status, 'was_changed': status_change},
486 }
486 }
487 return data
487 return data
488
488
489
489
490 @jsonrpc_method()
490 @jsonrpc_method()
491 def create_pull_request(
491 def create_pull_request(
492 request, apiuser, source_repo, target_repo, source_ref, target_ref,
492 request, apiuser, source_repo, target_repo, source_ref, target_ref,
493 title, description=Optional(''), reviewers=Optional(None)):
493 title, description=Optional(''), reviewers=Optional(None)):
494 """
494 """
495 Creates a new pull request.
495 Creates a new pull request.
496
496
497 Accepts refs in the following formats:
497 Accepts refs in the following formats:
498
498
499 * branch:<branch_name>:<sha>
499 * branch:<branch_name>:<sha>
500 * branch:<branch_name>
500 * branch:<branch_name>
501 * bookmark:<bookmark_name>:<sha> (Mercurial only)
501 * bookmark:<bookmark_name>:<sha> (Mercurial only)
502 * bookmark:<bookmark_name> (Mercurial only)
502 * bookmark:<bookmark_name> (Mercurial only)
503
503
504 :param apiuser: This is filled automatically from the |authtoken|.
504 :param apiuser: This is filled automatically from the |authtoken|.
505 :type apiuser: AuthUser
505 :type apiuser: AuthUser
506 :param source_repo: Set the source repository name.
506 :param source_repo: Set the source repository name.
507 :type source_repo: str
507 :type source_repo: str
508 :param target_repo: Set the target repository name.
508 :param target_repo: Set the target repository name.
509 :type target_repo: str
509 :type target_repo: str
510 :param source_ref: Set the source ref name.
510 :param source_ref: Set the source ref name.
511 :type source_ref: str
511 :type source_ref: str
512 :param target_ref: Set the target ref name.
512 :param target_ref: Set the target ref name.
513 :type target_ref: str
513 :type target_ref: str
514 :param title: Set the pull request title.
514 :param title: Set the pull request title.
515 :type title: str
515 :type title: str
516 :param description: Set the pull request description.
516 :param description: Set the pull request description.
517 :type description: Optional(str)
517 :type description: Optional(str)
518 :param reviewers: Set the new pull request reviewers list.
518 :param reviewers: Set the new pull request reviewers list.
519 :type reviewers: Optional(list)
519 :type reviewers: Optional(list)
520 Accepts username strings or objects of the format:
520 Accepts username strings or objects of the format:
521 {
521 {
522 'username': 'nick', 'reasons': ['original author']
522 'username': 'nick', 'reasons': ['original author']
523 }
523 }
524 """
524 """
525
525
526 source = get_repo_or_error(source_repo)
526 source = get_repo_or_error(source_repo)
527 target = get_repo_or_error(target_repo)
527 target = get_repo_or_error(target_repo)
528 if not has_superadmin_permission(apiuser):
528 if not has_superadmin_permission(apiuser):
529 _perms = ('repository.admin', 'repository.write', 'repository.read',)
529 _perms = ('repository.admin', 'repository.write', 'repository.read',)
530 validate_repo_permissions(apiuser, source_repo, source, _perms)
530 validate_repo_permissions(apiuser, source_repo, source, _perms)
531
531
532 full_source_ref = resolve_ref_or_error(source_ref, source)
532 full_source_ref = resolve_ref_or_error(source_ref, source)
533 full_target_ref = resolve_ref_or_error(target_ref, target)
533 full_target_ref = resolve_ref_or_error(target_ref, target)
534 source_commit = get_commit_or_error(full_source_ref, source)
534 source_commit = get_commit_or_error(full_source_ref, source)
535 target_commit = get_commit_or_error(full_target_ref, target)
535 target_commit = get_commit_or_error(full_target_ref, target)
536 source_scm = source.scm_instance()
536 source_scm = source.scm_instance()
537 target_scm = target.scm_instance()
537 target_scm = target.scm_instance()
538
538
539 commit_ranges = target_scm.compare(
539 commit_ranges = target_scm.compare(
540 target_commit.raw_id, source_commit.raw_id, source_scm,
540 target_commit.raw_id, source_commit.raw_id, source_scm,
541 merge=True, pre_load=[])
541 merge=True, pre_load=[])
542
542
543 ancestor = target_scm.get_common_ancestor(
543 ancestor = target_scm.get_common_ancestor(
544 target_commit.raw_id, source_commit.raw_id, source_scm)
544 target_commit.raw_id, source_commit.raw_id, source_scm)
545
545
546 if not commit_ranges:
546 if not commit_ranges:
547 raise JSONRPCError('no commits found')
547 raise JSONRPCError('no commits found')
548
548
549 if not ancestor:
549 if not ancestor:
550 raise JSONRPCError('no common ancestor found')
550 raise JSONRPCError('no common ancestor found')
551
551
552 reviewer_objects = Optional.extract(reviewers) or []
552 reviewer_objects = Optional.extract(reviewers) or []
553 if not isinstance(reviewer_objects, list):
553 if not isinstance(reviewer_objects, list):
554 raise JSONRPCError('reviewers should be specified as a list')
554 raise JSONRPCError('reviewers should be specified as a list')
555
555
556 reviewers_reasons = []
556 reviewers_reasons = []
557 for reviewer_object in reviewer_objects:
557 for reviewer_object in reviewer_objects:
558 reviewer_reasons = []
558 reviewer_reasons = []
559 if isinstance(reviewer_object, (basestring, int)):
559 if isinstance(reviewer_object, (basestring, int)):
560 reviewer_username = reviewer_object
560 reviewer_username = reviewer_object
561 else:
561 else:
562 reviewer_username = reviewer_object['username']
562 reviewer_username = reviewer_object['username']
563 reviewer_reasons = reviewer_object.get('reasons', [])
563 reviewer_reasons = reviewer_object.get('reasons', [])
564
564
565 user = get_user_or_error(reviewer_username)
565 user = get_user_or_error(reviewer_username)
566 reviewers_reasons.append((user.user_id, reviewer_reasons))
566 reviewers_reasons.append((user.user_id, reviewer_reasons))
567
567
568 pull_request_model = PullRequestModel()
568 pull_request_model = PullRequestModel()
569 pull_request = pull_request_model.create(
569 pull_request = pull_request_model.create(
570 created_by=apiuser.user_id,
570 created_by=apiuser.user_id,
571 source_repo=source_repo,
571 source_repo=source_repo,
572 source_ref=full_source_ref,
572 source_ref=full_source_ref,
573 target_repo=target_repo,
573 target_repo=target_repo,
574 target_ref=full_target_ref,
574 target_ref=full_target_ref,
575 revisions=reversed(
575 revisions=reversed(
576 [commit.raw_id for commit in reversed(commit_ranges)]),
576 [commit.raw_id for commit in reversed(commit_ranges)]),
577 reviewers=reviewers_reasons,
577 reviewers=reviewers_reasons,
578 title=title,
578 title=title,
579 description=Optional.extract(description)
579 description=Optional.extract(description)
580 )
580 )
581
581
582 Session().commit()
582 Session().commit()
583 data = {
583 data = {
584 'msg': 'Created new pull request `{}`'.format(title),
584 'msg': 'Created new pull request `{}`'.format(title),
585 'pull_request_id': pull_request.pull_request_id,
585 'pull_request_id': pull_request.pull_request_id,
586 }
586 }
587 return data
587 return data
588
588
589
589
590 @jsonrpc_method()
590 @jsonrpc_method()
591 def update_pull_request(
591 def update_pull_request(
592 request, apiuser, repoid, pullrequestid, title=Optional(''),
592 request, apiuser, repoid, pullrequestid, title=Optional(''),
593 description=Optional(''), reviewers=Optional(None),
593 description=Optional(''), reviewers=Optional(None),
594 update_commits=Optional(None), close_pull_request=Optional(None)):
594 update_commits=Optional(None), close_pull_request=Optional(None)):
595 """
595 """
596 Updates a pull request.
596 Updates a pull request.
597
597
598 :param apiuser: This is filled automatically from the |authtoken|.
598 :param apiuser: This is filled automatically from the |authtoken|.
599 :type apiuser: AuthUser
599 :type apiuser: AuthUser
600 :param repoid: The repository name or repository ID.
600 :param repoid: The repository name or repository ID.
601 :type repoid: str or int
601 :type repoid: str or int
602 :param pullrequestid: The pull request ID.
602 :param pullrequestid: The pull request ID.
603 :type pullrequestid: int
603 :type pullrequestid: int
604 :param title: Set the pull request title.
604 :param title: Set the pull request title.
605 :type title: str
605 :type title: str
606 :param description: Update pull request description.
606 :param description: Update pull request description.
607 :type description: Optional(str)
607 :type description: Optional(str)
608 :param reviewers: Update pull request reviewers list with new value.
608 :param reviewers: Update pull request reviewers list with new value.
609 :type reviewers: Optional(list)
609 :type reviewers: Optional(list)
610 :param update_commits: Trigger update of commits for this pull request
610 :param update_commits: Trigger update of commits for this pull request
611 :type: update_commits: Optional(bool)
611 :type: update_commits: Optional(bool)
612 :param close_pull_request: Close this pull request with rejected state
612 :param close_pull_request: Close this pull request with rejected state
613 :type: close_pull_request: Optional(bool)
613 :type: close_pull_request: Optional(bool)
614
614
615 Example output:
615 Example output:
616
616
617 .. code-block:: bash
617 .. code-block:: bash
618
618
619 id : <id_given_in_input>
619 id : <id_given_in_input>
620 result :
620 result :
621 {
621 {
622 "msg": "Updated pull request `63`",
622 "msg": "Updated pull request `63`",
623 "pull_request": <pull_request_object>,
623 "pull_request": <pull_request_object>,
624 "updated_reviewers": {
624 "updated_reviewers": {
625 "added": [
625 "added": [
626 "username"
626 "username"
627 ],
627 ],
628 "removed": []
628 "removed": []
629 },
629 },
630 "updated_commits": {
630 "updated_commits": {
631 "added": [
631 "added": [
632 "<sha1_hash>"
632 "<sha1_hash>"
633 ],
633 ],
634 "common": [
634 "common": [
635 "<sha1_hash>",
635 "<sha1_hash>",
636 "<sha1_hash>",
636 "<sha1_hash>",
637 ],
637 ],
638 "removed": []
638 "removed": []
639 }
639 }
640 }
640 }
641 error : null
641 error : null
642 """
642 """
643
643
644 repo = get_repo_or_error(repoid)
644 repo = get_repo_or_error(repoid)
645 pull_request = get_pull_request_or_error(pullrequestid)
645 pull_request = get_pull_request_or_error(pullrequestid)
646 if not PullRequestModel().check_user_update(
646 if not PullRequestModel().check_user_update(
647 pull_request, apiuser, api=True):
647 pull_request, apiuser, api=True):
648 raise JSONRPCError(
648 raise JSONRPCError(
649 'pull request `%s` update failed, no permission to update.' % (
649 'pull request `%s` update failed, no permission to update.' % (
650 pullrequestid,))
650 pullrequestid,))
651 if pull_request.is_closed():
651 if pull_request.is_closed():
652 raise JSONRPCError(
652 raise JSONRPCError(
653 'pull request `%s` update failed, pull request is closed' % (
653 'pull request `%s` update failed, pull request is closed' % (
654 pullrequestid,))
654 pullrequestid,))
655
655
656 reviewer_objects = Optional.extract(reviewers) or []
656 reviewer_objects = Optional.extract(reviewers) or []
657 if not isinstance(reviewer_objects, list):
657 if not isinstance(reviewer_objects, list):
658 raise JSONRPCError('reviewers should be specified as a list')
658 raise JSONRPCError('reviewers should be specified as a list')
659
659
660 reviewers_reasons = []
660 reviewers_reasons = []
661 reviewer_ids = set()
661 reviewer_ids = set()
662 for reviewer_object in reviewer_objects:
662 for reviewer_object in reviewer_objects:
663 reviewer_reasons = []
663 reviewer_reasons = []
664 if isinstance(reviewer_object, (int, basestring)):
664 if isinstance(reviewer_object, (int, basestring)):
665 reviewer_username = reviewer_object
665 reviewer_username = reviewer_object
666 else:
666 else:
667 reviewer_username = reviewer_object['username']
667 reviewer_username = reviewer_object['username']
668 reviewer_reasons = reviewer_object.get('reasons', [])
668 reviewer_reasons = reviewer_object.get('reasons', [])
669
669
670 user = get_user_or_error(reviewer_username)
670 user = get_user_or_error(reviewer_username)
671 reviewer_ids.add(user.user_id)
671 reviewer_ids.add(user.user_id)
672 reviewers_reasons.append((user.user_id, reviewer_reasons))
672 reviewers_reasons.append((user.user_id, reviewer_reasons))
673
673
674 title = Optional.extract(title)
674 title = Optional.extract(title)
675 description = Optional.extract(description)
675 description = Optional.extract(description)
676 if title or description:
676 if title or description:
677 PullRequestModel().edit(
677 PullRequestModel().edit(
678 pull_request, title or pull_request.title,
678 pull_request, title or pull_request.title,
679 description or pull_request.description)
679 description or pull_request.description)
680 Session().commit()
680 Session().commit()
681
681
682 commit_changes = {"added": [], "common": [], "removed": []}
682 commit_changes = {"added": [], "common": [], "removed": []}
683 if str2bool(Optional.extract(update_commits)):
683 if str2bool(Optional.extract(update_commits)):
684 if PullRequestModel().has_valid_update_type(pull_request):
684 if PullRequestModel().has_valid_update_type(pull_request):
685 update_response = PullRequestModel().update_commits(
685 update_response = PullRequestModel().update_commits(
686 pull_request)
686 pull_request)
687 commit_changes = update_response.changes or commit_changes
687 commit_changes = update_response.changes or commit_changes
688 Session().commit()
688 Session().commit()
689
689
690 reviewers_changes = {"added": [], "removed": []}
690 reviewers_changes = {"added": [], "removed": []}
691 if reviewer_ids:
691 if reviewer_ids:
692 added_reviewers, removed_reviewers = \
692 added_reviewers, removed_reviewers = \
693 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
693 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
694
694
695 reviewers_changes['added'] = sorted(
695 reviewers_changes['added'] = sorted(
696 [get_user_or_error(n).username for n in added_reviewers])
696 [get_user_or_error(n).username for n in added_reviewers])
697 reviewers_changes['removed'] = sorted(
697 reviewers_changes['removed'] = sorted(
698 [get_user_or_error(n).username for n in removed_reviewers])
698 [get_user_or_error(n).username for n in removed_reviewers])
699 Session().commit()
699 Session().commit()
700
700
701 if str2bool(Optional.extract(close_pull_request)):
701 if str2bool(Optional.extract(close_pull_request)):
702 PullRequestModel().close_pull_request_with_comment(
702 PullRequestModel().close_pull_request_with_comment(
703 pull_request, apiuser, repo)
703 pull_request, apiuser, repo)
704 Session().commit()
704 Session().commit()
705
705
706 data = {
706 data = {
707 'msg': 'Updated pull request `{}`'.format(
707 'msg': 'Updated pull request `{}`'.format(
708 pull_request.pull_request_id),
708 pull_request.pull_request_id),
709 'pull_request': pull_request.get_api_data(),
709 'pull_request': pull_request.get_api_data(),
710 'updated_commits': commit_changes,
710 'updated_commits': commit_changes,
711 'updated_reviewers': reviewers_changes
711 'updated_reviewers': reviewers_changes
712 }
712 }
713
713
714 return data
714 return data
@@ -1,1960 +1,1960 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 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
32 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
33 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
33 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
34 from rhodecode.lib.utils2 import str2bool, time_to_datetime
34 from rhodecode.lib.utils2 import str2bool, time_to_datetime
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.model.changeset_status import ChangesetStatusModel
36 from rhodecode.model.changeset_status import ChangesetStatusModel
37 from rhodecode.model.comment import ChangesetCommentsModel
37 from rhodecode.model.comment import CommentsModel
38 from rhodecode.model.db import (
38 from rhodecode.model.db import (
39 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup)
39 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup)
40 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.scm import ScmModel, RepoList
41 from rhodecode.model.scm import ScmModel, RepoList
42 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
42 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
43 from rhodecode.model import validation_schema
43 from rhodecode.model import validation_schema
44 from rhodecode.model.validation_schema.schemas import repo_schema
44 from rhodecode.model.validation_schema.schemas import repo_schema
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 @jsonrpc_method()
49 @jsonrpc_method()
50 def get_repo(request, apiuser, repoid, cache=Optional(True)):
50 def get_repo(request, apiuser, repoid, cache=Optional(True)):
51 """
51 """
52 Gets an existing repository by its name or repository_id.
52 Gets an existing repository by its name or repository_id.
53
53
54 The members section so the output returns users groups or users
54 The members section so the output returns users groups or users
55 associated with that repository.
55 associated with that repository.
56
56
57 This command can only be run using an |authtoken| with admin rights,
57 This command can only be run using an |authtoken| with admin rights,
58 or users with at least read rights to the |repo|.
58 or users with at least read rights to the |repo|.
59
59
60 :param apiuser: This is filled automatically from the |authtoken|.
60 :param apiuser: This is filled automatically from the |authtoken|.
61 :type apiuser: AuthUser
61 :type apiuser: AuthUser
62 :param repoid: The repository name or repository id.
62 :param repoid: The repository name or repository id.
63 :type repoid: str or int
63 :type repoid: str or int
64 :param cache: use the cached value for last changeset
64 :param cache: use the cached value for last changeset
65 :type: cache: Optional(bool)
65 :type: cache: Optional(bool)
66
66
67 Example output:
67 Example output:
68
68
69 .. code-block:: bash
69 .. code-block:: bash
70
70
71 {
71 {
72 "error": null,
72 "error": null,
73 "id": <repo_id>,
73 "id": <repo_id>,
74 "result": {
74 "result": {
75 "clone_uri": null,
75 "clone_uri": null,
76 "created_on": "timestamp",
76 "created_on": "timestamp",
77 "description": "repo description",
77 "description": "repo description",
78 "enable_downloads": false,
78 "enable_downloads": false,
79 "enable_locking": false,
79 "enable_locking": false,
80 "enable_statistics": false,
80 "enable_statistics": false,
81 "followers": [
81 "followers": [
82 {
82 {
83 "active": true,
83 "active": true,
84 "admin": false,
84 "admin": false,
85 "api_key": "****************************************",
85 "api_key": "****************************************",
86 "api_keys": [
86 "api_keys": [
87 "****************************************"
87 "****************************************"
88 ],
88 ],
89 "email": "user@example.com",
89 "email": "user@example.com",
90 "emails": [
90 "emails": [
91 "user@example.com"
91 "user@example.com"
92 ],
92 ],
93 "extern_name": "rhodecode",
93 "extern_name": "rhodecode",
94 "extern_type": "rhodecode",
94 "extern_type": "rhodecode",
95 "firstname": "username",
95 "firstname": "username",
96 "ip_addresses": [],
96 "ip_addresses": [],
97 "language": null,
97 "language": null,
98 "last_login": "2015-09-16T17:16:35.854",
98 "last_login": "2015-09-16T17:16:35.854",
99 "lastname": "surname",
99 "lastname": "surname",
100 "user_id": <user_id>,
100 "user_id": <user_id>,
101 "username": "name"
101 "username": "name"
102 }
102 }
103 ],
103 ],
104 "fork_of": "parent-repo",
104 "fork_of": "parent-repo",
105 "landing_rev": [
105 "landing_rev": [
106 "rev",
106 "rev",
107 "tip"
107 "tip"
108 ],
108 ],
109 "last_changeset": {
109 "last_changeset": {
110 "author": "User <user@example.com>",
110 "author": "User <user@example.com>",
111 "branch": "default",
111 "branch": "default",
112 "date": "timestamp",
112 "date": "timestamp",
113 "message": "last commit message",
113 "message": "last commit message",
114 "parents": [
114 "parents": [
115 {
115 {
116 "raw_id": "commit-id"
116 "raw_id": "commit-id"
117 }
117 }
118 ],
118 ],
119 "raw_id": "commit-id",
119 "raw_id": "commit-id",
120 "revision": <revision number>,
120 "revision": <revision number>,
121 "short_id": "short id"
121 "short_id": "short id"
122 },
122 },
123 "lock_reason": null,
123 "lock_reason": null,
124 "locked_by": null,
124 "locked_by": null,
125 "locked_date": null,
125 "locked_date": null,
126 "members": [
126 "members": [
127 {
127 {
128 "name": "super-admin-name",
128 "name": "super-admin-name",
129 "origin": "super-admin",
129 "origin": "super-admin",
130 "permission": "repository.admin",
130 "permission": "repository.admin",
131 "type": "user"
131 "type": "user"
132 },
132 },
133 {
133 {
134 "name": "owner-name",
134 "name": "owner-name",
135 "origin": "owner",
135 "origin": "owner",
136 "permission": "repository.admin",
136 "permission": "repository.admin",
137 "type": "user"
137 "type": "user"
138 },
138 },
139 {
139 {
140 "name": "user-group-name",
140 "name": "user-group-name",
141 "origin": "permission",
141 "origin": "permission",
142 "permission": "repository.write",
142 "permission": "repository.write",
143 "type": "user_group"
143 "type": "user_group"
144 }
144 }
145 ],
145 ],
146 "owner": "owner-name",
146 "owner": "owner-name",
147 "permissions": [
147 "permissions": [
148 {
148 {
149 "name": "super-admin-name",
149 "name": "super-admin-name",
150 "origin": "super-admin",
150 "origin": "super-admin",
151 "permission": "repository.admin",
151 "permission": "repository.admin",
152 "type": "user"
152 "type": "user"
153 },
153 },
154 {
154 {
155 "name": "owner-name",
155 "name": "owner-name",
156 "origin": "owner",
156 "origin": "owner",
157 "permission": "repository.admin",
157 "permission": "repository.admin",
158 "type": "user"
158 "type": "user"
159 },
159 },
160 {
160 {
161 "name": "user-group-name",
161 "name": "user-group-name",
162 "origin": "permission",
162 "origin": "permission",
163 "permission": "repository.write",
163 "permission": "repository.write",
164 "type": "user_group"
164 "type": "user_group"
165 }
165 }
166 ],
166 ],
167 "private": true,
167 "private": true,
168 "repo_id": 676,
168 "repo_id": 676,
169 "repo_name": "user-group/repo-name",
169 "repo_name": "user-group/repo-name",
170 "repo_type": "hg"
170 "repo_type": "hg"
171 }
171 }
172 }
172 }
173 """
173 """
174
174
175 repo = get_repo_or_error(repoid)
175 repo = get_repo_or_error(repoid)
176 cache = Optional.extract(cache)
176 cache = Optional.extract(cache)
177
177
178 include_secrets = False
178 include_secrets = False
179 if has_superadmin_permission(apiuser):
179 if has_superadmin_permission(apiuser):
180 include_secrets = True
180 include_secrets = True
181 else:
181 else:
182 # check if we have at least read permission for this repo !
182 # check if we have at least read permission for this repo !
183 _perms = (
183 _perms = (
184 'repository.admin', 'repository.write', 'repository.read',)
184 'repository.admin', 'repository.write', 'repository.read',)
185 validate_repo_permissions(apiuser, repoid, repo, _perms)
185 validate_repo_permissions(apiuser, repoid, repo, _perms)
186
186
187 permissions = []
187 permissions = []
188 for _user in repo.permissions():
188 for _user in repo.permissions():
189 user_data = {
189 user_data = {
190 'name': _user.username,
190 'name': _user.username,
191 'permission': _user.permission,
191 'permission': _user.permission,
192 'origin': get_origin(_user),
192 'origin': get_origin(_user),
193 'type': "user",
193 'type': "user",
194 }
194 }
195 permissions.append(user_data)
195 permissions.append(user_data)
196
196
197 for _user_group in repo.permission_user_groups():
197 for _user_group in repo.permission_user_groups():
198 user_group_data = {
198 user_group_data = {
199 'name': _user_group.users_group_name,
199 'name': _user_group.users_group_name,
200 'permission': _user_group.permission,
200 'permission': _user_group.permission,
201 'origin': get_origin(_user_group),
201 'origin': get_origin(_user_group),
202 'type': "user_group",
202 'type': "user_group",
203 }
203 }
204 permissions.append(user_group_data)
204 permissions.append(user_group_data)
205
205
206 following_users = [
206 following_users = [
207 user.user.get_api_data(include_secrets=include_secrets)
207 user.user.get_api_data(include_secrets=include_secrets)
208 for user in repo.followers]
208 for user in repo.followers]
209
209
210 if not cache:
210 if not cache:
211 repo.update_commit_cache()
211 repo.update_commit_cache()
212 data = repo.get_api_data(include_secrets=include_secrets)
212 data = repo.get_api_data(include_secrets=include_secrets)
213 data['members'] = permissions # TODO: this should be deprecated soon
213 data['members'] = permissions # TODO: this should be deprecated soon
214 data['permissions'] = permissions
214 data['permissions'] = permissions
215 data['followers'] = following_users
215 data['followers'] = following_users
216 return data
216 return data
217
217
218
218
219 @jsonrpc_method()
219 @jsonrpc_method()
220 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
220 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
221 """
221 """
222 Lists all existing repositories.
222 Lists all existing repositories.
223
223
224 This command can only be run using an |authtoken| with admin rights,
224 This command can only be run using an |authtoken| with admin rights,
225 or users with at least read rights to |repos|.
225 or users with at least read rights to |repos|.
226
226
227 :param apiuser: This is filled automatically from the |authtoken|.
227 :param apiuser: This is filled automatically from the |authtoken|.
228 :type apiuser: AuthUser
228 :type apiuser: AuthUser
229 :param root: specify root repository group to fetch repositories.
229 :param root: specify root repository group to fetch repositories.
230 filters the returned repositories to be members of given root group.
230 filters the returned repositories to be members of given root group.
231 :type root: Optional(None)
231 :type root: Optional(None)
232 :param traverse: traverse given root into subrepositories. With this flag
232 :param traverse: traverse given root into subrepositories. With this flag
233 set to False, it will only return top-level repositories from `root`.
233 set to False, it will only return top-level repositories from `root`.
234 if root is empty it will return just top-level repositories.
234 if root is empty it will return just top-level repositories.
235 :type traverse: Optional(True)
235 :type traverse: Optional(True)
236
236
237
237
238 Example output:
238 Example output:
239
239
240 .. code-block:: bash
240 .. code-block:: bash
241
241
242 id : <id_given_in_input>
242 id : <id_given_in_input>
243 result: [
243 result: [
244 {
244 {
245 "repo_id" : "<repo_id>",
245 "repo_id" : "<repo_id>",
246 "repo_name" : "<reponame>"
246 "repo_name" : "<reponame>"
247 "repo_type" : "<repo_type>",
247 "repo_type" : "<repo_type>",
248 "clone_uri" : "<clone_uri>",
248 "clone_uri" : "<clone_uri>",
249 "private": : "<bool>",
249 "private": : "<bool>",
250 "created_on" : "<datetimecreated>",
250 "created_on" : "<datetimecreated>",
251 "description" : "<description>",
251 "description" : "<description>",
252 "landing_rev": "<landing_rev>",
252 "landing_rev": "<landing_rev>",
253 "owner": "<repo_owner>",
253 "owner": "<repo_owner>",
254 "fork_of": "<name_of_fork_parent>",
254 "fork_of": "<name_of_fork_parent>",
255 "enable_downloads": "<bool>",
255 "enable_downloads": "<bool>",
256 "enable_locking": "<bool>",
256 "enable_locking": "<bool>",
257 "enable_statistics": "<bool>",
257 "enable_statistics": "<bool>",
258 },
258 },
259 ...
259 ...
260 ]
260 ]
261 error: null
261 error: null
262 """
262 """
263
263
264 include_secrets = has_superadmin_permission(apiuser)
264 include_secrets = has_superadmin_permission(apiuser)
265 _perms = ('repository.read', 'repository.write', 'repository.admin',)
265 _perms = ('repository.read', 'repository.write', 'repository.admin',)
266 extras = {'user': apiuser}
266 extras = {'user': apiuser}
267
267
268 root = Optional.extract(root)
268 root = Optional.extract(root)
269 traverse = Optional.extract(traverse, binary=True)
269 traverse = Optional.extract(traverse, binary=True)
270
270
271 if root:
271 if root:
272 # verify parent existance, if it's empty return an error
272 # verify parent existance, if it's empty return an error
273 parent = RepoGroup.get_by_group_name(root)
273 parent = RepoGroup.get_by_group_name(root)
274 if not parent:
274 if not parent:
275 raise JSONRPCError(
275 raise JSONRPCError(
276 'Root repository group `{}` does not exist'.format(root))
276 'Root repository group `{}` does not exist'.format(root))
277
277
278 if traverse:
278 if traverse:
279 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
279 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
280 else:
280 else:
281 repos = RepoModel().get_repos_for_root(root=parent)
281 repos = RepoModel().get_repos_for_root(root=parent)
282 else:
282 else:
283 if traverse:
283 if traverse:
284 repos = RepoModel().get_all()
284 repos = RepoModel().get_all()
285 else:
285 else:
286 # return just top-level
286 # return just top-level
287 repos = RepoModel().get_repos_for_root(root=None)
287 repos = RepoModel().get_repos_for_root(root=None)
288
288
289 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
289 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
290 return [repo.get_api_data(include_secrets=include_secrets)
290 return [repo.get_api_data(include_secrets=include_secrets)
291 for repo in repo_list]
291 for repo in repo_list]
292
292
293
293
294 @jsonrpc_method()
294 @jsonrpc_method()
295 def get_repo_changeset(request, apiuser, repoid, revision,
295 def get_repo_changeset(request, apiuser, repoid, revision,
296 details=Optional('basic')):
296 details=Optional('basic')):
297 """
297 """
298 Returns information about a changeset.
298 Returns information about a changeset.
299
299
300 Additionally parameters define the amount of details returned by
300 Additionally parameters define the amount of details returned by
301 this function.
301 this function.
302
302
303 This command can only be run using an |authtoken| with admin rights,
303 This command can only be run using an |authtoken| with admin rights,
304 or users with at least read rights to the |repo|.
304 or users with at least read rights to the |repo|.
305
305
306 :param apiuser: This is filled automatically from the |authtoken|.
306 :param apiuser: This is filled automatically from the |authtoken|.
307 :type apiuser: AuthUser
307 :type apiuser: AuthUser
308 :param repoid: The repository name or repository id
308 :param repoid: The repository name or repository id
309 :type repoid: str or int
309 :type repoid: str or int
310 :param revision: revision for which listing should be done
310 :param revision: revision for which listing should be done
311 :type revision: str
311 :type revision: str
312 :param details: details can be 'basic|extended|full' full gives diff
312 :param details: details can be 'basic|extended|full' full gives diff
313 info details like the diff itself, and number of changed files etc.
313 info details like the diff itself, and number of changed files etc.
314 :type details: Optional(str)
314 :type details: Optional(str)
315
315
316 """
316 """
317 repo = get_repo_or_error(repoid)
317 repo = get_repo_or_error(repoid)
318 if not has_superadmin_permission(apiuser):
318 if not has_superadmin_permission(apiuser):
319 _perms = (
319 _perms = (
320 'repository.admin', 'repository.write', 'repository.read',)
320 'repository.admin', 'repository.write', 'repository.read',)
321 validate_repo_permissions(apiuser, repoid, repo, _perms)
321 validate_repo_permissions(apiuser, repoid, repo, _perms)
322
322
323 changes_details = Optional.extract(details)
323 changes_details = Optional.extract(details)
324 _changes_details_types = ['basic', 'extended', 'full']
324 _changes_details_types = ['basic', 'extended', 'full']
325 if changes_details not in _changes_details_types:
325 if changes_details not in _changes_details_types:
326 raise JSONRPCError(
326 raise JSONRPCError(
327 'ret_type must be one of %s' % (
327 'ret_type must be one of %s' % (
328 ','.join(_changes_details_types)))
328 ','.join(_changes_details_types)))
329
329
330 pre_load = ['author', 'branch', 'date', 'message', 'parents',
330 pre_load = ['author', 'branch', 'date', 'message', 'parents',
331 'status', '_commit', '_file_paths']
331 'status', '_commit', '_file_paths']
332
332
333 try:
333 try:
334 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
334 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
335 except TypeError as e:
335 except TypeError as e:
336 raise JSONRPCError(e.message)
336 raise JSONRPCError(e.message)
337 _cs_json = cs.__json__()
337 _cs_json = cs.__json__()
338 _cs_json['diff'] = build_commit_data(cs, changes_details)
338 _cs_json['diff'] = build_commit_data(cs, changes_details)
339 if changes_details == 'full':
339 if changes_details == 'full':
340 _cs_json['refs'] = {
340 _cs_json['refs'] = {
341 'branches': [cs.branch],
341 'branches': [cs.branch],
342 'bookmarks': getattr(cs, 'bookmarks', []),
342 'bookmarks': getattr(cs, 'bookmarks', []),
343 'tags': cs.tags
343 'tags': cs.tags
344 }
344 }
345 return _cs_json
345 return _cs_json
346
346
347
347
348 @jsonrpc_method()
348 @jsonrpc_method()
349 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
349 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
350 details=Optional('basic')):
350 details=Optional('basic')):
351 """
351 """
352 Returns a set of commits limited by the number starting
352 Returns a set of commits limited by the number starting
353 from the `start_rev` option.
353 from the `start_rev` option.
354
354
355 Additional parameters define the amount of details returned by this
355 Additional parameters define the amount of details returned by this
356 function.
356 function.
357
357
358 This command can only be run using an |authtoken| with admin rights,
358 This command can only be run using an |authtoken| with admin rights,
359 or users with at least read rights to |repos|.
359 or users with at least read rights to |repos|.
360
360
361 :param apiuser: This is filled automatically from the |authtoken|.
361 :param apiuser: This is filled automatically from the |authtoken|.
362 :type apiuser: AuthUser
362 :type apiuser: AuthUser
363 :param repoid: The repository name or repository ID.
363 :param repoid: The repository name or repository ID.
364 :type repoid: str or int
364 :type repoid: str or int
365 :param start_rev: The starting revision from where to get changesets.
365 :param start_rev: The starting revision from where to get changesets.
366 :type start_rev: str
366 :type start_rev: str
367 :param limit: Limit the number of commits to this amount
367 :param limit: Limit the number of commits to this amount
368 :type limit: str or int
368 :type limit: str or int
369 :param details: Set the level of detail returned. Valid option are:
369 :param details: Set the level of detail returned. Valid option are:
370 ``basic``, ``extended`` and ``full``.
370 ``basic``, ``extended`` and ``full``.
371 :type details: Optional(str)
371 :type details: Optional(str)
372
372
373 .. note::
373 .. note::
374
374
375 Setting the parameter `details` to the value ``full`` is extensive
375 Setting the parameter `details` to the value ``full`` is extensive
376 and returns details like the diff itself, and the number
376 and returns details like the diff itself, and the number
377 of changed files.
377 of changed files.
378
378
379 """
379 """
380 repo = get_repo_or_error(repoid)
380 repo = get_repo_or_error(repoid)
381 if not has_superadmin_permission(apiuser):
381 if not has_superadmin_permission(apiuser):
382 _perms = (
382 _perms = (
383 'repository.admin', 'repository.write', 'repository.read',)
383 'repository.admin', 'repository.write', 'repository.read',)
384 validate_repo_permissions(apiuser, repoid, repo, _perms)
384 validate_repo_permissions(apiuser, repoid, repo, _perms)
385
385
386 changes_details = Optional.extract(details)
386 changes_details = Optional.extract(details)
387 _changes_details_types = ['basic', 'extended', 'full']
387 _changes_details_types = ['basic', 'extended', 'full']
388 if changes_details not in _changes_details_types:
388 if changes_details not in _changes_details_types:
389 raise JSONRPCError(
389 raise JSONRPCError(
390 'ret_type must be one of %s' % (
390 'ret_type must be one of %s' % (
391 ','.join(_changes_details_types)))
391 ','.join(_changes_details_types)))
392
392
393 limit = int(limit)
393 limit = int(limit)
394 pre_load = ['author', 'branch', 'date', 'message', 'parents',
394 pre_load = ['author', 'branch', 'date', 'message', 'parents',
395 'status', '_commit', '_file_paths']
395 'status', '_commit', '_file_paths']
396
396
397 vcs_repo = repo.scm_instance()
397 vcs_repo = repo.scm_instance()
398 # SVN needs a special case to distinguish its index and commit id
398 # SVN needs a special case to distinguish its index and commit id
399 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
399 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
400 start_rev = vcs_repo.commit_ids[0]
400 start_rev = vcs_repo.commit_ids[0]
401
401
402 try:
402 try:
403 commits = vcs_repo.get_commits(
403 commits = vcs_repo.get_commits(
404 start_id=start_rev, pre_load=pre_load)
404 start_id=start_rev, pre_load=pre_load)
405 except TypeError as e:
405 except TypeError as e:
406 raise JSONRPCError(e.message)
406 raise JSONRPCError(e.message)
407 except Exception:
407 except Exception:
408 log.exception('Fetching of commits failed')
408 log.exception('Fetching of commits failed')
409 raise JSONRPCError('Error occurred during commit fetching')
409 raise JSONRPCError('Error occurred during commit fetching')
410
410
411 ret = []
411 ret = []
412 for cnt, commit in enumerate(commits):
412 for cnt, commit in enumerate(commits):
413 if cnt >= limit != -1:
413 if cnt >= limit != -1:
414 break
414 break
415 _cs_json = commit.__json__()
415 _cs_json = commit.__json__()
416 _cs_json['diff'] = build_commit_data(commit, changes_details)
416 _cs_json['diff'] = build_commit_data(commit, changes_details)
417 if changes_details == 'full':
417 if changes_details == 'full':
418 _cs_json['refs'] = {
418 _cs_json['refs'] = {
419 'branches': [commit.branch],
419 'branches': [commit.branch],
420 'bookmarks': getattr(commit, 'bookmarks', []),
420 'bookmarks': getattr(commit, 'bookmarks', []),
421 'tags': commit.tags
421 'tags': commit.tags
422 }
422 }
423 ret.append(_cs_json)
423 ret.append(_cs_json)
424 return ret
424 return ret
425
425
426
426
427 @jsonrpc_method()
427 @jsonrpc_method()
428 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
428 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
429 ret_type=Optional('all'), details=Optional('basic'),
429 ret_type=Optional('all'), details=Optional('basic'),
430 max_file_bytes=Optional(None)):
430 max_file_bytes=Optional(None)):
431 """
431 """
432 Returns a list of nodes and children in a flat list for a given
432 Returns a list of nodes and children in a flat list for a given
433 path at given revision.
433 path at given revision.
434
434
435 It's possible to specify ret_type to show only `files` or `dirs`.
435 It's possible to specify ret_type to show only `files` or `dirs`.
436
436
437 This command can only be run using an |authtoken| with admin rights,
437 This command can only be run using an |authtoken| with admin rights,
438 or users with at least read rights to |repos|.
438 or users with at least read rights to |repos|.
439
439
440 :param apiuser: This is filled automatically from the |authtoken|.
440 :param apiuser: This is filled automatically from the |authtoken|.
441 :type apiuser: AuthUser
441 :type apiuser: AuthUser
442 :param repoid: The repository name or repository ID.
442 :param repoid: The repository name or repository ID.
443 :type repoid: str or int
443 :type repoid: str or int
444 :param revision: The revision for which listing should be done.
444 :param revision: The revision for which listing should be done.
445 :type revision: str
445 :type revision: str
446 :param root_path: The path from which to start displaying.
446 :param root_path: The path from which to start displaying.
447 :type root_path: str
447 :type root_path: str
448 :param ret_type: Set the return type. Valid options are
448 :param ret_type: Set the return type. Valid options are
449 ``all`` (default), ``files`` and ``dirs``.
449 ``all`` (default), ``files`` and ``dirs``.
450 :type ret_type: Optional(str)
450 :type ret_type: Optional(str)
451 :param details: Returns extended information about nodes, such as
451 :param details: Returns extended information about nodes, such as
452 md5, binary, and or content. The valid options are ``basic`` and
452 md5, binary, and or content. The valid options are ``basic`` and
453 ``full``.
453 ``full``.
454 :type details: Optional(str)
454 :type details: Optional(str)
455 :param max_file_bytes: Only return file content under this file size bytes
455 :param max_file_bytes: Only return file content under this file size bytes
456 :type details: Optional(int)
456 :type details: Optional(int)
457
457
458 Example output:
458 Example output:
459
459
460 .. code-block:: bash
460 .. code-block:: bash
461
461
462 id : <id_given_in_input>
462 id : <id_given_in_input>
463 result: [
463 result: [
464 {
464 {
465 "name" : "<name>"
465 "name" : "<name>"
466 "type" : "<type>",
466 "type" : "<type>",
467 "binary": "<true|false>" (only in extended mode)
467 "binary": "<true|false>" (only in extended mode)
468 "md5" : "<md5 of file content>" (only in extended mode)
468 "md5" : "<md5 of file content>" (only in extended mode)
469 },
469 },
470 ...
470 ...
471 ]
471 ]
472 error: null
472 error: null
473 """
473 """
474
474
475 repo = get_repo_or_error(repoid)
475 repo = get_repo_or_error(repoid)
476 if not has_superadmin_permission(apiuser):
476 if not has_superadmin_permission(apiuser):
477 _perms = (
477 _perms = (
478 'repository.admin', 'repository.write', 'repository.read',)
478 'repository.admin', 'repository.write', 'repository.read',)
479 validate_repo_permissions(apiuser, repoid, repo, _perms)
479 validate_repo_permissions(apiuser, repoid, repo, _perms)
480
480
481 ret_type = Optional.extract(ret_type)
481 ret_type = Optional.extract(ret_type)
482 details = Optional.extract(details)
482 details = Optional.extract(details)
483 _extended_types = ['basic', 'full']
483 _extended_types = ['basic', 'full']
484 if details not in _extended_types:
484 if details not in _extended_types:
485 raise JSONRPCError(
485 raise JSONRPCError(
486 'ret_type must be one of %s' % (','.join(_extended_types)))
486 'ret_type must be one of %s' % (','.join(_extended_types)))
487 extended_info = False
487 extended_info = False
488 content = False
488 content = False
489 if details == 'basic':
489 if details == 'basic':
490 extended_info = True
490 extended_info = True
491
491
492 if details == 'full':
492 if details == 'full':
493 extended_info = content = True
493 extended_info = content = True
494
494
495 _map = {}
495 _map = {}
496 try:
496 try:
497 # check if repo is not empty by any chance, skip quicker if it is.
497 # check if repo is not empty by any chance, skip quicker if it is.
498 _scm = repo.scm_instance()
498 _scm = repo.scm_instance()
499 if _scm.is_empty():
499 if _scm.is_empty():
500 return []
500 return []
501
501
502 _d, _f = ScmModel().get_nodes(
502 _d, _f = ScmModel().get_nodes(
503 repo, revision, root_path, flat=False,
503 repo, revision, root_path, flat=False,
504 extended_info=extended_info, content=content,
504 extended_info=extended_info, content=content,
505 max_file_bytes=max_file_bytes)
505 max_file_bytes=max_file_bytes)
506 _map = {
506 _map = {
507 'all': _d + _f,
507 'all': _d + _f,
508 'files': _f,
508 'files': _f,
509 'dirs': _d,
509 'dirs': _d,
510 }
510 }
511 return _map[ret_type]
511 return _map[ret_type]
512 except KeyError:
512 except KeyError:
513 raise JSONRPCError(
513 raise JSONRPCError(
514 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
514 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
515 except Exception:
515 except Exception:
516 log.exception("Exception occurred while trying to get repo nodes")
516 log.exception("Exception occurred while trying to get repo nodes")
517 raise JSONRPCError(
517 raise JSONRPCError(
518 'failed to get repo: `%s` nodes' % repo.repo_name
518 'failed to get repo: `%s` nodes' % repo.repo_name
519 )
519 )
520
520
521
521
522 @jsonrpc_method()
522 @jsonrpc_method()
523 def get_repo_refs(request, apiuser, repoid):
523 def get_repo_refs(request, apiuser, repoid):
524 """
524 """
525 Returns a dictionary of current references. It returns
525 Returns a dictionary of current references. It returns
526 bookmarks, branches, closed_branches, and tags for given repository
526 bookmarks, branches, closed_branches, and tags for given repository
527
527
528 It's possible to specify ret_type to show only `files` or `dirs`.
528 It's possible to specify ret_type to show only `files` or `dirs`.
529
529
530 This command can only be run using an |authtoken| with admin rights,
530 This command can only be run using an |authtoken| with admin rights,
531 or users with at least read rights to |repos|.
531 or users with at least read rights to |repos|.
532
532
533 :param apiuser: This is filled automatically from the |authtoken|.
533 :param apiuser: This is filled automatically from the |authtoken|.
534 :type apiuser: AuthUser
534 :type apiuser: AuthUser
535 :param repoid: The repository name or repository ID.
535 :param repoid: The repository name or repository ID.
536 :type repoid: str or int
536 :type repoid: str or int
537
537
538 Example output:
538 Example output:
539
539
540 .. code-block:: bash
540 .. code-block:: bash
541
541
542 id : <id_given_in_input>
542 id : <id_given_in_input>
543 "result": {
543 "result": {
544 "bookmarks": {
544 "bookmarks": {
545 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
545 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
546 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
546 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
547 },
547 },
548 "branches": {
548 "branches": {
549 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
549 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
550 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
550 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
551 },
551 },
552 "branches_closed": {},
552 "branches_closed": {},
553 "tags": {
553 "tags": {
554 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
554 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
555 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
555 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
556 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
556 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
557 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
557 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
558 }
558 }
559 }
559 }
560 error: null
560 error: null
561 """
561 """
562
562
563 repo = get_repo_or_error(repoid)
563 repo = get_repo_or_error(repoid)
564 if not has_superadmin_permission(apiuser):
564 if not has_superadmin_permission(apiuser):
565 _perms = ('repository.admin', 'repository.write', 'repository.read',)
565 _perms = ('repository.admin', 'repository.write', 'repository.read',)
566 validate_repo_permissions(apiuser, repoid, repo, _perms)
566 validate_repo_permissions(apiuser, repoid, repo, _perms)
567
567
568 try:
568 try:
569 # check if repo is not empty by any chance, skip quicker if it is.
569 # check if repo is not empty by any chance, skip quicker if it is.
570 vcs_instance = repo.scm_instance()
570 vcs_instance = repo.scm_instance()
571 refs = vcs_instance.refs()
571 refs = vcs_instance.refs()
572 return refs
572 return refs
573 except Exception:
573 except Exception:
574 log.exception("Exception occurred while trying to get repo refs")
574 log.exception("Exception occurred while trying to get repo refs")
575 raise JSONRPCError(
575 raise JSONRPCError(
576 'failed to get repo: `%s` references' % repo.repo_name
576 'failed to get repo: `%s` references' % repo.repo_name
577 )
577 )
578
578
579
579
580 @jsonrpc_method()
580 @jsonrpc_method()
581 def create_repo(
581 def create_repo(
582 request, apiuser, repo_name, repo_type,
582 request, apiuser, repo_name, repo_type,
583 owner=Optional(OAttr('apiuser')),
583 owner=Optional(OAttr('apiuser')),
584 description=Optional(''),
584 description=Optional(''),
585 private=Optional(False),
585 private=Optional(False),
586 clone_uri=Optional(None),
586 clone_uri=Optional(None),
587 landing_rev=Optional('rev:tip'),
587 landing_rev=Optional('rev:tip'),
588 enable_statistics=Optional(False),
588 enable_statistics=Optional(False),
589 enable_locking=Optional(False),
589 enable_locking=Optional(False),
590 enable_downloads=Optional(False),
590 enable_downloads=Optional(False),
591 copy_permissions=Optional(False)):
591 copy_permissions=Optional(False)):
592 """
592 """
593 Creates a repository.
593 Creates a repository.
594
594
595 * If the repository name contains "/", repository will be created inside
595 * If the repository name contains "/", repository will be created inside
596 a repository group or nested repository groups
596 a repository group or nested repository groups
597
597
598 For example "foo/bar/repo1" will create |repo| called "repo1" inside
598 For example "foo/bar/repo1" will create |repo| called "repo1" inside
599 group "foo/bar". You have to have permissions to access and write to
599 group "foo/bar". You have to have permissions to access and write to
600 the last repository group ("bar" in this example)
600 the last repository group ("bar" in this example)
601
601
602 This command can only be run using an |authtoken| with at least
602 This command can only be run using an |authtoken| with at least
603 permissions to create repositories, or write permissions to
603 permissions to create repositories, or write permissions to
604 parent repository groups.
604 parent repository groups.
605
605
606 :param apiuser: This is filled automatically from the |authtoken|.
606 :param apiuser: This is filled automatically from the |authtoken|.
607 :type apiuser: AuthUser
607 :type apiuser: AuthUser
608 :param repo_name: Set the repository name.
608 :param repo_name: Set the repository name.
609 :type repo_name: str
609 :type repo_name: str
610 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
610 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
611 :type repo_type: str
611 :type repo_type: str
612 :param owner: user_id or username
612 :param owner: user_id or username
613 :type owner: Optional(str)
613 :type owner: Optional(str)
614 :param description: Set the repository description.
614 :param description: Set the repository description.
615 :type description: Optional(str)
615 :type description: Optional(str)
616 :param private: set repository as private
616 :param private: set repository as private
617 :type private: bool
617 :type private: bool
618 :param clone_uri: set clone_uri
618 :param clone_uri: set clone_uri
619 :type clone_uri: str
619 :type clone_uri: str
620 :param landing_rev: <rev_type>:<rev>
620 :param landing_rev: <rev_type>:<rev>
621 :type landing_rev: str
621 :type landing_rev: str
622 :param enable_locking:
622 :param enable_locking:
623 :type enable_locking: bool
623 :type enable_locking: bool
624 :param enable_downloads:
624 :param enable_downloads:
625 :type enable_downloads: bool
625 :type enable_downloads: bool
626 :param enable_statistics:
626 :param enable_statistics:
627 :type enable_statistics: bool
627 :type enable_statistics: bool
628 :param copy_permissions: Copy permission from group in which the
628 :param copy_permissions: Copy permission from group in which the
629 repository is being created.
629 repository is being created.
630 :type copy_permissions: bool
630 :type copy_permissions: bool
631
631
632
632
633 Example output:
633 Example output:
634
634
635 .. code-block:: bash
635 .. code-block:: bash
636
636
637 id : <id_given_in_input>
637 id : <id_given_in_input>
638 result: {
638 result: {
639 "msg": "Created new repository `<reponame>`",
639 "msg": "Created new repository `<reponame>`",
640 "success": true,
640 "success": true,
641 "task": "<celery task id or None if done sync>"
641 "task": "<celery task id or None if done sync>"
642 }
642 }
643 error: null
643 error: null
644
644
645
645
646 Example error output:
646 Example error output:
647
647
648 .. code-block:: bash
648 .. code-block:: bash
649
649
650 id : <id_given_in_input>
650 id : <id_given_in_input>
651 result : null
651 result : null
652 error : {
652 error : {
653 'failed to create repository `<repo_name>`'
653 'failed to create repository `<repo_name>`'
654 }
654 }
655
655
656 """
656 """
657
657
658 owner = validate_set_owner_permissions(apiuser, owner)
658 owner = validate_set_owner_permissions(apiuser, owner)
659
659
660 description = Optional.extract(description)
660 description = Optional.extract(description)
661 copy_permissions = Optional.extract(copy_permissions)
661 copy_permissions = Optional.extract(copy_permissions)
662 clone_uri = Optional.extract(clone_uri)
662 clone_uri = Optional.extract(clone_uri)
663 landing_commit_ref = Optional.extract(landing_rev)
663 landing_commit_ref = Optional.extract(landing_rev)
664
664
665 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
665 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
666 if isinstance(private, Optional):
666 if isinstance(private, Optional):
667 private = defs.get('repo_private') or Optional.extract(private)
667 private = defs.get('repo_private') or Optional.extract(private)
668 if isinstance(repo_type, Optional):
668 if isinstance(repo_type, Optional):
669 repo_type = defs.get('repo_type')
669 repo_type = defs.get('repo_type')
670 if isinstance(enable_statistics, Optional):
670 if isinstance(enable_statistics, Optional):
671 enable_statistics = defs.get('repo_enable_statistics')
671 enable_statistics = defs.get('repo_enable_statistics')
672 if isinstance(enable_locking, Optional):
672 if isinstance(enable_locking, Optional):
673 enable_locking = defs.get('repo_enable_locking')
673 enable_locking = defs.get('repo_enable_locking')
674 if isinstance(enable_downloads, Optional):
674 if isinstance(enable_downloads, Optional):
675 enable_downloads = defs.get('repo_enable_downloads')
675 enable_downloads = defs.get('repo_enable_downloads')
676
676
677 schema = repo_schema.RepoSchema().bind(
677 schema = repo_schema.RepoSchema().bind(
678 repo_type_options=rhodecode.BACKENDS.keys(),
678 repo_type_options=rhodecode.BACKENDS.keys(),
679 # user caller
679 # user caller
680 user=apiuser)
680 user=apiuser)
681
681
682 try:
682 try:
683 schema_data = schema.deserialize(dict(
683 schema_data = schema.deserialize(dict(
684 repo_name=repo_name,
684 repo_name=repo_name,
685 repo_type=repo_type,
685 repo_type=repo_type,
686 repo_owner=owner.username,
686 repo_owner=owner.username,
687 repo_description=description,
687 repo_description=description,
688 repo_landing_commit_ref=landing_commit_ref,
688 repo_landing_commit_ref=landing_commit_ref,
689 repo_clone_uri=clone_uri,
689 repo_clone_uri=clone_uri,
690 repo_private=private,
690 repo_private=private,
691 repo_copy_permissions=copy_permissions,
691 repo_copy_permissions=copy_permissions,
692 repo_enable_statistics=enable_statistics,
692 repo_enable_statistics=enable_statistics,
693 repo_enable_downloads=enable_downloads,
693 repo_enable_downloads=enable_downloads,
694 repo_enable_locking=enable_locking))
694 repo_enable_locking=enable_locking))
695 except validation_schema.Invalid as err:
695 except validation_schema.Invalid as err:
696 raise JSONRPCValidationError(colander_exc=err)
696 raise JSONRPCValidationError(colander_exc=err)
697
697
698 try:
698 try:
699 data = {
699 data = {
700 'owner': owner,
700 'owner': owner,
701 'repo_name': schema_data['repo_group']['repo_name_without_group'],
701 'repo_name': schema_data['repo_group']['repo_name_without_group'],
702 'repo_name_full': schema_data['repo_name'],
702 'repo_name_full': schema_data['repo_name'],
703 'repo_group': schema_data['repo_group']['repo_group_id'],
703 'repo_group': schema_data['repo_group']['repo_group_id'],
704 'repo_type': schema_data['repo_type'],
704 'repo_type': schema_data['repo_type'],
705 'repo_description': schema_data['repo_description'],
705 'repo_description': schema_data['repo_description'],
706 'repo_private': schema_data['repo_private'],
706 'repo_private': schema_data['repo_private'],
707 'clone_uri': schema_data['repo_clone_uri'],
707 'clone_uri': schema_data['repo_clone_uri'],
708 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
708 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
709 'enable_statistics': schema_data['repo_enable_statistics'],
709 'enable_statistics': schema_data['repo_enable_statistics'],
710 'enable_locking': schema_data['repo_enable_locking'],
710 'enable_locking': schema_data['repo_enable_locking'],
711 'enable_downloads': schema_data['repo_enable_downloads'],
711 'enable_downloads': schema_data['repo_enable_downloads'],
712 'repo_copy_permissions': schema_data['repo_copy_permissions'],
712 'repo_copy_permissions': schema_data['repo_copy_permissions'],
713 }
713 }
714
714
715 task = RepoModel().create(form_data=data, cur_user=owner)
715 task = RepoModel().create(form_data=data, cur_user=owner)
716 from celery.result import BaseAsyncResult
716 from celery.result import BaseAsyncResult
717 task_id = None
717 task_id = None
718 if isinstance(task, BaseAsyncResult):
718 if isinstance(task, BaseAsyncResult):
719 task_id = task.task_id
719 task_id = task.task_id
720 # no commit, it's done in RepoModel, or async via celery
720 # no commit, it's done in RepoModel, or async via celery
721 return {
721 return {
722 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
722 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
723 'success': True, # cannot return the repo data here since fork
723 'success': True, # cannot return the repo data here since fork
724 # can be done async
724 # can be done async
725 'task': task_id
725 'task': task_id
726 }
726 }
727 except Exception:
727 except Exception:
728 log.exception(
728 log.exception(
729 u"Exception while trying to create the repository %s",
729 u"Exception while trying to create the repository %s",
730 schema_data['repo_name'])
730 schema_data['repo_name'])
731 raise JSONRPCError(
731 raise JSONRPCError(
732 'failed to create repository `%s`' % (schema_data['repo_name'],))
732 'failed to create repository `%s`' % (schema_data['repo_name'],))
733
733
734
734
735 @jsonrpc_method()
735 @jsonrpc_method()
736 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
736 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
737 description=Optional('')):
737 description=Optional('')):
738 """
738 """
739 Adds an extra field to a repository.
739 Adds an extra field to a repository.
740
740
741 This command can only be run using an |authtoken| with at least
741 This command can only be run using an |authtoken| with at least
742 write permissions to the |repo|.
742 write permissions to the |repo|.
743
743
744 :param apiuser: This is filled automatically from the |authtoken|.
744 :param apiuser: This is filled automatically from the |authtoken|.
745 :type apiuser: AuthUser
745 :type apiuser: AuthUser
746 :param repoid: Set the repository name or repository id.
746 :param repoid: Set the repository name or repository id.
747 :type repoid: str or int
747 :type repoid: str or int
748 :param key: Create a unique field key for this repository.
748 :param key: Create a unique field key for this repository.
749 :type key: str
749 :type key: str
750 :param label:
750 :param label:
751 :type label: Optional(str)
751 :type label: Optional(str)
752 :param description:
752 :param description:
753 :type description: Optional(str)
753 :type description: Optional(str)
754 """
754 """
755 repo = get_repo_or_error(repoid)
755 repo = get_repo_or_error(repoid)
756 if not has_superadmin_permission(apiuser):
756 if not has_superadmin_permission(apiuser):
757 _perms = ('repository.admin',)
757 _perms = ('repository.admin',)
758 validate_repo_permissions(apiuser, repoid, repo, _perms)
758 validate_repo_permissions(apiuser, repoid, repo, _perms)
759
759
760 label = Optional.extract(label) or key
760 label = Optional.extract(label) or key
761 description = Optional.extract(description)
761 description = Optional.extract(description)
762
762
763 field = RepositoryField.get_by_key_name(key, repo)
763 field = RepositoryField.get_by_key_name(key, repo)
764 if field:
764 if field:
765 raise JSONRPCError('Field with key '
765 raise JSONRPCError('Field with key '
766 '`%s` exists for repo `%s`' % (key, repoid))
766 '`%s` exists for repo `%s`' % (key, repoid))
767
767
768 try:
768 try:
769 RepoModel().add_repo_field(repo, key, field_label=label,
769 RepoModel().add_repo_field(repo, key, field_label=label,
770 field_desc=description)
770 field_desc=description)
771 Session().commit()
771 Session().commit()
772 return {
772 return {
773 'msg': "Added new repository field `%s`" % (key,),
773 'msg': "Added new repository field `%s`" % (key,),
774 'success': True,
774 'success': True,
775 }
775 }
776 except Exception:
776 except Exception:
777 log.exception("Exception occurred while trying to add field to repo")
777 log.exception("Exception occurred while trying to add field to repo")
778 raise JSONRPCError(
778 raise JSONRPCError(
779 'failed to create new field for repository `%s`' % (repoid,))
779 'failed to create new field for repository `%s`' % (repoid,))
780
780
781
781
782 @jsonrpc_method()
782 @jsonrpc_method()
783 def remove_field_from_repo(request, apiuser, repoid, key):
783 def remove_field_from_repo(request, apiuser, repoid, key):
784 """
784 """
785 Removes an extra field from a repository.
785 Removes an extra field from a repository.
786
786
787 This command can only be run using an |authtoken| with at least
787 This command can only be run using an |authtoken| with at least
788 write permissions to the |repo|.
788 write permissions to the |repo|.
789
789
790 :param apiuser: This is filled automatically from the |authtoken|.
790 :param apiuser: This is filled automatically from the |authtoken|.
791 :type apiuser: AuthUser
791 :type apiuser: AuthUser
792 :param repoid: Set the repository name or repository ID.
792 :param repoid: Set the repository name or repository ID.
793 :type repoid: str or int
793 :type repoid: str or int
794 :param key: Set the unique field key for this repository.
794 :param key: Set the unique field key for this repository.
795 :type key: str
795 :type key: str
796 """
796 """
797
797
798 repo = get_repo_or_error(repoid)
798 repo = get_repo_or_error(repoid)
799 if not has_superadmin_permission(apiuser):
799 if not has_superadmin_permission(apiuser):
800 _perms = ('repository.admin',)
800 _perms = ('repository.admin',)
801 validate_repo_permissions(apiuser, repoid, repo, _perms)
801 validate_repo_permissions(apiuser, repoid, repo, _perms)
802
802
803 field = RepositoryField.get_by_key_name(key, repo)
803 field = RepositoryField.get_by_key_name(key, repo)
804 if not field:
804 if not field:
805 raise JSONRPCError('Field with key `%s` does not '
805 raise JSONRPCError('Field with key `%s` does not '
806 'exists for repo `%s`' % (key, repoid))
806 'exists for repo `%s`' % (key, repoid))
807
807
808 try:
808 try:
809 RepoModel().delete_repo_field(repo, field_key=key)
809 RepoModel().delete_repo_field(repo, field_key=key)
810 Session().commit()
810 Session().commit()
811 return {
811 return {
812 'msg': "Deleted repository field `%s`" % (key,),
812 'msg': "Deleted repository field `%s`" % (key,),
813 'success': True,
813 'success': True,
814 }
814 }
815 except Exception:
815 except Exception:
816 log.exception(
816 log.exception(
817 "Exception occurred while trying to delete field from repo")
817 "Exception occurred while trying to delete field from repo")
818 raise JSONRPCError(
818 raise JSONRPCError(
819 'failed to delete field for repository `%s`' % (repoid,))
819 'failed to delete field for repository `%s`' % (repoid,))
820
820
821
821
822 @jsonrpc_method()
822 @jsonrpc_method()
823 def update_repo(
823 def update_repo(
824 request, apiuser, repoid, repo_name=Optional(None),
824 request, apiuser, repoid, repo_name=Optional(None),
825 owner=Optional(OAttr('apiuser')), description=Optional(''),
825 owner=Optional(OAttr('apiuser')), description=Optional(''),
826 private=Optional(False), clone_uri=Optional(None),
826 private=Optional(False), clone_uri=Optional(None),
827 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
827 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
828 enable_statistics=Optional(False),
828 enable_statistics=Optional(False),
829 enable_locking=Optional(False),
829 enable_locking=Optional(False),
830 enable_downloads=Optional(False), fields=Optional('')):
830 enable_downloads=Optional(False), fields=Optional('')):
831 """
831 """
832 Updates a repository with the given information.
832 Updates a repository with the given information.
833
833
834 This command can only be run using an |authtoken| with at least
834 This command can only be run using an |authtoken| with at least
835 admin permissions to the |repo|.
835 admin permissions to the |repo|.
836
836
837 * If the repository name contains "/", repository will be updated
837 * If the repository name contains "/", repository will be updated
838 accordingly with a repository group or nested repository groups
838 accordingly with a repository group or nested repository groups
839
839
840 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
840 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
841 called "repo-test" and place it inside group "foo/bar".
841 called "repo-test" and place it inside group "foo/bar".
842 You have to have permissions to access and write to the last repository
842 You have to have permissions to access and write to the last repository
843 group ("bar" in this example)
843 group ("bar" in this example)
844
844
845 :param apiuser: This is filled automatically from the |authtoken|.
845 :param apiuser: This is filled automatically from the |authtoken|.
846 :type apiuser: AuthUser
846 :type apiuser: AuthUser
847 :param repoid: repository name or repository ID.
847 :param repoid: repository name or repository ID.
848 :type repoid: str or int
848 :type repoid: str or int
849 :param repo_name: Update the |repo| name, including the
849 :param repo_name: Update the |repo| name, including the
850 repository group it's in.
850 repository group it's in.
851 :type repo_name: str
851 :type repo_name: str
852 :param owner: Set the |repo| owner.
852 :param owner: Set the |repo| owner.
853 :type owner: str
853 :type owner: str
854 :param fork_of: Set the |repo| as fork of another |repo|.
854 :param fork_of: Set the |repo| as fork of another |repo|.
855 :type fork_of: str
855 :type fork_of: str
856 :param description: Update the |repo| description.
856 :param description: Update the |repo| description.
857 :type description: str
857 :type description: str
858 :param private: Set the |repo| as private. (True | False)
858 :param private: Set the |repo| as private. (True | False)
859 :type private: bool
859 :type private: bool
860 :param clone_uri: Update the |repo| clone URI.
860 :param clone_uri: Update the |repo| clone URI.
861 :type clone_uri: str
861 :type clone_uri: str
862 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
862 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
863 :type landing_rev: str
863 :type landing_rev: str
864 :param enable_statistics: Enable statistics on the |repo|, (True | False).
864 :param enable_statistics: Enable statistics on the |repo|, (True | False).
865 :type enable_statistics: bool
865 :type enable_statistics: bool
866 :param enable_locking: Enable |repo| locking.
866 :param enable_locking: Enable |repo| locking.
867 :type enable_locking: bool
867 :type enable_locking: bool
868 :param enable_downloads: Enable downloads from the |repo|, (True | False).
868 :param enable_downloads: Enable downloads from the |repo|, (True | False).
869 :type enable_downloads: bool
869 :type enable_downloads: bool
870 :param fields: Add extra fields to the |repo|. Use the following
870 :param fields: Add extra fields to the |repo|. Use the following
871 example format: ``field_key=field_val,field_key2=fieldval2``.
871 example format: ``field_key=field_val,field_key2=fieldval2``.
872 Escape ', ' with \,
872 Escape ', ' with \,
873 :type fields: str
873 :type fields: str
874 """
874 """
875
875
876 repo = get_repo_or_error(repoid)
876 repo = get_repo_or_error(repoid)
877
877
878 include_secrets = False
878 include_secrets = False
879 if not has_superadmin_permission(apiuser):
879 if not has_superadmin_permission(apiuser):
880 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
880 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
881 else:
881 else:
882 include_secrets = True
882 include_secrets = True
883
883
884 updates = dict(
884 updates = dict(
885 repo_name=repo_name
885 repo_name=repo_name
886 if not isinstance(repo_name, Optional) else repo.repo_name,
886 if not isinstance(repo_name, Optional) else repo.repo_name,
887
887
888 fork_id=fork_of
888 fork_id=fork_of
889 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
889 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
890
890
891 user=owner
891 user=owner
892 if not isinstance(owner, Optional) else repo.user.username,
892 if not isinstance(owner, Optional) else repo.user.username,
893
893
894 repo_description=description
894 repo_description=description
895 if not isinstance(description, Optional) else repo.description,
895 if not isinstance(description, Optional) else repo.description,
896
896
897 repo_private=private
897 repo_private=private
898 if not isinstance(private, Optional) else repo.private,
898 if not isinstance(private, Optional) else repo.private,
899
899
900 clone_uri=clone_uri
900 clone_uri=clone_uri
901 if not isinstance(clone_uri, Optional) else repo.clone_uri,
901 if not isinstance(clone_uri, Optional) else repo.clone_uri,
902
902
903 repo_landing_rev=landing_rev
903 repo_landing_rev=landing_rev
904 if not isinstance(landing_rev, Optional) else repo._landing_revision,
904 if not isinstance(landing_rev, Optional) else repo._landing_revision,
905
905
906 repo_enable_statistics=enable_statistics
906 repo_enable_statistics=enable_statistics
907 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
907 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
908
908
909 repo_enable_locking=enable_locking
909 repo_enable_locking=enable_locking
910 if not isinstance(enable_locking, Optional) else repo.enable_locking,
910 if not isinstance(enable_locking, Optional) else repo.enable_locking,
911
911
912 repo_enable_downloads=enable_downloads
912 repo_enable_downloads=enable_downloads
913 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
913 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
914
914
915 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
915 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
916
916
917 schema = repo_schema.RepoSchema().bind(
917 schema = repo_schema.RepoSchema().bind(
918 repo_type_options=rhodecode.BACKENDS.keys(),
918 repo_type_options=rhodecode.BACKENDS.keys(),
919 repo_ref_options=ref_choices,
919 repo_ref_options=ref_choices,
920 # user caller
920 # user caller
921 user=apiuser,
921 user=apiuser,
922 old_values=repo.get_api_data())
922 old_values=repo.get_api_data())
923 try:
923 try:
924 schema_data = schema.deserialize(dict(
924 schema_data = schema.deserialize(dict(
925 # we save old value, users cannot change type
925 # we save old value, users cannot change type
926 repo_type=repo.repo_type,
926 repo_type=repo.repo_type,
927
927
928 repo_name=updates['repo_name'],
928 repo_name=updates['repo_name'],
929 repo_owner=updates['user'],
929 repo_owner=updates['user'],
930 repo_description=updates['repo_description'],
930 repo_description=updates['repo_description'],
931 repo_clone_uri=updates['clone_uri'],
931 repo_clone_uri=updates['clone_uri'],
932 repo_fork_of=updates['fork_id'],
932 repo_fork_of=updates['fork_id'],
933 repo_private=updates['repo_private'],
933 repo_private=updates['repo_private'],
934 repo_landing_commit_ref=updates['repo_landing_rev'],
934 repo_landing_commit_ref=updates['repo_landing_rev'],
935 repo_enable_statistics=updates['repo_enable_statistics'],
935 repo_enable_statistics=updates['repo_enable_statistics'],
936 repo_enable_downloads=updates['repo_enable_downloads'],
936 repo_enable_downloads=updates['repo_enable_downloads'],
937 repo_enable_locking=updates['repo_enable_locking']))
937 repo_enable_locking=updates['repo_enable_locking']))
938 except validation_schema.Invalid as err:
938 except validation_schema.Invalid as err:
939 raise JSONRPCValidationError(colander_exc=err)
939 raise JSONRPCValidationError(colander_exc=err)
940
940
941 # save validated data back into the updates dict
941 # save validated data back into the updates dict
942 validated_updates = dict(
942 validated_updates = dict(
943 repo_name=schema_data['repo_group']['repo_name_without_group'],
943 repo_name=schema_data['repo_group']['repo_name_without_group'],
944 repo_group=schema_data['repo_group']['repo_group_id'],
944 repo_group=schema_data['repo_group']['repo_group_id'],
945
945
946 user=schema_data['repo_owner'],
946 user=schema_data['repo_owner'],
947 repo_description=schema_data['repo_description'],
947 repo_description=schema_data['repo_description'],
948 repo_private=schema_data['repo_private'],
948 repo_private=schema_data['repo_private'],
949 clone_uri=schema_data['repo_clone_uri'],
949 clone_uri=schema_data['repo_clone_uri'],
950 repo_landing_rev=schema_data['repo_landing_commit_ref'],
950 repo_landing_rev=schema_data['repo_landing_commit_ref'],
951 repo_enable_statistics=schema_data['repo_enable_statistics'],
951 repo_enable_statistics=schema_data['repo_enable_statistics'],
952 repo_enable_locking=schema_data['repo_enable_locking'],
952 repo_enable_locking=schema_data['repo_enable_locking'],
953 repo_enable_downloads=schema_data['repo_enable_downloads'],
953 repo_enable_downloads=schema_data['repo_enable_downloads'],
954 )
954 )
955
955
956 if schema_data['repo_fork_of']:
956 if schema_data['repo_fork_of']:
957 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
957 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
958 validated_updates['fork_id'] = fork_repo.repo_id
958 validated_updates['fork_id'] = fork_repo.repo_id
959
959
960 # extra fields
960 # extra fields
961 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
961 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
962 if fields:
962 if fields:
963 validated_updates.update(fields)
963 validated_updates.update(fields)
964
964
965 try:
965 try:
966 RepoModel().update(repo, **validated_updates)
966 RepoModel().update(repo, **validated_updates)
967 Session().commit()
967 Session().commit()
968 return {
968 return {
969 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
969 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
970 'repository': repo.get_api_data(include_secrets=include_secrets)
970 'repository': repo.get_api_data(include_secrets=include_secrets)
971 }
971 }
972 except Exception:
972 except Exception:
973 log.exception(
973 log.exception(
974 u"Exception while trying to update the repository %s",
974 u"Exception while trying to update the repository %s",
975 repoid)
975 repoid)
976 raise JSONRPCError('failed to update repo `%s`' % repoid)
976 raise JSONRPCError('failed to update repo `%s`' % repoid)
977
977
978
978
979 @jsonrpc_method()
979 @jsonrpc_method()
980 def fork_repo(request, apiuser, repoid, fork_name,
980 def fork_repo(request, apiuser, repoid, fork_name,
981 owner=Optional(OAttr('apiuser')),
981 owner=Optional(OAttr('apiuser')),
982 description=Optional(''),
982 description=Optional(''),
983 private=Optional(False),
983 private=Optional(False),
984 clone_uri=Optional(None),
984 clone_uri=Optional(None),
985 landing_rev=Optional('rev:tip'),
985 landing_rev=Optional('rev:tip'),
986 copy_permissions=Optional(False)):
986 copy_permissions=Optional(False)):
987 """
987 """
988 Creates a fork of the specified |repo|.
988 Creates a fork of the specified |repo|.
989
989
990 * If the fork_name contains "/", fork will be created inside
990 * If the fork_name contains "/", fork will be created inside
991 a repository group or nested repository groups
991 a repository group or nested repository groups
992
992
993 For example "foo/bar/fork-repo" will create fork called "fork-repo"
993 For example "foo/bar/fork-repo" will create fork called "fork-repo"
994 inside group "foo/bar". You have to have permissions to access and
994 inside group "foo/bar". You have to have permissions to access and
995 write to the last repository group ("bar" in this example)
995 write to the last repository group ("bar" in this example)
996
996
997 This command can only be run using an |authtoken| with minimum
997 This command can only be run using an |authtoken| with minimum
998 read permissions of the forked repo, create fork permissions for an user.
998 read permissions of the forked repo, create fork permissions for an user.
999
999
1000 :param apiuser: This is filled automatically from the |authtoken|.
1000 :param apiuser: This is filled automatically from the |authtoken|.
1001 :type apiuser: AuthUser
1001 :type apiuser: AuthUser
1002 :param repoid: Set repository name or repository ID.
1002 :param repoid: Set repository name or repository ID.
1003 :type repoid: str or int
1003 :type repoid: str or int
1004 :param fork_name: Set the fork name, including it's repository group membership.
1004 :param fork_name: Set the fork name, including it's repository group membership.
1005 :type fork_name: str
1005 :type fork_name: str
1006 :param owner: Set the fork owner.
1006 :param owner: Set the fork owner.
1007 :type owner: str
1007 :type owner: str
1008 :param description: Set the fork description.
1008 :param description: Set the fork description.
1009 :type description: str
1009 :type description: str
1010 :param copy_permissions: Copy permissions from parent |repo|. The
1010 :param copy_permissions: Copy permissions from parent |repo|. The
1011 default is False.
1011 default is False.
1012 :type copy_permissions: bool
1012 :type copy_permissions: bool
1013 :param private: Make the fork private. The default is False.
1013 :param private: Make the fork private. The default is False.
1014 :type private: bool
1014 :type private: bool
1015 :param landing_rev: Set the landing revision. The default is tip.
1015 :param landing_rev: Set the landing revision. The default is tip.
1016
1016
1017 Example output:
1017 Example output:
1018
1018
1019 .. code-block:: bash
1019 .. code-block:: bash
1020
1020
1021 id : <id_for_response>
1021 id : <id_for_response>
1022 api_key : "<api_key>"
1022 api_key : "<api_key>"
1023 args: {
1023 args: {
1024 "repoid" : "<reponame or repo_id>",
1024 "repoid" : "<reponame or repo_id>",
1025 "fork_name": "<forkname>",
1025 "fork_name": "<forkname>",
1026 "owner": "<username or user_id = Optional(=apiuser)>",
1026 "owner": "<username or user_id = Optional(=apiuser)>",
1027 "description": "<description>",
1027 "description": "<description>",
1028 "copy_permissions": "<bool>",
1028 "copy_permissions": "<bool>",
1029 "private": "<bool>",
1029 "private": "<bool>",
1030 "landing_rev": "<landing_rev>"
1030 "landing_rev": "<landing_rev>"
1031 }
1031 }
1032
1032
1033 Example error output:
1033 Example error output:
1034
1034
1035 .. code-block:: bash
1035 .. code-block:: bash
1036
1036
1037 id : <id_given_in_input>
1037 id : <id_given_in_input>
1038 result: {
1038 result: {
1039 "msg": "Created fork of `<reponame>` as `<forkname>`",
1039 "msg": "Created fork of `<reponame>` as `<forkname>`",
1040 "success": true,
1040 "success": true,
1041 "task": "<celery task id or None if done sync>"
1041 "task": "<celery task id or None if done sync>"
1042 }
1042 }
1043 error: null
1043 error: null
1044
1044
1045 """
1045 """
1046
1046
1047 repo = get_repo_or_error(repoid)
1047 repo = get_repo_or_error(repoid)
1048 repo_name = repo.repo_name
1048 repo_name = repo.repo_name
1049
1049
1050 if not has_superadmin_permission(apiuser):
1050 if not has_superadmin_permission(apiuser):
1051 # check if we have at least read permission for
1051 # check if we have at least read permission for
1052 # this repo that we fork !
1052 # this repo that we fork !
1053 _perms = (
1053 _perms = (
1054 'repository.admin', 'repository.write', 'repository.read')
1054 'repository.admin', 'repository.write', 'repository.read')
1055 validate_repo_permissions(apiuser, repoid, repo, _perms)
1055 validate_repo_permissions(apiuser, repoid, repo, _perms)
1056
1056
1057 # check if the regular user has at least fork permissions as well
1057 # check if the regular user has at least fork permissions as well
1058 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1058 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1059 raise JSONRPCForbidden()
1059 raise JSONRPCForbidden()
1060
1060
1061 # check if user can set owner parameter
1061 # check if user can set owner parameter
1062 owner = validate_set_owner_permissions(apiuser, owner)
1062 owner = validate_set_owner_permissions(apiuser, owner)
1063
1063
1064 description = Optional.extract(description)
1064 description = Optional.extract(description)
1065 copy_permissions = Optional.extract(copy_permissions)
1065 copy_permissions = Optional.extract(copy_permissions)
1066 clone_uri = Optional.extract(clone_uri)
1066 clone_uri = Optional.extract(clone_uri)
1067 landing_commit_ref = Optional.extract(landing_rev)
1067 landing_commit_ref = Optional.extract(landing_rev)
1068 private = Optional.extract(private)
1068 private = Optional.extract(private)
1069
1069
1070 schema = repo_schema.RepoSchema().bind(
1070 schema = repo_schema.RepoSchema().bind(
1071 repo_type_options=rhodecode.BACKENDS.keys(),
1071 repo_type_options=rhodecode.BACKENDS.keys(),
1072 # user caller
1072 # user caller
1073 user=apiuser)
1073 user=apiuser)
1074
1074
1075 try:
1075 try:
1076 schema_data = schema.deserialize(dict(
1076 schema_data = schema.deserialize(dict(
1077 repo_name=fork_name,
1077 repo_name=fork_name,
1078 repo_type=repo.repo_type,
1078 repo_type=repo.repo_type,
1079 repo_owner=owner.username,
1079 repo_owner=owner.username,
1080 repo_description=description,
1080 repo_description=description,
1081 repo_landing_commit_ref=landing_commit_ref,
1081 repo_landing_commit_ref=landing_commit_ref,
1082 repo_clone_uri=clone_uri,
1082 repo_clone_uri=clone_uri,
1083 repo_private=private,
1083 repo_private=private,
1084 repo_copy_permissions=copy_permissions))
1084 repo_copy_permissions=copy_permissions))
1085 except validation_schema.Invalid as err:
1085 except validation_schema.Invalid as err:
1086 raise JSONRPCValidationError(colander_exc=err)
1086 raise JSONRPCValidationError(colander_exc=err)
1087
1087
1088 try:
1088 try:
1089 data = {
1089 data = {
1090 'fork_parent_id': repo.repo_id,
1090 'fork_parent_id': repo.repo_id,
1091
1091
1092 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1092 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1093 'repo_name_full': schema_data['repo_name'],
1093 'repo_name_full': schema_data['repo_name'],
1094 'repo_group': schema_data['repo_group']['repo_group_id'],
1094 'repo_group': schema_data['repo_group']['repo_group_id'],
1095 'repo_type': schema_data['repo_type'],
1095 'repo_type': schema_data['repo_type'],
1096 'description': schema_data['repo_description'],
1096 'description': schema_data['repo_description'],
1097 'private': schema_data['repo_private'],
1097 'private': schema_data['repo_private'],
1098 'copy_permissions': schema_data['repo_copy_permissions'],
1098 'copy_permissions': schema_data['repo_copy_permissions'],
1099 'landing_rev': schema_data['repo_landing_commit_ref'],
1099 'landing_rev': schema_data['repo_landing_commit_ref'],
1100 }
1100 }
1101
1101
1102 task = RepoModel().create_fork(data, cur_user=owner)
1102 task = RepoModel().create_fork(data, cur_user=owner)
1103 # no commit, it's done in RepoModel, or async via celery
1103 # no commit, it's done in RepoModel, or async via celery
1104 from celery.result import BaseAsyncResult
1104 from celery.result import BaseAsyncResult
1105 task_id = None
1105 task_id = None
1106 if isinstance(task, BaseAsyncResult):
1106 if isinstance(task, BaseAsyncResult):
1107 task_id = task.task_id
1107 task_id = task.task_id
1108 return {
1108 return {
1109 'msg': 'Created fork of `%s` as `%s`' % (
1109 'msg': 'Created fork of `%s` as `%s`' % (
1110 repo.repo_name, schema_data['repo_name']),
1110 repo.repo_name, schema_data['repo_name']),
1111 'success': True, # cannot return the repo data here since fork
1111 'success': True, # cannot return the repo data here since fork
1112 # can be done async
1112 # can be done async
1113 'task': task_id
1113 'task': task_id
1114 }
1114 }
1115 except Exception:
1115 except Exception:
1116 log.exception(
1116 log.exception(
1117 u"Exception while trying to create fork %s",
1117 u"Exception while trying to create fork %s",
1118 schema_data['repo_name'])
1118 schema_data['repo_name'])
1119 raise JSONRPCError(
1119 raise JSONRPCError(
1120 'failed to fork repository `%s` as `%s`' % (
1120 'failed to fork repository `%s` as `%s`' % (
1121 repo_name, schema_data['repo_name']))
1121 repo_name, schema_data['repo_name']))
1122
1122
1123
1123
1124 @jsonrpc_method()
1124 @jsonrpc_method()
1125 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1125 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1126 """
1126 """
1127 Deletes a repository.
1127 Deletes a repository.
1128
1128
1129 * When the `forks` parameter is set it's possible to detach or delete
1129 * When the `forks` parameter is set it's possible to detach or delete
1130 forks of deleted repository.
1130 forks of deleted repository.
1131
1131
1132 This command can only be run using an |authtoken| with admin
1132 This command can only be run using an |authtoken| with admin
1133 permissions on the |repo|.
1133 permissions on the |repo|.
1134
1134
1135 :param apiuser: This is filled automatically from the |authtoken|.
1135 :param apiuser: This is filled automatically from the |authtoken|.
1136 :type apiuser: AuthUser
1136 :type apiuser: AuthUser
1137 :param repoid: Set the repository name or repository ID.
1137 :param repoid: Set the repository name or repository ID.
1138 :type repoid: str or int
1138 :type repoid: str or int
1139 :param forks: Set to `detach` or `delete` forks from the |repo|.
1139 :param forks: Set to `detach` or `delete` forks from the |repo|.
1140 :type forks: Optional(str)
1140 :type forks: Optional(str)
1141
1141
1142 Example error output:
1142 Example error output:
1143
1143
1144 .. code-block:: bash
1144 .. code-block:: bash
1145
1145
1146 id : <id_given_in_input>
1146 id : <id_given_in_input>
1147 result: {
1147 result: {
1148 "msg": "Deleted repository `<reponame>`",
1148 "msg": "Deleted repository `<reponame>`",
1149 "success": true
1149 "success": true
1150 }
1150 }
1151 error: null
1151 error: null
1152 """
1152 """
1153
1153
1154 repo = get_repo_or_error(repoid)
1154 repo = get_repo_or_error(repoid)
1155 if not has_superadmin_permission(apiuser):
1155 if not has_superadmin_permission(apiuser):
1156 _perms = ('repository.admin',)
1156 _perms = ('repository.admin',)
1157 validate_repo_permissions(apiuser, repoid, repo, _perms)
1157 validate_repo_permissions(apiuser, repoid, repo, _perms)
1158
1158
1159 try:
1159 try:
1160 handle_forks = Optional.extract(forks)
1160 handle_forks = Optional.extract(forks)
1161 _forks_msg = ''
1161 _forks_msg = ''
1162 _forks = [f for f in repo.forks]
1162 _forks = [f for f in repo.forks]
1163 if handle_forks == 'detach':
1163 if handle_forks == 'detach':
1164 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1164 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1165 elif handle_forks == 'delete':
1165 elif handle_forks == 'delete':
1166 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1166 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1167 elif _forks:
1167 elif _forks:
1168 raise JSONRPCError(
1168 raise JSONRPCError(
1169 'Cannot delete `%s` it still contains attached forks' %
1169 'Cannot delete `%s` it still contains attached forks' %
1170 (repo.repo_name,)
1170 (repo.repo_name,)
1171 )
1171 )
1172
1172
1173 RepoModel().delete(repo, forks=forks)
1173 RepoModel().delete(repo, forks=forks)
1174 Session().commit()
1174 Session().commit()
1175 return {
1175 return {
1176 'msg': 'Deleted repository `%s`%s' % (
1176 'msg': 'Deleted repository `%s`%s' % (
1177 repo.repo_name, _forks_msg),
1177 repo.repo_name, _forks_msg),
1178 'success': True
1178 'success': True
1179 }
1179 }
1180 except Exception:
1180 except Exception:
1181 log.exception("Exception occurred while trying to delete repo")
1181 log.exception("Exception occurred while trying to delete repo")
1182 raise JSONRPCError(
1182 raise JSONRPCError(
1183 'failed to delete repository `%s`' % (repo.repo_name,)
1183 'failed to delete repository `%s`' % (repo.repo_name,)
1184 )
1184 )
1185
1185
1186
1186
1187 #TODO: marcink, change name ?
1187 #TODO: marcink, change name ?
1188 @jsonrpc_method()
1188 @jsonrpc_method()
1189 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1189 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1190 """
1190 """
1191 Invalidates the cache for the specified repository.
1191 Invalidates the cache for the specified repository.
1192
1192
1193 This command can only be run using an |authtoken| with admin rights to
1193 This command can only be run using an |authtoken| with admin rights to
1194 the specified repository.
1194 the specified repository.
1195
1195
1196 This command takes the following options:
1196 This command takes the following options:
1197
1197
1198 :param apiuser: This is filled automatically from |authtoken|.
1198 :param apiuser: This is filled automatically from |authtoken|.
1199 :type apiuser: AuthUser
1199 :type apiuser: AuthUser
1200 :param repoid: Sets the repository name or repository ID.
1200 :param repoid: Sets the repository name or repository ID.
1201 :type repoid: str or int
1201 :type repoid: str or int
1202 :param delete_keys: This deletes the invalidated keys instead of
1202 :param delete_keys: This deletes the invalidated keys instead of
1203 just flagging them.
1203 just flagging them.
1204 :type delete_keys: Optional(``True`` | ``False``)
1204 :type delete_keys: Optional(``True`` | ``False``)
1205
1205
1206 Example output:
1206 Example output:
1207
1207
1208 .. code-block:: bash
1208 .. code-block:: bash
1209
1209
1210 id : <id_given_in_input>
1210 id : <id_given_in_input>
1211 result : {
1211 result : {
1212 'msg': Cache for repository `<repository name>` was invalidated,
1212 'msg': Cache for repository `<repository name>` was invalidated,
1213 'repository': <repository name>
1213 'repository': <repository name>
1214 }
1214 }
1215 error : null
1215 error : null
1216
1216
1217 Example error output:
1217 Example error output:
1218
1218
1219 .. code-block:: bash
1219 .. code-block:: bash
1220
1220
1221 id : <id_given_in_input>
1221 id : <id_given_in_input>
1222 result : null
1222 result : null
1223 error : {
1223 error : {
1224 'Error occurred during cache invalidation action'
1224 'Error occurred during cache invalidation action'
1225 }
1225 }
1226
1226
1227 """
1227 """
1228
1228
1229 repo = get_repo_or_error(repoid)
1229 repo = get_repo_or_error(repoid)
1230 if not has_superadmin_permission(apiuser):
1230 if not has_superadmin_permission(apiuser):
1231 _perms = ('repository.admin', 'repository.write',)
1231 _perms = ('repository.admin', 'repository.write',)
1232 validate_repo_permissions(apiuser, repoid, repo, _perms)
1232 validate_repo_permissions(apiuser, repoid, repo, _perms)
1233
1233
1234 delete = Optional.extract(delete_keys)
1234 delete = Optional.extract(delete_keys)
1235 try:
1235 try:
1236 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1236 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1237 return {
1237 return {
1238 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1238 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1239 'repository': repo.repo_name
1239 'repository': repo.repo_name
1240 }
1240 }
1241 except Exception:
1241 except Exception:
1242 log.exception(
1242 log.exception(
1243 "Exception occurred while trying to invalidate repo cache")
1243 "Exception occurred while trying to invalidate repo cache")
1244 raise JSONRPCError(
1244 raise JSONRPCError(
1245 'Error occurred during cache invalidation action'
1245 'Error occurred during cache invalidation action'
1246 )
1246 )
1247
1247
1248
1248
1249 #TODO: marcink, change name ?
1249 #TODO: marcink, change name ?
1250 @jsonrpc_method()
1250 @jsonrpc_method()
1251 def lock(request, apiuser, repoid, locked=Optional(None),
1251 def lock(request, apiuser, repoid, locked=Optional(None),
1252 userid=Optional(OAttr('apiuser'))):
1252 userid=Optional(OAttr('apiuser'))):
1253 """
1253 """
1254 Sets the lock state of the specified |repo| by the given user.
1254 Sets the lock state of the specified |repo| by the given user.
1255 From more information, see :ref:`repo-locking`.
1255 From more information, see :ref:`repo-locking`.
1256
1256
1257 * If the ``userid`` option is not set, the repository is locked to the
1257 * If the ``userid`` option is not set, the repository is locked to the
1258 user who called the method.
1258 user who called the method.
1259 * If the ``locked`` parameter is not set, the current lock state of the
1259 * If the ``locked`` parameter is not set, the current lock state of the
1260 repository is displayed.
1260 repository is displayed.
1261
1261
1262 This command can only be run using an |authtoken| with admin rights to
1262 This command can only be run using an |authtoken| with admin rights to
1263 the specified repository.
1263 the specified repository.
1264
1264
1265 This command takes the following options:
1265 This command takes the following options:
1266
1266
1267 :param apiuser: This is filled automatically from the |authtoken|.
1267 :param apiuser: This is filled automatically from the |authtoken|.
1268 :type apiuser: AuthUser
1268 :type apiuser: AuthUser
1269 :param repoid: Sets the repository name or repository ID.
1269 :param repoid: Sets the repository name or repository ID.
1270 :type repoid: str or int
1270 :type repoid: str or int
1271 :param locked: Sets the lock state.
1271 :param locked: Sets the lock state.
1272 :type locked: Optional(``True`` | ``False``)
1272 :type locked: Optional(``True`` | ``False``)
1273 :param userid: Set the repository lock to this user.
1273 :param userid: Set the repository lock to this user.
1274 :type userid: Optional(str or int)
1274 :type userid: Optional(str or int)
1275
1275
1276 Example error output:
1276 Example error output:
1277
1277
1278 .. code-block:: bash
1278 .. code-block:: bash
1279
1279
1280 id : <id_given_in_input>
1280 id : <id_given_in_input>
1281 result : {
1281 result : {
1282 'repo': '<reponame>',
1282 'repo': '<reponame>',
1283 'locked': <bool: lock state>,
1283 'locked': <bool: lock state>,
1284 'locked_since': <int: lock timestamp>,
1284 'locked_since': <int: lock timestamp>,
1285 'locked_by': <username of person who made the lock>,
1285 'locked_by': <username of person who made the lock>,
1286 'lock_reason': <str: reason for locking>,
1286 'lock_reason': <str: reason for locking>,
1287 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1287 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1288 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1288 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1289 or
1289 or
1290 'msg': 'Repo `<repository name>` not locked.'
1290 'msg': 'Repo `<repository name>` not locked.'
1291 or
1291 or
1292 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1292 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1293 }
1293 }
1294 error : null
1294 error : null
1295
1295
1296 Example error output:
1296 Example error output:
1297
1297
1298 .. code-block:: bash
1298 .. code-block:: bash
1299
1299
1300 id : <id_given_in_input>
1300 id : <id_given_in_input>
1301 result : null
1301 result : null
1302 error : {
1302 error : {
1303 'Error occurred locking repository `<reponame>`'
1303 'Error occurred locking repository `<reponame>`'
1304 }
1304 }
1305 """
1305 """
1306
1306
1307 repo = get_repo_or_error(repoid)
1307 repo = get_repo_or_error(repoid)
1308 if not has_superadmin_permission(apiuser):
1308 if not has_superadmin_permission(apiuser):
1309 # check if we have at least write permission for this repo !
1309 # check if we have at least write permission for this repo !
1310 _perms = ('repository.admin', 'repository.write',)
1310 _perms = ('repository.admin', 'repository.write',)
1311 validate_repo_permissions(apiuser, repoid, repo, _perms)
1311 validate_repo_permissions(apiuser, repoid, repo, _perms)
1312
1312
1313 # make sure normal user does not pass someone else userid,
1313 # make sure normal user does not pass someone else userid,
1314 # he is not allowed to do that
1314 # he is not allowed to do that
1315 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1315 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1316 raise JSONRPCError('userid is not the same as your user')
1316 raise JSONRPCError('userid is not the same as your user')
1317
1317
1318 if isinstance(userid, Optional):
1318 if isinstance(userid, Optional):
1319 userid = apiuser.user_id
1319 userid = apiuser.user_id
1320
1320
1321 user = get_user_or_error(userid)
1321 user = get_user_or_error(userid)
1322
1322
1323 if isinstance(locked, Optional):
1323 if isinstance(locked, Optional):
1324 lockobj = repo.locked
1324 lockobj = repo.locked
1325
1325
1326 if lockobj[0] is None:
1326 if lockobj[0] is None:
1327 _d = {
1327 _d = {
1328 'repo': repo.repo_name,
1328 'repo': repo.repo_name,
1329 'locked': False,
1329 'locked': False,
1330 'locked_since': None,
1330 'locked_since': None,
1331 'locked_by': None,
1331 'locked_by': None,
1332 'lock_reason': None,
1332 'lock_reason': None,
1333 'lock_state_changed': False,
1333 'lock_state_changed': False,
1334 'msg': 'Repo `%s` not locked.' % repo.repo_name
1334 'msg': 'Repo `%s` not locked.' % repo.repo_name
1335 }
1335 }
1336 return _d
1336 return _d
1337 else:
1337 else:
1338 _user_id, _time, _reason = lockobj
1338 _user_id, _time, _reason = lockobj
1339 lock_user = get_user_or_error(userid)
1339 lock_user = get_user_or_error(userid)
1340 _d = {
1340 _d = {
1341 'repo': repo.repo_name,
1341 'repo': repo.repo_name,
1342 'locked': True,
1342 'locked': True,
1343 'locked_since': _time,
1343 'locked_since': _time,
1344 'locked_by': lock_user.username,
1344 'locked_by': lock_user.username,
1345 'lock_reason': _reason,
1345 'lock_reason': _reason,
1346 'lock_state_changed': False,
1346 'lock_state_changed': False,
1347 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1347 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1348 % (repo.repo_name, lock_user.username,
1348 % (repo.repo_name, lock_user.username,
1349 json.dumps(time_to_datetime(_time))))
1349 json.dumps(time_to_datetime(_time))))
1350 }
1350 }
1351 return _d
1351 return _d
1352
1352
1353 # force locked state through a flag
1353 # force locked state through a flag
1354 else:
1354 else:
1355 locked = str2bool(locked)
1355 locked = str2bool(locked)
1356 lock_reason = Repository.LOCK_API
1356 lock_reason = Repository.LOCK_API
1357 try:
1357 try:
1358 if locked:
1358 if locked:
1359 lock_time = time.time()
1359 lock_time = time.time()
1360 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1360 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1361 else:
1361 else:
1362 lock_time = None
1362 lock_time = None
1363 Repository.unlock(repo)
1363 Repository.unlock(repo)
1364 _d = {
1364 _d = {
1365 'repo': repo.repo_name,
1365 'repo': repo.repo_name,
1366 'locked': locked,
1366 'locked': locked,
1367 'locked_since': lock_time,
1367 'locked_since': lock_time,
1368 'locked_by': user.username,
1368 'locked_by': user.username,
1369 'lock_reason': lock_reason,
1369 'lock_reason': lock_reason,
1370 'lock_state_changed': True,
1370 'lock_state_changed': True,
1371 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1371 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1372 % (user.username, repo.repo_name, locked))
1372 % (user.username, repo.repo_name, locked))
1373 }
1373 }
1374 return _d
1374 return _d
1375 except Exception:
1375 except Exception:
1376 log.exception(
1376 log.exception(
1377 "Exception occurred while trying to lock repository")
1377 "Exception occurred while trying to lock repository")
1378 raise JSONRPCError(
1378 raise JSONRPCError(
1379 'Error occurred locking repository `%s`' % repo.repo_name
1379 'Error occurred locking repository `%s`' % repo.repo_name
1380 )
1380 )
1381
1381
1382
1382
1383 @jsonrpc_method()
1383 @jsonrpc_method()
1384 def comment_commit(
1384 def comment_commit(
1385 request, apiuser, repoid, commit_id, message,
1385 request, apiuser, repoid, commit_id, message,
1386 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1386 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1387 """
1387 """
1388 Set a commit comment, and optionally change the status of the commit.
1388 Set a commit comment, and optionally change the status of the commit.
1389
1389
1390 :param apiuser: This is filled automatically from the |authtoken|.
1390 :param apiuser: This is filled automatically from the |authtoken|.
1391 :type apiuser: AuthUser
1391 :type apiuser: AuthUser
1392 :param repoid: Set the repository name or repository ID.
1392 :param repoid: Set the repository name or repository ID.
1393 :type repoid: str or int
1393 :type repoid: str or int
1394 :param commit_id: Specify the commit_id for which to set a comment.
1394 :param commit_id: Specify the commit_id for which to set a comment.
1395 :type commit_id: str
1395 :type commit_id: str
1396 :param message: The comment text.
1396 :param message: The comment text.
1397 :type message: str
1397 :type message: str
1398 :param userid: Set the user name of the comment creator.
1398 :param userid: Set the user name of the comment creator.
1399 :type userid: Optional(str or int)
1399 :type userid: Optional(str or int)
1400 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1400 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1401 'under_review'
1401 'under_review'
1402 :type status: str
1402 :type status: str
1403
1403
1404 Example error output:
1404 Example error output:
1405
1405
1406 .. code-block:: json
1406 .. code-block:: json
1407
1407
1408 {
1408 {
1409 "id" : <id_given_in_input>,
1409 "id" : <id_given_in_input>,
1410 "result" : {
1410 "result" : {
1411 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1411 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1412 "status_change": null or <status>,
1412 "status_change": null or <status>,
1413 "success": true
1413 "success": true
1414 },
1414 },
1415 "error" : null
1415 "error" : null
1416 }
1416 }
1417
1417
1418 """
1418 """
1419 repo = get_repo_or_error(repoid)
1419 repo = get_repo_or_error(repoid)
1420 if not has_superadmin_permission(apiuser):
1420 if not has_superadmin_permission(apiuser):
1421 _perms = ('repository.read', 'repository.write', 'repository.admin')
1421 _perms = ('repository.read', 'repository.write', 'repository.admin')
1422 validate_repo_permissions(apiuser, repoid, repo, _perms)
1422 validate_repo_permissions(apiuser, repoid, repo, _perms)
1423
1423
1424 if isinstance(userid, Optional):
1424 if isinstance(userid, Optional):
1425 userid = apiuser.user_id
1425 userid = apiuser.user_id
1426
1426
1427 user = get_user_or_error(userid)
1427 user = get_user_or_error(userid)
1428 status = Optional.extract(status)
1428 status = Optional.extract(status)
1429
1429
1430 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1430 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1431 if status and status not in allowed_statuses:
1431 if status and status not in allowed_statuses:
1432 raise JSONRPCError('Bad status, must be on '
1432 raise JSONRPCError('Bad status, must be on '
1433 'of %s got %s' % (allowed_statuses, status,))
1433 'of %s got %s' % (allowed_statuses, status,))
1434
1434
1435 try:
1435 try:
1436 rc_config = SettingsModel().get_all_settings()
1436 rc_config = SettingsModel().get_all_settings()
1437 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1437 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1438 status_change_label = ChangesetStatus.get_status_lbl(status)
1438 status_change_label = ChangesetStatus.get_status_lbl(status)
1439 comm = ChangesetCommentsModel().create(
1439 comm = CommentsModel().create(
1440 message, repo, user, commit_id=commit_id,
1440 message, repo, user, commit_id=commit_id,
1441 status_change=status_change_label,
1441 status_change=status_change_label,
1442 status_change_type=status,
1442 status_change_type=status,
1443 renderer=renderer)
1443 renderer=renderer)
1444 if status:
1444 if status:
1445 # also do a status change
1445 # also do a status change
1446 try:
1446 try:
1447 ChangesetStatusModel().set_status(
1447 ChangesetStatusModel().set_status(
1448 repo, status, user, comm, revision=commit_id,
1448 repo, status, user, comm, revision=commit_id,
1449 dont_allow_on_closed_pull_request=True
1449 dont_allow_on_closed_pull_request=True
1450 )
1450 )
1451 except StatusChangeOnClosedPullRequestError:
1451 except StatusChangeOnClosedPullRequestError:
1452 log.exception(
1452 log.exception(
1453 "Exception occurred while trying to change repo commit status")
1453 "Exception occurred while trying to change repo commit status")
1454 msg = ('Changing status on a changeset associated with '
1454 msg = ('Changing status on a changeset associated with '
1455 'a closed pull request is not allowed')
1455 'a closed pull request is not allowed')
1456 raise JSONRPCError(msg)
1456 raise JSONRPCError(msg)
1457
1457
1458 Session().commit()
1458 Session().commit()
1459 return {
1459 return {
1460 'msg': (
1460 'msg': (
1461 'Commented on commit `%s` for repository `%s`' % (
1461 'Commented on commit `%s` for repository `%s`' % (
1462 comm.revision, repo.repo_name)),
1462 comm.revision, repo.repo_name)),
1463 'status_change': status,
1463 'status_change': status,
1464 'success': True,
1464 'success': True,
1465 }
1465 }
1466 except JSONRPCError:
1466 except JSONRPCError:
1467 # catch any inside errors, and re-raise them to prevent from
1467 # catch any inside errors, and re-raise them to prevent from
1468 # below global catch to silence them
1468 # below global catch to silence them
1469 raise
1469 raise
1470 except Exception:
1470 except Exception:
1471 log.exception("Exception occurred while trying to comment on commit")
1471 log.exception("Exception occurred while trying to comment on commit")
1472 raise JSONRPCError(
1472 raise JSONRPCError(
1473 'failed to set comment on repository `%s`' % (repo.repo_name,)
1473 'failed to set comment on repository `%s`' % (repo.repo_name,)
1474 )
1474 )
1475
1475
1476
1476
1477 @jsonrpc_method()
1477 @jsonrpc_method()
1478 def grant_user_permission(request, apiuser, repoid, userid, perm):
1478 def grant_user_permission(request, apiuser, repoid, userid, perm):
1479 """
1479 """
1480 Grant permissions for the specified user on the given repository,
1480 Grant permissions for the specified user on the given repository,
1481 or update existing permissions if found.
1481 or update existing permissions if found.
1482
1482
1483 This command can only be run using an |authtoken| with admin
1483 This command can only be run using an |authtoken| with admin
1484 permissions on the |repo|.
1484 permissions on the |repo|.
1485
1485
1486 :param apiuser: This is filled automatically from the |authtoken|.
1486 :param apiuser: This is filled automatically from the |authtoken|.
1487 :type apiuser: AuthUser
1487 :type apiuser: AuthUser
1488 :param repoid: Set the repository name or repository ID.
1488 :param repoid: Set the repository name or repository ID.
1489 :type repoid: str or int
1489 :type repoid: str or int
1490 :param userid: Set the user name.
1490 :param userid: Set the user name.
1491 :type userid: str
1491 :type userid: str
1492 :param perm: Set the user permissions, using the following format
1492 :param perm: Set the user permissions, using the following format
1493 ``(repository.(none|read|write|admin))``
1493 ``(repository.(none|read|write|admin))``
1494 :type perm: str
1494 :type perm: str
1495
1495
1496 Example output:
1496 Example output:
1497
1497
1498 .. code-block:: bash
1498 .. code-block:: bash
1499
1499
1500 id : <id_given_in_input>
1500 id : <id_given_in_input>
1501 result: {
1501 result: {
1502 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1502 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1503 "success": true
1503 "success": true
1504 }
1504 }
1505 error: null
1505 error: null
1506 """
1506 """
1507
1507
1508 repo = get_repo_or_error(repoid)
1508 repo = get_repo_or_error(repoid)
1509 user = get_user_or_error(userid)
1509 user = get_user_or_error(userid)
1510 perm = get_perm_or_error(perm)
1510 perm = get_perm_or_error(perm)
1511 if not has_superadmin_permission(apiuser):
1511 if not has_superadmin_permission(apiuser):
1512 _perms = ('repository.admin',)
1512 _perms = ('repository.admin',)
1513 validate_repo_permissions(apiuser, repoid, repo, _perms)
1513 validate_repo_permissions(apiuser, repoid, repo, _perms)
1514
1514
1515 try:
1515 try:
1516
1516
1517 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1517 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1518
1518
1519 Session().commit()
1519 Session().commit()
1520 return {
1520 return {
1521 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1521 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1522 perm.permission_name, user.username, repo.repo_name
1522 perm.permission_name, user.username, repo.repo_name
1523 ),
1523 ),
1524 'success': True
1524 'success': True
1525 }
1525 }
1526 except Exception:
1526 except Exception:
1527 log.exception(
1527 log.exception(
1528 "Exception occurred while trying edit permissions for repo")
1528 "Exception occurred while trying edit permissions for repo")
1529 raise JSONRPCError(
1529 raise JSONRPCError(
1530 'failed to edit permission for user: `%s` in repo: `%s`' % (
1530 'failed to edit permission for user: `%s` in repo: `%s`' % (
1531 userid, repoid
1531 userid, repoid
1532 )
1532 )
1533 )
1533 )
1534
1534
1535
1535
1536 @jsonrpc_method()
1536 @jsonrpc_method()
1537 def revoke_user_permission(request, apiuser, repoid, userid):
1537 def revoke_user_permission(request, apiuser, repoid, userid):
1538 """
1538 """
1539 Revoke permission for a user on the specified repository.
1539 Revoke permission for a user on the specified repository.
1540
1540
1541 This command can only be run using an |authtoken| with admin
1541 This command can only be run using an |authtoken| with admin
1542 permissions on the |repo|.
1542 permissions on the |repo|.
1543
1543
1544 :param apiuser: This is filled automatically from the |authtoken|.
1544 :param apiuser: This is filled automatically from the |authtoken|.
1545 :type apiuser: AuthUser
1545 :type apiuser: AuthUser
1546 :param repoid: Set the repository name or repository ID.
1546 :param repoid: Set the repository name or repository ID.
1547 :type repoid: str or int
1547 :type repoid: str or int
1548 :param userid: Set the user name of revoked user.
1548 :param userid: Set the user name of revoked user.
1549 :type userid: str or int
1549 :type userid: str or int
1550
1550
1551 Example error output:
1551 Example error output:
1552
1552
1553 .. code-block:: bash
1553 .. code-block:: bash
1554
1554
1555 id : <id_given_in_input>
1555 id : <id_given_in_input>
1556 result: {
1556 result: {
1557 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1557 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1558 "success": true
1558 "success": true
1559 }
1559 }
1560 error: null
1560 error: null
1561 """
1561 """
1562
1562
1563 repo = get_repo_or_error(repoid)
1563 repo = get_repo_or_error(repoid)
1564 user = get_user_or_error(userid)
1564 user = get_user_or_error(userid)
1565 if not has_superadmin_permission(apiuser):
1565 if not has_superadmin_permission(apiuser):
1566 _perms = ('repository.admin',)
1566 _perms = ('repository.admin',)
1567 validate_repo_permissions(apiuser, repoid, repo, _perms)
1567 validate_repo_permissions(apiuser, repoid, repo, _perms)
1568
1568
1569 try:
1569 try:
1570 RepoModel().revoke_user_permission(repo=repo, user=user)
1570 RepoModel().revoke_user_permission(repo=repo, user=user)
1571 Session().commit()
1571 Session().commit()
1572 return {
1572 return {
1573 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1573 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1574 user.username, repo.repo_name
1574 user.username, repo.repo_name
1575 ),
1575 ),
1576 'success': True
1576 'success': True
1577 }
1577 }
1578 except Exception:
1578 except Exception:
1579 log.exception(
1579 log.exception(
1580 "Exception occurred while trying revoke permissions to repo")
1580 "Exception occurred while trying revoke permissions to repo")
1581 raise JSONRPCError(
1581 raise JSONRPCError(
1582 'failed to edit permission for user: `%s` in repo: `%s`' % (
1582 'failed to edit permission for user: `%s` in repo: `%s`' % (
1583 userid, repoid
1583 userid, repoid
1584 )
1584 )
1585 )
1585 )
1586
1586
1587
1587
1588 @jsonrpc_method()
1588 @jsonrpc_method()
1589 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1589 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1590 """
1590 """
1591 Grant permission for a user group on the specified repository,
1591 Grant permission for a user group on the specified repository,
1592 or update existing permissions.
1592 or update existing permissions.
1593
1593
1594 This command can only be run using an |authtoken| with admin
1594 This command can only be run using an |authtoken| with admin
1595 permissions on the |repo|.
1595 permissions on the |repo|.
1596
1596
1597 :param apiuser: This is filled automatically from the |authtoken|.
1597 :param apiuser: This is filled automatically from the |authtoken|.
1598 :type apiuser: AuthUser
1598 :type apiuser: AuthUser
1599 :param repoid: Set the repository name or repository ID.
1599 :param repoid: Set the repository name or repository ID.
1600 :type repoid: str or int
1600 :type repoid: str or int
1601 :param usergroupid: Specify the ID of the user group.
1601 :param usergroupid: Specify the ID of the user group.
1602 :type usergroupid: str or int
1602 :type usergroupid: str or int
1603 :param perm: Set the user group permissions using the following
1603 :param perm: Set the user group permissions using the following
1604 format: (repository.(none|read|write|admin))
1604 format: (repository.(none|read|write|admin))
1605 :type perm: str
1605 :type perm: str
1606
1606
1607 Example output:
1607 Example output:
1608
1608
1609 .. code-block:: bash
1609 .. code-block:: bash
1610
1610
1611 id : <id_given_in_input>
1611 id : <id_given_in_input>
1612 result : {
1612 result : {
1613 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1613 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1614 "success": true
1614 "success": true
1615
1615
1616 }
1616 }
1617 error : null
1617 error : null
1618
1618
1619 Example error output:
1619 Example error output:
1620
1620
1621 .. code-block:: bash
1621 .. code-block:: bash
1622
1622
1623 id : <id_given_in_input>
1623 id : <id_given_in_input>
1624 result : null
1624 result : null
1625 error : {
1625 error : {
1626 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1626 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1627 }
1627 }
1628
1628
1629 """
1629 """
1630
1630
1631 repo = get_repo_or_error(repoid)
1631 repo = get_repo_or_error(repoid)
1632 perm = get_perm_or_error(perm)
1632 perm = get_perm_or_error(perm)
1633 if not has_superadmin_permission(apiuser):
1633 if not has_superadmin_permission(apiuser):
1634 _perms = ('repository.admin',)
1634 _perms = ('repository.admin',)
1635 validate_repo_permissions(apiuser, repoid, repo, _perms)
1635 validate_repo_permissions(apiuser, repoid, repo, _perms)
1636
1636
1637 user_group = get_user_group_or_error(usergroupid)
1637 user_group = get_user_group_or_error(usergroupid)
1638 if not has_superadmin_permission(apiuser):
1638 if not has_superadmin_permission(apiuser):
1639 # check if we have at least read permission for this user group !
1639 # check if we have at least read permission for this user group !
1640 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1640 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1641 if not HasUserGroupPermissionAnyApi(*_perms)(
1641 if not HasUserGroupPermissionAnyApi(*_perms)(
1642 user=apiuser, user_group_name=user_group.users_group_name):
1642 user=apiuser, user_group_name=user_group.users_group_name):
1643 raise JSONRPCError(
1643 raise JSONRPCError(
1644 'user group `%s` does not exist' % (usergroupid,))
1644 'user group `%s` does not exist' % (usergroupid,))
1645
1645
1646 try:
1646 try:
1647 RepoModel().grant_user_group_permission(
1647 RepoModel().grant_user_group_permission(
1648 repo=repo, group_name=user_group, perm=perm)
1648 repo=repo, group_name=user_group, perm=perm)
1649
1649
1650 Session().commit()
1650 Session().commit()
1651 return {
1651 return {
1652 'msg': 'Granted perm: `%s` for user group: `%s` in '
1652 'msg': 'Granted perm: `%s` for user group: `%s` in '
1653 'repo: `%s`' % (
1653 'repo: `%s`' % (
1654 perm.permission_name, user_group.users_group_name,
1654 perm.permission_name, user_group.users_group_name,
1655 repo.repo_name
1655 repo.repo_name
1656 ),
1656 ),
1657 'success': True
1657 'success': True
1658 }
1658 }
1659 except Exception:
1659 except Exception:
1660 log.exception(
1660 log.exception(
1661 "Exception occurred while trying change permission on repo")
1661 "Exception occurred while trying change permission on repo")
1662 raise JSONRPCError(
1662 raise JSONRPCError(
1663 'failed to edit permission for user group: `%s` in '
1663 'failed to edit permission for user group: `%s` in '
1664 'repo: `%s`' % (
1664 'repo: `%s`' % (
1665 usergroupid, repo.repo_name
1665 usergroupid, repo.repo_name
1666 )
1666 )
1667 )
1667 )
1668
1668
1669
1669
1670 @jsonrpc_method()
1670 @jsonrpc_method()
1671 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1671 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1672 """
1672 """
1673 Revoke the permissions of a user group on a given repository.
1673 Revoke the permissions of a user group on a given repository.
1674
1674
1675 This command can only be run using an |authtoken| with admin
1675 This command can only be run using an |authtoken| with admin
1676 permissions on the |repo|.
1676 permissions on the |repo|.
1677
1677
1678 :param apiuser: This is filled automatically from the |authtoken|.
1678 :param apiuser: This is filled automatically from the |authtoken|.
1679 :type apiuser: AuthUser
1679 :type apiuser: AuthUser
1680 :param repoid: Set the repository name or repository ID.
1680 :param repoid: Set the repository name or repository ID.
1681 :type repoid: str or int
1681 :type repoid: str or int
1682 :param usergroupid: Specify the user group ID.
1682 :param usergroupid: Specify the user group ID.
1683 :type usergroupid: str or int
1683 :type usergroupid: str or int
1684
1684
1685 Example output:
1685 Example output:
1686
1686
1687 .. code-block:: bash
1687 .. code-block:: bash
1688
1688
1689 id : <id_given_in_input>
1689 id : <id_given_in_input>
1690 result: {
1690 result: {
1691 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1691 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1692 "success": true
1692 "success": true
1693 }
1693 }
1694 error: null
1694 error: null
1695 """
1695 """
1696
1696
1697 repo = get_repo_or_error(repoid)
1697 repo = get_repo_or_error(repoid)
1698 if not has_superadmin_permission(apiuser):
1698 if not has_superadmin_permission(apiuser):
1699 _perms = ('repository.admin',)
1699 _perms = ('repository.admin',)
1700 validate_repo_permissions(apiuser, repoid, repo, _perms)
1700 validate_repo_permissions(apiuser, repoid, repo, _perms)
1701
1701
1702 user_group = get_user_group_or_error(usergroupid)
1702 user_group = get_user_group_or_error(usergroupid)
1703 if not has_superadmin_permission(apiuser):
1703 if not has_superadmin_permission(apiuser):
1704 # check if we have at least read permission for this user group !
1704 # check if we have at least read permission for this user group !
1705 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1705 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1706 if not HasUserGroupPermissionAnyApi(*_perms)(
1706 if not HasUserGroupPermissionAnyApi(*_perms)(
1707 user=apiuser, user_group_name=user_group.users_group_name):
1707 user=apiuser, user_group_name=user_group.users_group_name):
1708 raise JSONRPCError(
1708 raise JSONRPCError(
1709 'user group `%s` does not exist' % (usergroupid,))
1709 'user group `%s` does not exist' % (usergroupid,))
1710
1710
1711 try:
1711 try:
1712 RepoModel().revoke_user_group_permission(
1712 RepoModel().revoke_user_group_permission(
1713 repo=repo, group_name=user_group)
1713 repo=repo, group_name=user_group)
1714
1714
1715 Session().commit()
1715 Session().commit()
1716 return {
1716 return {
1717 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1717 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1718 user_group.users_group_name, repo.repo_name
1718 user_group.users_group_name, repo.repo_name
1719 ),
1719 ),
1720 'success': True
1720 'success': True
1721 }
1721 }
1722 except Exception:
1722 except Exception:
1723 log.exception("Exception occurred while trying revoke "
1723 log.exception("Exception occurred while trying revoke "
1724 "user group permission on repo")
1724 "user group permission on repo")
1725 raise JSONRPCError(
1725 raise JSONRPCError(
1726 'failed to edit permission for user group: `%s` in '
1726 'failed to edit permission for user group: `%s` in '
1727 'repo: `%s`' % (
1727 'repo: `%s`' % (
1728 user_group.users_group_name, repo.repo_name
1728 user_group.users_group_name, repo.repo_name
1729 )
1729 )
1730 )
1730 )
1731
1731
1732
1732
1733 @jsonrpc_method()
1733 @jsonrpc_method()
1734 def pull(request, apiuser, repoid):
1734 def pull(request, apiuser, repoid):
1735 """
1735 """
1736 Triggers a pull on the given repository from a remote location. You
1736 Triggers a pull on the given repository from a remote location. You
1737 can use this to keep remote repositories up-to-date.
1737 can use this to keep remote repositories up-to-date.
1738
1738
1739 This command can only be run using an |authtoken| with admin
1739 This command can only be run using an |authtoken| with admin
1740 rights to the specified repository. For more information,
1740 rights to the specified repository. For more information,
1741 see :ref:`config-token-ref`.
1741 see :ref:`config-token-ref`.
1742
1742
1743 This command takes the following options:
1743 This command takes the following options:
1744
1744
1745 :param apiuser: This is filled automatically from the |authtoken|.
1745 :param apiuser: This is filled automatically from the |authtoken|.
1746 :type apiuser: AuthUser
1746 :type apiuser: AuthUser
1747 :param repoid: The repository name or repository ID.
1747 :param repoid: The repository name or repository ID.
1748 :type repoid: str or int
1748 :type repoid: str or int
1749
1749
1750 Example output:
1750 Example output:
1751
1751
1752 .. code-block:: bash
1752 .. code-block:: bash
1753
1753
1754 id : <id_given_in_input>
1754 id : <id_given_in_input>
1755 result : {
1755 result : {
1756 "msg": "Pulled from `<repository name>`"
1756 "msg": "Pulled from `<repository name>`"
1757 "repository": "<repository name>"
1757 "repository": "<repository name>"
1758 }
1758 }
1759 error : null
1759 error : null
1760
1760
1761 Example error output:
1761 Example error output:
1762
1762
1763 .. code-block:: bash
1763 .. code-block:: bash
1764
1764
1765 id : <id_given_in_input>
1765 id : <id_given_in_input>
1766 result : null
1766 result : null
1767 error : {
1767 error : {
1768 "Unable to pull changes from `<reponame>`"
1768 "Unable to pull changes from `<reponame>`"
1769 }
1769 }
1770
1770
1771 """
1771 """
1772
1772
1773 repo = get_repo_or_error(repoid)
1773 repo = get_repo_or_error(repoid)
1774 if not has_superadmin_permission(apiuser):
1774 if not has_superadmin_permission(apiuser):
1775 _perms = ('repository.admin',)
1775 _perms = ('repository.admin',)
1776 validate_repo_permissions(apiuser, repoid, repo, _perms)
1776 validate_repo_permissions(apiuser, repoid, repo, _perms)
1777
1777
1778 try:
1778 try:
1779 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1779 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1780 return {
1780 return {
1781 'msg': 'Pulled from `%s`' % repo.repo_name,
1781 'msg': 'Pulled from `%s`' % repo.repo_name,
1782 'repository': repo.repo_name
1782 'repository': repo.repo_name
1783 }
1783 }
1784 except Exception:
1784 except Exception:
1785 log.exception("Exception occurred while trying to "
1785 log.exception("Exception occurred while trying to "
1786 "pull changes from remote location")
1786 "pull changes from remote location")
1787 raise JSONRPCError(
1787 raise JSONRPCError(
1788 'Unable to pull changes from `%s`' % repo.repo_name
1788 'Unable to pull changes from `%s`' % repo.repo_name
1789 )
1789 )
1790
1790
1791
1791
1792 @jsonrpc_method()
1792 @jsonrpc_method()
1793 def strip(request, apiuser, repoid, revision, branch):
1793 def strip(request, apiuser, repoid, revision, branch):
1794 """
1794 """
1795 Strips the given revision from the specified repository.
1795 Strips the given revision from the specified repository.
1796
1796
1797 * This will remove the revision and all of its decendants.
1797 * This will remove the revision and all of its decendants.
1798
1798
1799 This command can only be run using an |authtoken| with admin rights to
1799 This command can only be run using an |authtoken| with admin rights to
1800 the specified repository.
1800 the specified repository.
1801
1801
1802 This command takes the following options:
1802 This command takes the following options:
1803
1803
1804 :param apiuser: This is filled automatically from the |authtoken|.
1804 :param apiuser: This is filled automatically from the |authtoken|.
1805 :type apiuser: AuthUser
1805 :type apiuser: AuthUser
1806 :param repoid: The repository name or repository ID.
1806 :param repoid: The repository name or repository ID.
1807 :type repoid: str or int
1807 :type repoid: str or int
1808 :param revision: The revision you wish to strip.
1808 :param revision: The revision you wish to strip.
1809 :type revision: str
1809 :type revision: str
1810 :param branch: The branch from which to strip the revision.
1810 :param branch: The branch from which to strip the revision.
1811 :type branch: str
1811 :type branch: str
1812
1812
1813 Example output:
1813 Example output:
1814
1814
1815 .. code-block:: bash
1815 .. code-block:: bash
1816
1816
1817 id : <id_given_in_input>
1817 id : <id_given_in_input>
1818 result : {
1818 result : {
1819 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1819 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1820 "repository": "<repository name>"
1820 "repository": "<repository name>"
1821 }
1821 }
1822 error : null
1822 error : null
1823
1823
1824 Example error output:
1824 Example error output:
1825
1825
1826 .. code-block:: bash
1826 .. code-block:: bash
1827
1827
1828 id : <id_given_in_input>
1828 id : <id_given_in_input>
1829 result : null
1829 result : null
1830 error : {
1830 error : {
1831 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1831 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1832 }
1832 }
1833
1833
1834 """
1834 """
1835
1835
1836 repo = get_repo_or_error(repoid)
1836 repo = get_repo_or_error(repoid)
1837 if not has_superadmin_permission(apiuser):
1837 if not has_superadmin_permission(apiuser):
1838 _perms = ('repository.admin',)
1838 _perms = ('repository.admin',)
1839 validate_repo_permissions(apiuser, repoid, repo, _perms)
1839 validate_repo_permissions(apiuser, repoid, repo, _perms)
1840
1840
1841 try:
1841 try:
1842 ScmModel().strip(repo, revision, branch)
1842 ScmModel().strip(repo, revision, branch)
1843 return {
1843 return {
1844 'msg': 'Stripped commit %s from repo `%s`' % (
1844 'msg': 'Stripped commit %s from repo `%s`' % (
1845 revision, repo.repo_name),
1845 revision, repo.repo_name),
1846 'repository': repo.repo_name
1846 'repository': repo.repo_name
1847 }
1847 }
1848 except Exception:
1848 except Exception:
1849 log.exception("Exception while trying to strip")
1849 log.exception("Exception while trying to strip")
1850 raise JSONRPCError(
1850 raise JSONRPCError(
1851 'Unable to strip commit %s from repo `%s`' % (
1851 'Unable to strip commit %s from repo `%s`' % (
1852 revision, repo.repo_name)
1852 revision, repo.repo_name)
1853 )
1853 )
1854
1854
1855
1855
1856 @jsonrpc_method()
1856 @jsonrpc_method()
1857 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1857 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1858 """
1858 """
1859 Returns all settings for a repository. If key is given it only returns the
1859 Returns all settings for a repository. If key is given it only returns the
1860 setting identified by the key or null.
1860 setting identified by the key or null.
1861
1861
1862 :param apiuser: This is filled automatically from the |authtoken|.
1862 :param apiuser: This is filled automatically from the |authtoken|.
1863 :type apiuser: AuthUser
1863 :type apiuser: AuthUser
1864 :param repoid: The repository name or repository id.
1864 :param repoid: The repository name or repository id.
1865 :type repoid: str or int
1865 :type repoid: str or int
1866 :param key: Key of the setting to return.
1866 :param key: Key of the setting to return.
1867 :type: key: Optional(str)
1867 :type: key: Optional(str)
1868
1868
1869 Example output:
1869 Example output:
1870
1870
1871 .. code-block:: bash
1871 .. code-block:: bash
1872
1872
1873 {
1873 {
1874 "error": null,
1874 "error": null,
1875 "id": 237,
1875 "id": 237,
1876 "result": {
1876 "result": {
1877 "extensions_largefiles": true,
1877 "extensions_largefiles": true,
1878 "hooks_changegroup_push_logger": true,
1878 "hooks_changegroup_push_logger": true,
1879 "hooks_changegroup_repo_size": false,
1879 "hooks_changegroup_repo_size": false,
1880 "hooks_outgoing_pull_logger": true,
1880 "hooks_outgoing_pull_logger": true,
1881 "phases_publish": "True",
1881 "phases_publish": "True",
1882 "rhodecode_hg_use_rebase_for_merging": true,
1882 "rhodecode_hg_use_rebase_for_merging": true,
1883 "rhodecode_pr_merge_enabled": true,
1883 "rhodecode_pr_merge_enabled": true,
1884 "rhodecode_use_outdated_comments": true
1884 "rhodecode_use_outdated_comments": true
1885 }
1885 }
1886 }
1886 }
1887 """
1887 """
1888
1888
1889 # Restrict access to this api method to admins only.
1889 # Restrict access to this api method to admins only.
1890 if not has_superadmin_permission(apiuser):
1890 if not has_superadmin_permission(apiuser):
1891 raise JSONRPCForbidden()
1891 raise JSONRPCForbidden()
1892
1892
1893 try:
1893 try:
1894 repo = get_repo_or_error(repoid)
1894 repo = get_repo_or_error(repoid)
1895 settings_model = VcsSettingsModel(repo=repo)
1895 settings_model = VcsSettingsModel(repo=repo)
1896 settings = settings_model.get_global_settings()
1896 settings = settings_model.get_global_settings()
1897 settings.update(settings_model.get_repo_settings())
1897 settings.update(settings_model.get_repo_settings())
1898
1898
1899 # If only a single setting is requested fetch it from all settings.
1899 # If only a single setting is requested fetch it from all settings.
1900 key = Optional.extract(key)
1900 key = Optional.extract(key)
1901 if key is not None:
1901 if key is not None:
1902 settings = settings.get(key, None)
1902 settings = settings.get(key, None)
1903 except Exception:
1903 except Exception:
1904 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1904 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1905 log.exception(msg)
1905 log.exception(msg)
1906 raise JSONRPCError(msg)
1906 raise JSONRPCError(msg)
1907
1907
1908 return settings
1908 return settings
1909
1909
1910
1910
1911 @jsonrpc_method()
1911 @jsonrpc_method()
1912 def set_repo_settings(request, apiuser, repoid, settings):
1912 def set_repo_settings(request, apiuser, repoid, settings):
1913 """
1913 """
1914 Update repository settings. Returns true on success.
1914 Update repository settings. Returns true on success.
1915
1915
1916 :param apiuser: This is filled automatically from the |authtoken|.
1916 :param apiuser: This is filled automatically from the |authtoken|.
1917 :type apiuser: AuthUser
1917 :type apiuser: AuthUser
1918 :param repoid: The repository name or repository id.
1918 :param repoid: The repository name or repository id.
1919 :type repoid: str or int
1919 :type repoid: str or int
1920 :param settings: The new settings for the repository.
1920 :param settings: The new settings for the repository.
1921 :type: settings: dict
1921 :type: settings: dict
1922
1922
1923 Example output:
1923 Example output:
1924
1924
1925 .. code-block:: bash
1925 .. code-block:: bash
1926
1926
1927 {
1927 {
1928 "error": null,
1928 "error": null,
1929 "id": 237,
1929 "id": 237,
1930 "result": true
1930 "result": true
1931 }
1931 }
1932 """
1932 """
1933 # Restrict access to this api method to admins only.
1933 # Restrict access to this api method to admins only.
1934 if not has_superadmin_permission(apiuser):
1934 if not has_superadmin_permission(apiuser):
1935 raise JSONRPCForbidden()
1935 raise JSONRPCForbidden()
1936
1936
1937 if type(settings) is not dict:
1937 if type(settings) is not dict:
1938 raise JSONRPCError('Settings have to be a JSON Object.')
1938 raise JSONRPCError('Settings have to be a JSON Object.')
1939
1939
1940 try:
1940 try:
1941 settings_model = VcsSettingsModel(repo=repoid)
1941 settings_model = VcsSettingsModel(repo=repoid)
1942
1942
1943 # Merge global, repo and incoming settings.
1943 # Merge global, repo and incoming settings.
1944 new_settings = settings_model.get_global_settings()
1944 new_settings = settings_model.get_global_settings()
1945 new_settings.update(settings_model.get_repo_settings())
1945 new_settings.update(settings_model.get_repo_settings())
1946 new_settings.update(settings)
1946 new_settings.update(settings)
1947
1947
1948 # Update the settings.
1948 # Update the settings.
1949 inherit_global_settings = new_settings.get(
1949 inherit_global_settings = new_settings.get(
1950 'inherit_global_settings', False)
1950 'inherit_global_settings', False)
1951 settings_model.create_or_update_repo_settings(
1951 settings_model.create_or_update_repo_settings(
1952 new_settings, inherit_global_settings=inherit_global_settings)
1952 new_settings, inherit_global_settings=inherit_global_settings)
1953 Session().commit()
1953 Session().commit()
1954 except Exception:
1954 except Exception:
1955 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1955 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1956 log.exception(msg)
1956 log.exception(msg)
1957 raise JSONRPCError(msg)
1957 raise JSONRPCError(msg)
1958
1958
1959 # Indicate success.
1959 # Indicate success.
1960 return True
1960 return True
@@ -1,468 +1,468 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-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 my account controller for RhodeCode admin
23 my account controller for RhodeCode admin
24 """
24 """
25
25
26 import logging
26 import logging
27 import datetime
27 import datetime
28
28
29 import formencode
29 import formencode
30 from formencode import htmlfill
30 from formencode import htmlfill
31 from pyramid.threadlocal import get_current_registry
31 from pyramid.threadlocal import get_current_registry
32 from pylons import request, tmpl_context as c, url, session
32 from pylons import request, tmpl_context as c, url, session
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 sqlalchemy.orm import joinedload
35 from sqlalchemy.orm import joinedload
36 from webob.exc import HTTPBadGateway
36 from webob.exc import HTTPBadGateway
37
37
38 from rhodecode import forms
38 from rhodecode import forms
39 from rhodecode.lib import helpers as h
39 from rhodecode.lib import helpers as h
40 from rhodecode.lib import auth
40 from rhodecode.lib import auth
41 from rhodecode.lib.auth import (
41 from rhodecode.lib.auth import (
42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
43 from rhodecode.lib.base import BaseController, render
43 from rhodecode.lib.base import BaseController, render
44 from rhodecode.lib.utils import jsonify
44 from rhodecode.lib.utils import jsonify
45 from rhodecode.lib.utils2 import safe_int, md5, str2bool
45 from rhodecode.lib.utils2 import safe_int, md5, str2bool
46 from rhodecode.lib.ext_json import json
46 from rhodecode.lib.ext_json import json
47 from rhodecode.lib.channelstream import channelstream_request, \
47 from rhodecode.lib.channelstream import channelstream_request, \
48 ChannelstreamException
48 ChannelstreamException
49
49
50 from rhodecode.model.validation_schema.schemas import user_schema
50 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 Repository, PullRequest, UserEmailMap, User, UserFollowing)
52 Repository, PullRequest, UserEmailMap, User, UserFollowing)
53 from rhodecode.model.forms import UserForm
53 from rhodecode.model.forms import UserForm
54 from rhodecode.model.scm import RepoList
54 from rhodecode.model.scm import RepoList
55 from rhodecode.model.user import UserModel
55 from rhodecode.model.user import UserModel
56 from rhodecode.model.repo import RepoModel
56 from rhodecode.model.repo import RepoModel
57 from rhodecode.model.auth_token import AuthTokenModel
57 from rhodecode.model.auth_token import AuthTokenModel
58 from rhodecode.model.meta import Session
58 from rhodecode.model.meta import Session
59 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.comment import ChangesetCommentsModel
60 from rhodecode.model.comment import CommentsModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 class MyAccountController(BaseController):
65 class MyAccountController(BaseController):
66 """REST Controller styled on the Atom Publishing Protocol"""
66 """REST Controller styled on the Atom Publishing Protocol"""
67 # To properly map this controller, ensure your config/routing.py
67 # To properly map this controller, ensure your config/routing.py
68 # file has a resource setup:
68 # file has a resource setup:
69 # map.resource('setting', 'settings', controller='admin/settings',
69 # map.resource('setting', 'settings', controller='admin/settings',
70 # path_prefix='/admin', name_prefix='admin_')
70 # path_prefix='/admin', name_prefix='admin_')
71
71
72 @LoginRequired()
72 @LoginRequired()
73 @NotAnonymous()
73 @NotAnonymous()
74 def __before__(self):
74 def __before__(self):
75 super(MyAccountController, self).__before__()
75 super(MyAccountController, self).__before__()
76
76
77 def __load_data(self):
77 def __load_data(self):
78 c.user = User.get(c.rhodecode_user.user_id)
78 c.user = User.get(c.rhodecode_user.user_id)
79 if c.user.username == User.DEFAULT_USER:
79 if c.user.username == User.DEFAULT_USER:
80 h.flash(_("You can't edit this user since it's"
80 h.flash(_("You can't edit this user since it's"
81 " crucial for entire application"), category='warning')
81 " crucial for entire application"), category='warning')
82 return redirect(url('users'))
82 return redirect(url('users'))
83
83
84 c.auth_user = AuthUser(
84 c.auth_user = AuthUser(
85 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
85 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
86
86
87 def _load_my_repos_data(self, watched=False):
87 def _load_my_repos_data(self, watched=False):
88 if watched:
88 if watched:
89 admin = False
89 admin = False
90 follows_repos = Session().query(UserFollowing)\
90 follows_repos = Session().query(UserFollowing)\
91 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
91 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
92 .options(joinedload(UserFollowing.follows_repository))\
92 .options(joinedload(UserFollowing.follows_repository))\
93 .all()
93 .all()
94 repo_list = [x.follows_repository for x in follows_repos]
94 repo_list = [x.follows_repository for x in follows_repos]
95 else:
95 else:
96 admin = True
96 admin = True
97 repo_list = Repository.get_all_repos(
97 repo_list = Repository.get_all_repos(
98 user_id=c.rhodecode_user.user_id)
98 user_id=c.rhodecode_user.user_id)
99 repo_list = RepoList(repo_list, perm_set=[
99 repo_list = RepoList(repo_list, perm_set=[
100 'repository.read', 'repository.write', 'repository.admin'])
100 'repository.read', 'repository.write', 'repository.admin'])
101
101
102 repos_data = RepoModel().get_repos_as_dict(
102 repos_data = RepoModel().get_repos_as_dict(
103 repo_list=repo_list, admin=admin)
103 repo_list=repo_list, admin=admin)
104 # json used to render the grid
104 # json used to render the grid
105 return json.dumps(repos_data)
105 return json.dumps(repos_data)
106
106
107 @auth.CSRFRequired()
107 @auth.CSRFRequired()
108 def my_account_update(self):
108 def my_account_update(self):
109 """
109 """
110 POST /_admin/my_account Updates info of my account
110 POST /_admin/my_account Updates info of my account
111 """
111 """
112 # url('my_account')
112 # url('my_account')
113 c.active = 'profile_edit'
113 c.active = 'profile_edit'
114 self.__load_data()
114 self.__load_data()
115 c.perm_user = c.auth_user
115 c.perm_user = c.auth_user
116 c.extern_type = c.user.extern_type
116 c.extern_type = c.user.extern_type
117 c.extern_name = c.user.extern_name
117 c.extern_name = c.user.extern_name
118
118
119 defaults = c.user.get_dict()
119 defaults = c.user.get_dict()
120 update = False
120 update = False
121 _form = UserForm(edit=True,
121 _form = UserForm(edit=True,
122 old_data={'user_id': c.rhodecode_user.user_id,
122 old_data={'user_id': c.rhodecode_user.user_id,
123 'email': c.rhodecode_user.email})()
123 'email': c.rhodecode_user.email})()
124 form_result = {}
124 form_result = {}
125 try:
125 try:
126 post_data = dict(request.POST)
126 post_data = dict(request.POST)
127 post_data['new_password'] = ''
127 post_data['new_password'] = ''
128 post_data['password_confirmation'] = ''
128 post_data['password_confirmation'] = ''
129 form_result = _form.to_python(post_data)
129 form_result = _form.to_python(post_data)
130 # skip updating those attrs for my account
130 # skip updating those attrs for my account
131 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
131 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
132 'new_password', 'password_confirmation']
132 'new_password', 'password_confirmation']
133 # TODO: plugin should define if username can be updated
133 # TODO: plugin should define if username can be updated
134 if c.extern_type != "rhodecode":
134 if c.extern_type != "rhodecode":
135 # forbid updating username for external accounts
135 # forbid updating username for external accounts
136 skip_attrs.append('username')
136 skip_attrs.append('username')
137
137
138 UserModel().update_user(
138 UserModel().update_user(
139 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
139 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
140 h.flash(_('Your account was updated successfully'),
140 h.flash(_('Your account was updated successfully'),
141 category='success')
141 category='success')
142 Session().commit()
142 Session().commit()
143 update = True
143 update = True
144
144
145 except formencode.Invalid as errors:
145 except formencode.Invalid as errors:
146 return htmlfill.render(
146 return htmlfill.render(
147 render('admin/my_account/my_account.mako'),
147 render('admin/my_account/my_account.mako'),
148 defaults=errors.value,
148 defaults=errors.value,
149 errors=errors.error_dict or {},
149 errors=errors.error_dict or {},
150 prefix_error=False,
150 prefix_error=False,
151 encoding="UTF-8",
151 encoding="UTF-8",
152 force_defaults=False)
152 force_defaults=False)
153 except Exception:
153 except Exception:
154 log.exception("Exception updating user")
154 log.exception("Exception updating user")
155 h.flash(_('Error occurred during update of user %s')
155 h.flash(_('Error occurred during update of user %s')
156 % form_result.get('username'), category='error')
156 % form_result.get('username'), category='error')
157
157
158 if update:
158 if update:
159 return redirect('my_account')
159 return redirect('my_account')
160
160
161 return htmlfill.render(
161 return htmlfill.render(
162 render('admin/my_account/my_account.mako'),
162 render('admin/my_account/my_account.mako'),
163 defaults=defaults,
163 defaults=defaults,
164 encoding="UTF-8",
164 encoding="UTF-8",
165 force_defaults=False
165 force_defaults=False
166 )
166 )
167
167
168 def my_account(self):
168 def my_account(self):
169 """
169 """
170 GET /_admin/my_account Displays info about my account
170 GET /_admin/my_account Displays info about my account
171 """
171 """
172 # url('my_account')
172 # url('my_account')
173 c.active = 'profile'
173 c.active = 'profile'
174 self.__load_data()
174 self.__load_data()
175
175
176 defaults = c.user.get_dict()
176 defaults = c.user.get_dict()
177 return htmlfill.render(
177 return htmlfill.render(
178 render('admin/my_account/my_account.mako'),
178 render('admin/my_account/my_account.mako'),
179 defaults=defaults, encoding="UTF-8", force_defaults=False)
179 defaults=defaults, encoding="UTF-8", force_defaults=False)
180
180
181 def my_account_edit(self):
181 def my_account_edit(self):
182 """
182 """
183 GET /_admin/my_account/edit Displays edit form of my account
183 GET /_admin/my_account/edit Displays edit form of my account
184 """
184 """
185 c.active = 'profile_edit'
185 c.active = 'profile_edit'
186 self.__load_data()
186 self.__load_data()
187 c.perm_user = c.auth_user
187 c.perm_user = c.auth_user
188 c.extern_type = c.user.extern_type
188 c.extern_type = c.user.extern_type
189 c.extern_name = c.user.extern_name
189 c.extern_name = c.user.extern_name
190
190
191 defaults = c.user.get_dict()
191 defaults = c.user.get_dict()
192 return htmlfill.render(
192 return htmlfill.render(
193 render('admin/my_account/my_account.mako'),
193 render('admin/my_account/my_account.mako'),
194 defaults=defaults,
194 defaults=defaults,
195 encoding="UTF-8",
195 encoding="UTF-8",
196 force_defaults=False
196 force_defaults=False
197 )
197 )
198
198
199 @auth.CSRFRequired(except_methods=['GET'])
199 @auth.CSRFRequired(except_methods=['GET'])
200 def my_account_password(self):
200 def my_account_password(self):
201 c.active = 'password'
201 c.active = 'password'
202 self.__load_data()
202 self.__load_data()
203 c.extern_type = c.user.extern_type
203 c.extern_type = c.user.extern_type
204
204
205 schema = user_schema.ChangePasswordSchema().bind(
205 schema = user_schema.ChangePasswordSchema().bind(
206 username=c.rhodecode_user.username)
206 username=c.rhodecode_user.username)
207
207
208 form = forms.Form(schema,
208 form = forms.Form(schema,
209 buttons=(forms.buttons.save, forms.buttons.reset))
209 buttons=(forms.buttons.save, forms.buttons.reset))
210
210
211 if request.method == 'POST' and c.extern_type == 'rhodecode':
211 if request.method == 'POST' and c.extern_type == 'rhodecode':
212 controls = request.POST.items()
212 controls = request.POST.items()
213 try:
213 try:
214 valid_data = form.validate(controls)
214 valid_data = form.validate(controls)
215 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
215 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
216 instance = c.rhodecode_user.get_instance()
216 instance = c.rhodecode_user.get_instance()
217 instance.update_userdata(force_password_change=False)
217 instance.update_userdata(force_password_change=False)
218 Session().commit()
218 Session().commit()
219 except forms.ValidationFailure as e:
219 except forms.ValidationFailure as e:
220 request.session.flash(
220 request.session.flash(
221 _('Error occurred during update of user password'),
221 _('Error occurred during update of user password'),
222 queue='error')
222 queue='error')
223 form = e
223 form = e
224 except Exception:
224 except Exception:
225 log.exception("Exception updating password")
225 log.exception("Exception updating password")
226 request.session.flash(
226 request.session.flash(
227 _('Error occurred during update of user password'),
227 _('Error occurred during update of user password'),
228 queue='error')
228 queue='error')
229 else:
229 else:
230 session.setdefault('rhodecode_user', {}).update(
230 session.setdefault('rhodecode_user', {}).update(
231 {'password': md5(instance.password)})
231 {'password': md5(instance.password)})
232 session.save()
232 session.save()
233 request.session.flash(
233 request.session.flash(
234 _("Successfully updated password"), queue='success')
234 _("Successfully updated password"), queue='success')
235 return redirect(url('my_account_password'))
235 return redirect(url('my_account_password'))
236
236
237 c.form = form
237 c.form = form
238 return render('admin/my_account/my_account.mako')
238 return render('admin/my_account/my_account.mako')
239
239
240 def my_account_repos(self):
240 def my_account_repos(self):
241 c.active = 'repos'
241 c.active = 'repos'
242 self.__load_data()
242 self.__load_data()
243
243
244 # json used to render the grid
244 # json used to render the grid
245 c.data = self._load_my_repos_data()
245 c.data = self._load_my_repos_data()
246 return render('admin/my_account/my_account.mako')
246 return render('admin/my_account/my_account.mako')
247
247
248 def my_account_watched(self):
248 def my_account_watched(self):
249 c.active = 'watched'
249 c.active = 'watched'
250 self.__load_data()
250 self.__load_data()
251
251
252 # json used to render the grid
252 # json used to render the grid
253 c.data = self._load_my_repos_data(watched=True)
253 c.data = self._load_my_repos_data(watched=True)
254 return render('admin/my_account/my_account.mako')
254 return render('admin/my_account/my_account.mako')
255
255
256 def my_account_perms(self):
256 def my_account_perms(self):
257 c.active = 'perms'
257 c.active = 'perms'
258 self.__load_data()
258 self.__load_data()
259 c.perm_user = c.auth_user
259 c.perm_user = c.auth_user
260
260
261 return render('admin/my_account/my_account.mako')
261 return render('admin/my_account/my_account.mako')
262
262
263 def my_account_emails(self):
263 def my_account_emails(self):
264 c.active = 'emails'
264 c.active = 'emails'
265 self.__load_data()
265 self.__load_data()
266
266
267 c.user_email_map = UserEmailMap.query()\
267 c.user_email_map = UserEmailMap.query()\
268 .filter(UserEmailMap.user == c.user).all()
268 .filter(UserEmailMap.user == c.user).all()
269 return render('admin/my_account/my_account.mako')
269 return render('admin/my_account/my_account.mako')
270
270
271 @auth.CSRFRequired()
271 @auth.CSRFRequired()
272 def my_account_emails_add(self):
272 def my_account_emails_add(self):
273 email = request.POST.get('new_email')
273 email = request.POST.get('new_email')
274
274
275 try:
275 try:
276 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
276 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
277 Session().commit()
277 Session().commit()
278 h.flash(_("Added new email address `%s` for user account") % email,
278 h.flash(_("Added new email address `%s` for user account") % email,
279 category='success')
279 category='success')
280 except formencode.Invalid as error:
280 except formencode.Invalid as error:
281 msg = error.error_dict['email']
281 msg = error.error_dict['email']
282 h.flash(msg, category='error')
282 h.flash(msg, category='error')
283 except Exception:
283 except Exception:
284 log.exception("Exception in my_account_emails")
284 log.exception("Exception in my_account_emails")
285 h.flash(_('An error occurred during email saving'),
285 h.flash(_('An error occurred during email saving'),
286 category='error')
286 category='error')
287 return redirect(url('my_account_emails'))
287 return redirect(url('my_account_emails'))
288
288
289 @auth.CSRFRequired()
289 @auth.CSRFRequired()
290 def my_account_emails_delete(self):
290 def my_account_emails_delete(self):
291 email_id = request.POST.get('del_email_id')
291 email_id = request.POST.get('del_email_id')
292 user_model = UserModel()
292 user_model = UserModel()
293 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
293 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
294 Session().commit()
294 Session().commit()
295 h.flash(_("Removed email address from user account"),
295 h.flash(_("Removed email address from user account"),
296 category='success')
296 category='success')
297 return redirect(url('my_account_emails'))
297 return redirect(url('my_account_emails'))
298
298
299 def _extract_ordering(self, request):
299 def _extract_ordering(self, request):
300 column_index = safe_int(request.GET.get('order[0][column]'))
300 column_index = safe_int(request.GET.get('order[0][column]'))
301 order_dir = request.GET.get('order[0][dir]', 'desc')
301 order_dir = request.GET.get('order[0][dir]', 'desc')
302 order_by = request.GET.get(
302 order_by = request.GET.get(
303 'columns[%s][data][sort]' % column_index, 'name_raw')
303 'columns[%s][data][sort]' % column_index, 'name_raw')
304 return order_by, order_dir
304 return order_by, order_dir
305
305
306 def _get_pull_requests_list(self, statuses):
306 def _get_pull_requests_list(self, statuses):
307 start = safe_int(request.GET.get('start'), 0)
307 start = safe_int(request.GET.get('start'), 0)
308 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
308 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
309 order_by, order_dir = self._extract_ordering(request)
309 order_by, order_dir = self._extract_ordering(request)
310
310
311 pull_requests = PullRequestModel().get_im_participating_in(
311 pull_requests = PullRequestModel().get_im_participating_in(
312 user_id=c.rhodecode_user.user_id,
312 user_id=c.rhodecode_user.user_id,
313 statuses=statuses,
313 statuses=statuses,
314 offset=start, length=length, order_by=order_by,
314 offset=start, length=length, order_by=order_by,
315 order_dir=order_dir)
315 order_dir=order_dir)
316
316
317 pull_requests_total_count = PullRequestModel().count_im_participating_in(
317 pull_requests_total_count = PullRequestModel().count_im_participating_in(
318 user_id=c.rhodecode_user.user_id, statuses=statuses)
318 user_id=c.rhodecode_user.user_id, statuses=statuses)
319
319
320 from rhodecode.lib.utils import PartialRenderer
320 from rhodecode.lib.utils import PartialRenderer
321 _render = PartialRenderer('data_table/_dt_elements.mako')
321 _render = PartialRenderer('data_table/_dt_elements.mako')
322 data = []
322 data = []
323 for pr in pull_requests:
323 for pr in pull_requests:
324 repo_id = pr.target_repo_id
324 repo_id = pr.target_repo_id
325 comments = ChangesetCommentsModel().get_all_comments(
325 comments = CommentsModel().get_all_comments(
326 repo_id, pull_request=pr)
326 repo_id, pull_request=pr)
327 owned = pr.user_id == c.rhodecode_user.user_id
327 owned = pr.user_id == c.rhodecode_user.user_id
328 status = pr.calculated_review_status()
328 status = pr.calculated_review_status()
329
329
330 data.append({
330 data.append({
331 'target_repo': _render('pullrequest_target_repo',
331 'target_repo': _render('pullrequest_target_repo',
332 pr.target_repo.repo_name),
332 pr.target_repo.repo_name),
333 'name': _render('pullrequest_name',
333 'name': _render('pullrequest_name',
334 pr.pull_request_id, pr.target_repo.repo_name,
334 pr.pull_request_id, pr.target_repo.repo_name,
335 short=True),
335 short=True),
336 'name_raw': pr.pull_request_id,
336 'name_raw': pr.pull_request_id,
337 'status': _render('pullrequest_status', status),
337 'status': _render('pullrequest_status', status),
338 'title': _render(
338 'title': _render(
339 'pullrequest_title', pr.title, pr.description),
339 'pullrequest_title', pr.title, pr.description),
340 'description': h.escape(pr.description),
340 'description': h.escape(pr.description),
341 'updated_on': _render('pullrequest_updated_on',
341 'updated_on': _render('pullrequest_updated_on',
342 h.datetime_to_time(pr.updated_on)),
342 h.datetime_to_time(pr.updated_on)),
343 'updated_on_raw': h.datetime_to_time(pr.updated_on),
343 'updated_on_raw': h.datetime_to_time(pr.updated_on),
344 'created_on': _render('pullrequest_updated_on',
344 'created_on': _render('pullrequest_updated_on',
345 h.datetime_to_time(pr.created_on)),
345 h.datetime_to_time(pr.created_on)),
346 'created_on_raw': h.datetime_to_time(pr.created_on),
346 'created_on_raw': h.datetime_to_time(pr.created_on),
347 'author': _render('pullrequest_author',
347 'author': _render('pullrequest_author',
348 pr.author.full_contact, ),
348 pr.author.full_contact, ),
349 'author_raw': pr.author.full_name,
349 'author_raw': pr.author.full_name,
350 'comments': _render('pullrequest_comments', len(comments)),
350 'comments': _render('pullrequest_comments', len(comments)),
351 'comments_raw': len(comments),
351 'comments_raw': len(comments),
352 'closed': pr.is_closed(),
352 'closed': pr.is_closed(),
353 'owned': owned
353 'owned': owned
354 })
354 })
355 # json used to render the grid
355 # json used to render the grid
356 data = ({
356 data = ({
357 'data': data,
357 'data': data,
358 'recordsTotal': pull_requests_total_count,
358 'recordsTotal': pull_requests_total_count,
359 'recordsFiltered': pull_requests_total_count,
359 'recordsFiltered': pull_requests_total_count,
360 })
360 })
361 return data
361 return data
362
362
363 def my_account_pullrequests(self):
363 def my_account_pullrequests(self):
364 c.active = 'pullrequests'
364 c.active = 'pullrequests'
365 self.__load_data()
365 self.__load_data()
366 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
366 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
367
367
368 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
368 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
369 if c.show_closed:
369 if c.show_closed:
370 statuses += [PullRequest.STATUS_CLOSED]
370 statuses += [PullRequest.STATUS_CLOSED]
371 data = self._get_pull_requests_list(statuses)
371 data = self._get_pull_requests_list(statuses)
372 if not request.is_xhr:
372 if not request.is_xhr:
373 c.data_participate = json.dumps(data['data'])
373 c.data_participate = json.dumps(data['data'])
374 c.records_total_participate = data['recordsTotal']
374 c.records_total_participate = data['recordsTotal']
375 return render('admin/my_account/my_account.mako')
375 return render('admin/my_account/my_account.mako')
376 else:
376 else:
377 return json.dumps(data)
377 return json.dumps(data)
378
378
379 def my_account_auth_tokens(self):
379 def my_account_auth_tokens(self):
380 c.active = 'auth_tokens'
380 c.active = 'auth_tokens'
381 self.__load_data()
381 self.__load_data()
382 show_expired = True
382 show_expired = True
383 c.lifetime_values = [
383 c.lifetime_values = [
384 (str(-1), _('forever')),
384 (str(-1), _('forever')),
385 (str(5), _('5 minutes')),
385 (str(5), _('5 minutes')),
386 (str(60), _('1 hour')),
386 (str(60), _('1 hour')),
387 (str(60 * 24), _('1 day')),
387 (str(60 * 24), _('1 day')),
388 (str(60 * 24 * 30), _('1 month')),
388 (str(60 * 24 * 30), _('1 month')),
389 ]
389 ]
390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
392 for x in AuthTokenModel.cls.ROLES]
392 for x in AuthTokenModel.cls.ROLES]
393 c.role_options = [(c.role_values, _("Role"))]
393 c.role_options = [(c.role_values, _("Role"))]
394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
395 c.rhodecode_user.user_id, show_expired=show_expired)
395 c.rhodecode_user.user_id, show_expired=show_expired)
396 return render('admin/my_account/my_account.mako')
396 return render('admin/my_account/my_account.mako')
397
397
398 @auth.CSRFRequired()
398 @auth.CSRFRequired()
399 def my_account_auth_tokens_add(self):
399 def my_account_auth_tokens_add(self):
400 lifetime = safe_int(request.POST.get('lifetime'), -1)
400 lifetime = safe_int(request.POST.get('lifetime'), -1)
401 description = request.POST.get('description')
401 description = request.POST.get('description')
402 role = request.POST.get('role')
402 role = request.POST.get('role')
403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
404 role)
404 role)
405 Session().commit()
405 Session().commit()
406 h.flash(_("Auth token successfully created"), category='success')
406 h.flash(_("Auth token successfully created"), category='success')
407 return redirect(url('my_account_auth_tokens'))
407 return redirect(url('my_account_auth_tokens'))
408
408
409 @auth.CSRFRequired()
409 @auth.CSRFRequired()
410 def my_account_auth_tokens_delete(self):
410 def my_account_auth_tokens_delete(self):
411 auth_token = request.POST.get('del_auth_token')
411 auth_token = request.POST.get('del_auth_token')
412 user_id = c.rhodecode_user.user_id
412 user_id = c.rhodecode_user.user_id
413 if request.POST.get('del_auth_token_builtin'):
413 if request.POST.get('del_auth_token_builtin'):
414 user = User.get(user_id)
414 user = User.get(user_id)
415 if user:
415 if user:
416 user.api_key = generate_auth_token(user.username)
416 user.api_key = generate_auth_token(user.username)
417 Session().add(user)
417 Session().add(user)
418 Session().commit()
418 Session().commit()
419 h.flash(_("Auth token successfully reset"), category='success')
419 h.flash(_("Auth token successfully reset"), category='success')
420 elif auth_token:
420 elif auth_token:
421 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
421 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
422 Session().commit()
422 Session().commit()
423 h.flash(_("Auth token successfully deleted"), category='success')
423 h.flash(_("Auth token successfully deleted"), category='success')
424
424
425 return redirect(url('my_account_auth_tokens'))
425 return redirect(url('my_account_auth_tokens'))
426
426
427 def my_notifications(self):
427 def my_notifications(self):
428 c.active = 'notifications'
428 c.active = 'notifications'
429 return render('admin/my_account/my_account.mako')
429 return render('admin/my_account/my_account.mako')
430
430
431 @auth.CSRFRequired()
431 @auth.CSRFRequired()
432 @jsonify
432 @jsonify
433 def my_notifications_toggle_visibility(self):
433 def my_notifications_toggle_visibility(self):
434 user = c.rhodecode_user.get_instance()
434 user = c.rhodecode_user.get_instance()
435 new_status = not user.user_data.get('notification_status', True)
435 new_status = not user.user_data.get('notification_status', True)
436 user.update_userdata(notification_status=new_status)
436 user.update_userdata(notification_status=new_status)
437 Session().commit()
437 Session().commit()
438 return user.user_data['notification_status']
438 return user.user_data['notification_status']
439
439
440 @auth.CSRFRequired()
440 @auth.CSRFRequired()
441 @jsonify
441 @jsonify
442 def my_account_notifications_test_channelstream(self):
442 def my_account_notifications_test_channelstream(self):
443 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
443 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
444 c.rhodecode_user.username, datetime.datetime.now())
444 c.rhodecode_user.username, datetime.datetime.now())
445 payload = {
445 payload = {
446 'type': 'message',
446 'type': 'message',
447 'timestamp': datetime.datetime.utcnow(),
447 'timestamp': datetime.datetime.utcnow(),
448 'user': 'system',
448 'user': 'system',
449 #'channel': 'broadcast',
449 #'channel': 'broadcast',
450 'pm_users': [c.rhodecode_user.username],
450 'pm_users': [c.rhodecode_user.username],
451 'message': {
451 'message': {
452 'message': message,
452 'message': message,
453 'level': 'info',
453 'level': 'info',
454 'topic': '/notifications'
454 'topic': '/notifications'
455 }
455 }
456 }
456 }
457
457
458 registry = get_current_registry()
458 registry = get_current_registry()
459 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
459 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
460 channelstream_config = rhodecode_plugins.get('channelstream', {})
460 channelstream_config = rhodecode_plugins.get('channelstream', {})
461
461
462 try:
462 try:
463 channelstream_request(channelstream_config, [payload], '/message')
463 channelstream_request(channelstream_config, [payload], '/message')
464 except ChannelstreamException as e:
464 except ChannelstreamException as e:
465 log.exception('Failed to send channelstream data')
465 log.exception('Failed to send channelstream data')
466 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
466 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
467 return {"response": 'Channelstream data sent. '
467 return {"response": 'Channelstream data sent. '
468 'You should see a new live message now.'}
468 'You should see a new live message now.'}
@@ -1,470 +1,470 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 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 # auto collapse if we have more than limit
201 # auto collapse if we have more than limit
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204
204
205 c.commit_statuses = ChangesetStatus.STATUSES
205 c.commit_statuses = ChangesetStatus.STATUSES
206 c.inline_comments = []
206 c.inline_comments = []
207 c.files = []
207 c.files = []
208
208
209 c.statuses = []
209 c.statuses = []
210 c.comments = []
210 c.comments = []
211 if len(c.commit_ranges) == 1:
211 if len(c.commit_ranges) == 1:
212 commit = c.commit_ranges[0]
212 commit = c.commit_ranges[0]
213 c.comments = ChangesetCommentsModel().get_comments(
213 c.comments = CommentsModel().get_comments(
214 c.rhodecode_db_repo.repo_id,
214 c.rhodecode_db_repo.repo_id,
215 revision=commit.raw_id)
215 revision=commit.raw_id)
216 c.statuses.append(ChangesetStatusModel().get_status(
216 c.statuses.append(ChangesetStatusModel().get_status(
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 # comments from PR
218 # comments from PR
219 statuses = ChangesetStatusModel().get_statuses(
219 statuses = ChangesetStatusModel().get_statuses(
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 with_revisions=True)
221 with_revisions=True)
222 prs = set(st.pull_request for st in statuses
222 prs = set(st.pull_request for st in statuses
223 if st.pull_request is not None)
223 if st.pull_request is not None)
224 # from associated statuses, check the pull requests, and
224 # from associated statuses, check the pull requests, and
225 # show comments from them
225 # show comments from them
226 for pr in prs:
226 for pr in prs:
227 c.comments.extend(pr.comments)
227 c.comments.extend(pr.comments)
228
228
229 # Iterate over ranges (default commit view is always one commit)
229 # Iterate over ranges (default commit view is always one commit)
230 for commit in c.commit_ranges:
230 for commit in c.commit_ranges:
231 c.changes[commit.raw_id] = []
231 c.changes[commit.raw_id] = []
232
232
233 commit2 = commit
233 commit2 = commit
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235
235
236 _diff = c.rhodecode_repo.get_diff(
236 _diff = c.rhodecode_repo.get_diff(
237 commit1, commit2,
237 commit1, commit2,
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 diff_processor = diffs.DiffProcessor(
239 diff_processor = diffs.DiffProcessor(
240 _diff, format='newdiff', diff_limit=diff_limit,
240 _diff, format='newdiff', diff_limit=diff_limit,
241 file_limit=file_limit, show_full_diff=fulldiff)
241 file_limit=file_limit, show_full_diff=fulldiff)
242
242
243 commit_changes = OrderedDict()
243 commit_changes = OrderedDict()
244 if method == 'show':
244 if method == 'show':
245 _parsed = diff_processor.prepare()
245 _parsed = diff_processor.prepare()
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247
247
248 _parsed = diff_processor.prepare()
248 _parsed = diff_processor.prepare()
249
249
250 def _node_getter(commit):
250 def _node_getter(commit):
251 def get_node(fname):
251 def get_node(fname):
252 try:
252 try:
253 return commit.get_node(fname)
253 return commit.get_node(fname)
254 except NodeDoesNotExistError:
254 except NodeDoesNotExistError:
255 return None
255 return None
256 return get_node
256 return get_node
257
257
258 inline_comments = ChangesetCommentsModel().get_inline_comments(
258 inline_comments = CommentsModel().get_inline_comments(
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
260 c.inline_cnt = CommentsModel().get_inline_comments_count(
261 inline_comments)
261 inline_comments)
262
262
263 diffset = codeblocks.DiffSet(
263 diffset = codeblocks.DiffSet(
264 repo_name=c.repo_name,
264 repo_name=c.repo_name,
265 source_node_getter=_node_getter(commit1),
265 source_node_getter=_node_getter(commit1),
266 target_node_getter=_node_getter(commit2),
266 target_node_getter=_node_getter(commit2),
267 comments=inline_comments
267 comments=inline_comments
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 c.changes[commit.raw_id] = diffset
269 c.changes[commit.raw_id] = diffset
270 else:
270 else:
271 # downloads/raw we only need RAW diff nothing else
271 # downloads/raw we only need RAW diff nothing else
272 diff = diff_processor.as_raw()
272 diff = diff_processor.as_raw()
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274
274
275 # sort comments by how they were generated
275 # sort comments by how they were generated
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277
277
278
278
279 if len(c.commit_ranges) == 1:
279 if len(c.commit_ranges) == 1:
280 c.commit = c.commit_ranges[0]
280 c.commit = c.commit_ranges[0]
281 c.parent_tmpl = ''.join(
281 c.parent_tmpl = ''.join(
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 if method == 'download':
283 if method == 'download':
284 response.content_type = 'text/plain'
284 response.content_type = 'text/plain'
285 response.content_disposition = (
285 response.content_disposition = (
286 'attachment; filename=%s.diff' % commit_id_range[:12])
286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 return diff
287 return diff
288 elif method == 'patch':
288 elif method == 'patch':
289 response.content_type = 'text/plain'
289 response.content_type = 'text/plain'
290 c.diff = safe_unicode(diff)
290 c.diff = safe_unicode(diff)
291 return render('changeset/patch_changeset.mako')
291 return render('changeset/patch_changeset.mako')
292 elif method == 'raw':
292 elif method == 'raw':
293 response.content_type = 'text/plain'
293 response.content_type = 'text/plain'
294 return diff
294 return diff
295 elif method == 'show':
295 elif method == 'show':
296 if len(c.commit_ranges) == 1:
296 if len(c.commit_ranges) == 1:
297 return render('changeset/changeset.mako')
297 return render('changeset/changeset.mako')
298 else:
298 else:
299 c.ancestor = None
299 c.ancestor = None
300 c.target_repo = c.rhodecode_db_repo
300 c.target_repo = c.rhodecode_db_repo
301 return render('changeset/changeset_range.mako')
301 return render('changeset/changeset_range.mako')
302
302
303 @LoginRequired()
303 @LoginRequired()
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 'repository.admin')
305 'repository.admin')
306 def index(self, revision, method='show'):
306 def index(self, revision, method='show'):
307 return self._index(revision, method=method)
307 return self._index(revision, method=method)
308
308
309 @LoginRequired()
309 @LoginRequired()
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 'repository.admin')
311 'repository.admin')
312 def changeset_raw(self, revision):
312 def changeset_raw(self, revision):
313 return self._index(revision, method='raw')
313 return self._index(revision, method='raw')
314
314
315 @LoginRequired()
315 @LoginRequired()
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 'repository.admin')
317 'repository.admin')
318 def changeset_patch(self, revision):
318 def changeset_patch(self, revision):
319 return self._index(revision, method='patch')
319 return self._index(revision, method='patch')
320
320
321 @LoginRequired()
321 @LoginRequired()
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 'repository.admin')
323 'repository.admin')
324 def changeset_download(self, revision):
324 def changeset_download(self, revision):
325 return self._index(revision, method='download')
325 return self._index(revision, method='download')
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @NotAnonymous()
328 @NotAnonymous()
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 'repository.admin')
330 'repository.admin')
331 @auth.CSRFRequired()
331 @auth.CSRFRequired()
332 @jsonify
332 @jsonify
333 def comment(self, repo_name, revision):
333 def comment(self, repo_name, revision):
334 commit_id = revision
334 commit_id = revision
335 status = request.POST.get('changeset_status', None)
335 status = request.POST.get('changeset_status', None)
336 text = request.POST.get('text')
336 text = request.POST.get('text')
337 if status:
337 if status:
338 text = text or (_('Status change %(transition_icon)s %(status)s')
338 text = text or (_('Status change %(transition_icon)s %(status)s')
339 % {'transition_icon': '>',
339 % {'transition_icon': '>',
340 'status': ChangesetStatus.get_status_lbl(status)})
340 'status': ChangesetStatus.get_status_lbl(status)})
341
341
342 multi_commit_ids = filter(
342 multi_commit_ids = filter(
343 lambda s: s not in ['', None],
343 lambda s: s not in ['', None],
344 request.POST.get('commit_ids', '').split(','),)
344 request.POST.get('commit_ids', '').split(','),)
345
345
346 commit_ids = multi_commit_ids or [commit_id]
346 commit_ids = multi_commit_ids or [commit_id]
347 comment = None
347 comment = None
348 for current_id in filter(None, commit_ids):
348 for current_id in filter(None, commit_ids):
349 c.co = comment = ChangesetCommentsModel().create(
349 c.co = comment = CommentsModel().create(
350 text=text,
350 text=text,
351 repo=c.rhodecode_db_repo.repo_id,
351 repo=c.rhodecode_db_repo.repo_id,
352 user=c.rhodecode_user.user_id,
352 user=c.rhodecode_user.user_id,
353 commit_id=current_id,
353 commit_id=current_id,
354 f_path=request.POST.get('f_path'),
354 f_path=request.POST.get('f_path'),
355 line_no=request.POST.get('line'),
355 line_no=request.POST.get('line'),
356 status_change=(ChangesetStatus.get_status_lbl(status)
356 status_change=(ChangesetStatus.get_status_lbl(status)
357 if status else None),
357 if status else None),
358 status_change_type=status
358 status_change_type=status
359 )
359 )
360 c.inline_comment = True if comment.line_no else False
360 c.inline_comment = True if comment.line_no else False
361
361
362 # get status if set !
362 # get status if set !
363 if status:
363 if status:
364 # if latest status was from pull request and it's closed
364 # if latest status was from pull request and it's closed
365 # disallow changing status !
365 # disallow changing status !
366 # dont_allow_on_closed_pull_request = True !
366 # dont_allow_on_closed_pull_request = True !
367
367
368 try:
368 try:
369 ChangesetStatusModel().set_status(
369 ChangesetStatusModel().set_status(
370 c.rhodecode_db_repo.repo_id,
370 c.rhodecode_db_repo.repo_id,
371 status,
371 status,
372 c.rhodecode_user.user_id,
372 c.rhodecode_user.user_id,
373 comment,
373 comment,
374 revision=current_id,
374 revision=current_id,
375 dont_allow_on_closed_pull_request=True
375 dont_allow_on_closed_pull_request=True
376 )
376 )
377 except StatusChangeOnClosedPullRequestError:
377 except StatusChangeOnClosedPullRequestError:
378 msg = _('Changing the status of a commit associated with '
378 msg = _('Changing the status of a commit associated with '
379 'a closed pull request is not allowed')
379 'a closed pull request is not allowed')
380 log.exception(msg)
380 log.exception(msg)
381 h.flash(msg, category='warning')
381 h.flash(msg, category='warning')
382 return redirect(h.url(
382 return redirect(h.url(
383 'changeset_home', repo_name=repo_name,
383 'changeset_home', repo_name=repo_name,
384 revision=current_id))
384 revision=current_id))
385
385
386 # finalize, commit and redirect
386 # finalize, commit and redirect
387 Session().commit()
387 Session().commit()
388
388
389 data = {
389 data = {
390 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
390 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
391 }
391 }
392 if comment:
392 if comment:
393 data.update(comment.get_dict())
393 data.update(comment.get_dict())
394 data.update({'rendered_text':
394 data.update({'rendered_text':
395 render('changeset/changeset_comment_block.mako')})
395 render('changeset/changeset_comment_block.mako')})
396
396
397 return data
397 return data
398
398
399 @LoginRequired()
399 @LoginRequired()
400 @NotAnonymous()
400 @NotAnonymous()
401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 'repository.admin')
402 'repository.admin')
403 @auth.CSRFRequired()
403 @auth.CSRFRequired()
404 def preview_comment(self):
404 def preview_comment(self):
405 # Technically a CSRF token is not needed as no state changes with this
405 # Technically a CSRF token is not needed as no state changes with this
406 # call. However, as this is a POST is better to have it, so automated
406 # call. However, as this is a POST is better to have it, so automated
407 # tools don't flag it as potential CSRF.
407 # tools don't flag it as potential CSRF.
408 # Post is required because the payload could be bigger than the maximum
408 # Post is required because the payload could be bigger than the maximum
409 # allowed by GET.
409 # allowed by GET.
410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
411 raise HTTPBadRequest()
411 raise HTTPBadRequest()
412 text = request.POST.get('text')
412 text = request.POST.get('text')
413 renderer = request.POST.get('renderer') or 'rst'
413 renderer = request.POST.get('renderer') or 'rst'
414 if text:
414 if text:
415 return h.render(text, renderer=renderer, mentions=True)
415 return h.render(text, renderer=renderer, mentions=True)
416 return ''
416 return ''
417
417
418 @LoginRequired()
418 @LoginRequired()
419 @NotAnonymous()
419 @NotAnonymous()
420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
421 'repository.admin')
421 'repository.admin')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 @jsonify
423 @jsonify
424 def delete_comment(self, repo_name, comment_id):
424 def delete_comment(self, repo_name, comment_id):
425 comment = ChangesetComment.get(comment_id)
425 comment = ChangesetComment.get(comment_id)
426 owner = (comment.author.user_id == c.rhodecode_user.user_id)
426 owner = (comment.author.user_id == c.rhodecode_user.user_id)
427 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
427 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
428 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
428 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
429 ChangesetCommentsModel().delete(comment=comment)
429 CommentsModel().delete(comment=comment)
430 Session().commit()
430 Session().commit()
431 return True
431 return True
432 else:
432 else:
433 raise HTTPForbidden()
433 raise HTTPForbidden()
434
434
435 @LoginRequired()
435 @LoginRequired()
436 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
436 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
437 'repository.admin')
437 'repository.admin')
438 @jsonify
438 @jsonify
439 def changeset_info(self, repo_name, revision):
439 def changeset_info(self, repo_name, revision):
440 if request.is_xhr:
440 if request.is_xhr:
441 try:
441 try:
442 return c.rhodecode_repo.get_commit(commit_id=revision)
442 return c.rhodecode_repo.get_commit(commit_id=revision)
443 except CommitDoesNotExistError as e:
443 except CommitDoesNotExistError as e:
444 return EmptyCommit(message=str(e))
444 return EmptyCommit(message=str(e))
445 else:
445 else:
446 raise HTTPBadRequest()
446 raise HTTPBadRequest()
447
447
448 @LoginRequired()
448 @LoginRequired()
449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 'repository.admin')
450 'repository.admin')
451 @jsonify
451 @jsonify
452 def changeset_children(self, repo_name, revision):
452 def changeset_children(self, repo_name, revision):
453 if request.is_xhr:
453 if request.is_xhr:
454 commit = c.rhodecode_repo.get_commit(commit_id=revision)
454 commit = c.rhodecode_repo.get_commit(commit_id=revision)
455 result = {"results": commit.children}
455 result = {"results": commit.children}
456 return result
456 return result
457 else:
457 else:
458 raise HTTPBadRequest()
458 raise HTTPBadRequest()
459
459
460 @LoginRequired()
460 @LoginRequired()
461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
462 'repository.admin')
462 'repository.admin')
463 @jsonify
463 @jsonify
464 def changeset_parents(self, repo_name, revision):
464 def changeset_parents(self, repo_name, revision):
465 if request.is_xhr:
465 if request.is_xhr:
466 commit = c.rhodecode_repo.get_commit(commit_id=revision)
466 commit = c.rhodecode_repo.get_commit(commit_id=revision)
467 result = {"results": commit.parents}
467 result = {"results": commit.parents}
468 return result
468 return result
469 else:
469 else:
470 raise HTTPBadRequest()
470 raise HTTPBadRequest()
@@ -1,1024 +1,1024 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 ChangesetCommentsModel
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
63 from rhodecode.model.pull_request import PullRequestModel
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 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = ChangesetCommentsModel().get_all_comments(
261 comments = CommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After succesfull merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 if self._meets_merge_pre_conditions(pull_request, user):
608 log.debug("Pre-conditions checked, trying to merge.")
608 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
609 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
611 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
612 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
613 self._merge_pull_request(pull_request, user, extras)
614
614
615 return redirect(url(
615 return redirect(url(
616 'pullrequest_show',
616 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
617 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
618 pull_request_id=pull_request.pull_request_id))
619
619
620 def _meets_merge_pre_conditions(self, pull_request, user):
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
622 raise HTTPForbidden()
623
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
627 h.flash(msg, category='error')
628 return False
628 return False
629
629
630 if (pull_request.calculated_review_status()
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
634 h.flash(msg, category='error')
635 return False
635 return False
636 return True
636 return True
637
637
638 def _merge_pull_request(self, pull_request, user, extras):
638 def _merge_pull_request(self, pull_request, user, extras):
639 merge_resp = PullRequestModel().merge(
639 merge_resp = PullRequestModel().merge(
640 pull_request, user, extras=extras)
640 pull_request, user, extras=extras)
641
641
642 if merge_resp.executed:
642 if merge_resp.executed:
643 log.debug("The merge was successful, closing the pull request.")
643 log.debug("The merge was successful, closing the pull request.")
644 PullRequestModel().close_pull_request(
644 PullRequestModel().close_pull_request(
645 pull_request.pull_request_id, user)
645 pull_request.pull_request_id, user)
646 Session().commit()
646 Session().commit()
647 msg = _('Pull request was successfully merged and closed.')
647 msg = _('Pull request was successfully merged and closed.')
648 h.flash(msg, category='success')
648 h.flash(msg, category='success')
649 else:
649 else:
650 log.debug(
650 log.debug(
651 "The merge was not successful. Merge response: %s",
651 "The merge was not successful. Merge response: %s",
652 merge_resp)
652 merge_resp)
653 msg = PullRequestModel().merge_status_message(
653 msg = PullRequestModel().merge_status_message(
654 merge_resp.failure_reason)
654 merge_resp.failure_reason)
655 h.flash(msg, category='error')
655 h.flash(msg, category='error')
656
656
657 def _update_reviewers(self, pull_request_id, review_members):
657 def _update_reviewers(self, pull_request_id, review_members):
658 reviewers = [
658 reviewers = [
659 (int(r['user_id']), r['reasons']) for r in review_members]
659 (int(r['user_id']), r['reasons']) for r in review_members]
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 Session().commit()
661 Session().commit()
662
662
663 def _reject_close(self, pull_request):
663 def _reject_close(self, pull_request):
664 if pull_request.is_closed():
664 if pull_request.is_closed():
665 raise HTTPForbidden()
665 raise HTTPForbidden()
666
666
667 PullRequestModel().close_pull_request_with_comment(
667 PullRequestModel().close_pull_request_with_comment(
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 Session().commit()
669 Session().commit()
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 'repository.admin')
674 'repository.admin')
675 @auth.CSRFRequired()
675 @auth.CSRFRequired()
676 @jsonify
676 @jsonify
677 def delete(self, repo_name, pull_request_id):
677 def delete(self, repo_name, pull_request_id):
678 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
680 # only owner can delete it !
680 # only owner can delete it !
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 PullRequestModel().delete(pull_request)
682 PullRequestModel().delete(pull_request)
683 Session().commit()
683 Session().commit()
684 h.flash(_('Successfully deleted pull request'),
684 h.flash(_('Successfully deleted pull request'),
685 category='success')
685 category='success')
686 return redirect(url('my_account_pullrequests'))
686 return redirect(url('my_account_pullrequests'))
687 raise HTTPForbidden()
687 raise HTTPForbidden()
688
688
689 def _get_pr_version(self, pull_request_id, version=None):
689 def _get_pr_version(self, pull_request_id, version=None):
690 pull_request_id = safe_int(pull_request_id)
690 pull_request_id = safe_int(pull_request_id)
691 at_version = None
691 at_version = None
692
692
693 if version and version == 'latest':
693 if version and version == 'latest':
694 pull_request_ver = PullRequest.get(pull_request_id)
694 pull_request_ver = PullRequest.get(pull_request_id)
695 pull_request_obj = pull_request_ver
695 pull_request_obj = pull_request_ver
696 _org_pull_request_obj = pull_request_obj
696 _org_pull_request_obj = pull_request_obj
697 at_version = 'latest'
697 at_version = 'latest'
698 elif version:
698 elif version:
699 pull_request_ver = PullRequestVersion.get_or_404(version)
699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 pull_request_obj = pull_request_ver
700 pull_request_obj = pull_request_ver
701 _org_pull_request_obj = pull_request_ver.pull_request
701 _org_pull_request_obj = pull_request_ver.pull_request
702 at_version = pull_request_ver.pull_request_version_id
702 at_version = pull_request_ver.pull_request_version_id
703 else:
703 else:
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705
705
706 pull_request_display_obj = PullRequest.get_pr_display_object(
706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 pull_request_obj, _org_pull_request_obj)
707 pull_request_obj, _org_pull_request_obj)
708 return _org_pull_request_obj, pull_request_obj, \
708 return _org_pull_request_obj, pull_request_obj, \
709 pull_request_display_obj, at_version
709 pull_request_display_obj, at_version
710
710
711 def _get_pr_version_changes(self, version, pull_request_latest):
711 def _get_pr_version_changes(self, version, pull_request_latest):
712 """
712 """
713 Generate changes commits, and diff data based on the current pr version
713 Generate changes commits, and diff data based on the current pr version
714 """
714 """
715
715
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717
717
718 # fake the version to add the "initial" state object
718 # fake the version to add the "initial" state object
719 pull_request_initial = PullRequest.get_pr_display_object(
719 pull_request_initial = PullRequest.get_pr_display_object(
720 pull_request_latest, pull_request_latest,
720 pull_request_latest, pull_request_latest,
721 internal_methods=['get_commit', 'versions'])
721 internal_methods=['get_commit', 'versions'])
722 pull_request_initial.revisions = []
722 pull_request_initial.revisions = []
723 pull_request_initial.source_repo.get_commit = types.MethodType(
723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727
727
728 _changes_versions = [pull_request_latest] + \
728 _changes_versions = [pull_request_latest] + \
729 list(reversed(c.versions)) + \
729 list(reversed(c.versions)) + \
730 [pull_request_initial]
730 [pull_request_initial]
731
731
732 if version == 'latest':
732 if version == 'latest':
733 index = 0
733 index = 0
734 else:
734 else:
735 for pos, prver in enumerate(_changes_versions):
735 for pos, prver in enumerate(_changes_versions):
736 ver = getattr(prver, 'pull_request_version_id', -1)
736 ver = getattr(prver, 'pull_request_version_id', -1)
737 if ver == safe_int(version):
737 if ver == safe_int(version):
738 index = pos
738 index = pos
739 break
739 break
740 else:
740 else:
741 index = 0
741 index = 0
742
742
743 cur_obj = _changes_versions[index]
743 cur_obj = _changes_versions[index]
744 prev_obj = _changes_versions[index + 1]
744 prev_obj = _changes_versions[index + 1]
745
745
746 old_commit_ids = set(prev_obj.revisions)
746 old_commit_ids = set(prev_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
748
748
749 changes = PullRequestModel()._calculate_commit_id_changes(
749 changes = PullRequestModel()._calculate_commit_id_changes(
750 old_commit_ids, new_commit_ids)
750 old_commit_ids, new_commit_ids)
751
751
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 cur_obj, prev_obj)
753 cur_obj, prev_obj)
754 file_changes = PullRequestModel()._calculate_file_changes(
754 file_changes = PullRequestModel()._calculate_file_changes(
755 old_diff_data, new_diff_data)
755 old_diff_data, new_diff_data)
756 return changes, file_changes
756 return changes, file_changes
757
757
758 @LoginRequired()
758 @LoginRequired()
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 'repository.admin')
760 'repository.admin')
761 def show(self, repo_name, pull_request_id):
761 def show(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
762 pull_request_id = safe_int(pull_request_id)
763 version = request.GET.get('version')
763 version = request.GET.get('version')
764
764
765 (pull_request_latest,
765 (pull_request_latest,
766 pull_request_at_ver,
766 pull_request_at_ver,
767 pull_request_display_obj,
767 pull_request_display_obj,
768 at_version) = self._get_pr_version(pull_request_id, version=version)
768 at_version) = self._get_pr_version(pull_request_id, version=version)
769
769
770 c.template_context['pull_request_data']['pull_request_id'] = \
770 c.template_context['pull_request_data']['pull_request_id'] = \
771 pull_request_id
771 pull_request_id
772
772
773 # pull_requests repo_name we opened it against
773 # pull_requests repo_name we opened it against
774 # ie. target_repo must match
774 # ie. target_repo must match
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 raise HTTPNotFound
776 raise HTTPNotFound
777
777
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 pull_request_at_ver)
779 pull_request_at_ver)
780
780
781 pr_closed = pull_request_latest.is_closed()
781 pr_closed = pull_request_latest.is_closed()
782 if at_version and not at_version == 'latest':
782 if at_version and not at_version == 'latest':
783 c.allowed_to_change_status = False
783 c.allowed_to_change_status = False
784 c.allowed_to_update = False
784 c.allowed_to_update = False
785 c.allowed_to_merge = False
785 c.allowed_to_merge = False
786 c.allowed_to_delete = False
786 c.allowed_to_delete = False
787 c.allowed_to_comment = False
787 c.allowed_to_comment = False
788 else:
788 else:
789 c.allowed_to_change_status = PullRequestModel(). \
789 c.allowed_to_change_status = PullRequestModel(). \
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 c.allowed_to_update = PullRequestModel().check_user_update(
791 c.allowed_to_update = PullRequestModel().check_user_update(
792 pull_request_latest, c.rhodecode_user) and not pr_closed
792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 pull_request_latest, c.rhodecode_user) and not pr_closed
794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 pull_request_latest, c.rhodecode_user) and not pr_closed
796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 c.allowed_to_comment = not pr_closed
797 c.allowed_to_comment = not pr_closed
798
798
799 cc_model = ChangesetCommentsModel()
799 cc_model = CommentsModel()
800
800
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
804 pull_request_at_ver)
805 c.approval_msg = None
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
808 c.pr_merge_status = False
809
809
810 # inline comments
810 # inline comments
811 inline_comments = cc_model.get_inline_comments(
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
815 inline_comments, version=at_version, include_aggregates=True)
816
816
817 c.versions = pull_request_display_obj.versions()
817 c.versions = pull_request_display_obj.versions()
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 c.at_version_pos = ChangesetComment.get_index_from_version(
819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 c.at_version_num, c.versions)
820 c.at_version_num, c.versions)
821
821
822 is_outdated = lambda co: \
822 is_outdated = lambda co: \
823 not c.at_version_num \
823 not c.at_version_num \
824 or co.pull_request_version_id <= c.at_version_num
824 or co.pull_request_version_id <= c.at_version_num
825
825
826 # inline_comments_until_version
826 # inline_comments_until_version
827 if c.at_version_num:
827 if c.at_version_num:
828 # if we use version, then do not show later comments
828 # if we use version, then do not show later comments
829 # than current version
829 # than current version
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
831 for fname, per_line_comments in inline_comments.iteritems():
831 for fname, per_line_comments in inline_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
833 for co in comments:
833 for co in comments:
834 if co.pull_request_version_id and is_outdated(co):
834 if co.pull_request_version_id and is_outdated(co):
835 paths[co.f_path][co.line_no].append(co)
835 paths[co.f_path][co.line_no].append(co)
836 inline_comments = paths
836 inline_comments = paths
837
837
838 # outdated comments
838 # outdated comments
839 c.outdated_cnt = 0
839 c.outdated_cnt = 0
840 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
840 if CommentsModel.use_outdated_comments(pull_request_latest):
841 outdated_comments = cc_model.get_outdated_comments(
841 outdated_comments = cc_model.get_outdated_comments(
842 c.rhodecode_db_repo.repo_id,
842 c.rhodecode_db_repo.repo_id,
843 pull_request=pull_request_at_ver)
843 pull_request=pull_request_at_ver)
844
844
845 # Count outdated comments and check for deleted files
845 # Count outdated comments and check for deleted files
846 is_outdated = lambda co: \
846 is_outdated = lambda co: \
847 not c.at_version_num \
847 not c.at_version_num \
848 or co.pull_request_version_id < c.at_version_num
848 or co.pull_request_version_id < c.at_version_num
849 for file_name, lines in outdated_comments.iteritems():
849 for file_name, lines in outdated_comments.iteritems():
850 for comments in lines.values():
850 for comments in lines.values():
851 comments = [comm for comm in comments if is_outdated(comm)]
851 comments = [comm for comm in comments if is_outdated(comm)]
852 c.outdated_cnt += len(comments)
852 c.outdated_cnt += len(comments)
853
853
854 # load compare data into template context
854 # load compare data into template context
855 self._load_compare_data(pull_request_at_ver, inline_comments)
855 self._load_compare_data(pull_request_at_ver, inline_comments)
856
856
857 # this is a hack to properly display links, when creating PR, the
857 # this is a hack to properly display links, when creating PR, the
858 # compare view and others uses different notation, and
858 # compare view and others uses different notation, and
859 # compare_commits.mako renders links based on the target_repo.
859 # compare_commits.mako renders links based on the target_repo.
860 # We need to swap that here to generate it properly on the html side
860 # We need to swap that here to generate it properly on the html side
861 c.target_repo = c.source_repo
861 c.target_repo = c.source_repo
862
862
863 # general comments
863 # general comments
864 c.comments = cc_model.get_comments(
864 c.comments = cc_model.get_comments(
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866
866
867 if c.allowed_to_update:
867 if c.allowed_to_update:
868 force_close = ('forced_closed', _('Close Pull Request'))
868 force_close = ('forced_closed', _('Close Pull Request'))
869 statuses = ChangesetStatus.STATUSES + [force_close]
869 statuses = ChangesetStatus.STATUSES + [force_close]
870 else:
870 else:
871 statuses = ChangesetStatus.STATUSES
871 statuses = ChangesetStatus.STATUSES
872 c.commit_statuses = statuses
872 c.commit_statuses = statuses
873
873
874 c.ancestor = None # TODO: add ancestor here
874 c.ancestor = None # TODO: add ancestor here
875 c.pull_request = pull_request_display_obj
875 c.pull_request = pull_request_display_obj
876 c.pull_request_latest = pull_request_latest
876 c.pull_request_latest = pull_request_latest
877 c.at_version = at_version
877 c.at_version = at_version
878
878
879 c.changes = None
879 c.changes = None
880 c.file_changes = None
880 c.file_changes = None
881
881
882 c.show_version_changes = 1 # control flag, not used yet
882 c.show_version_changes = 1 # control flag, not used yet
883
883
884 if at_version and c.show_version_changes:
884 if at_version and c.show_version_changes:
885 c.changes, c.file_changes = self._get_pr_version_changes(
885 c.changes, c.file_changes = self._get_pr_version_changes(
886 version, pull_request_latest)
886 version, pull_request_latest)
887
887
888 return render('/pullrequests/pullrequest_show.mako')
888 return render('/pullrequests/pullrequest_show.mako')
889
889
890 @LoginRequired()
890 @LoginRequired()
891 @NotAnonymous()
891 @NotAnonymous()
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 'repository.admin')
893 'repository.admin')
894 @auth.CSRFRequired()
894 @auth.CSRFRequired()
895 @jsonify
895 @jsonify
896 def comment(self, repo_name, pull_request_id):
896 def comment(self, repo_name, pull_request_id):
897 pull_request_id = safe_int(pull_request_id)
897 pull_request_id = safe_int(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
899 if pull_request.is_closed():
899 if pull_request.is_closed():
900 raise HTTPForbidden()
900 raise HTTPForbidden()
901
901
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 # as a changeset status, still we want to send it in one value.
903 # as a changeset status, still we want to send it in one value.
904 status = request.POST.get('changeset_status', None)
904 status = request.POST.get('changeset_status', None)
905 text = request.POST.get('text')
905 text = request.POST.get('text')
906 if status and '_closed' in status:
906 if status and '_closed' in status:
907 close_pr = True
907 close_pr = True
908 status = status.replace('_closed', '')
908 status = status.replace('_closed', '')
909 else:
909 else:
910 close_pr = False
910 close_pr = False
911
911
912 forced = (status == 'forced')
912 forced = (status == 'forced')
913 if forced:
913 if forced:
914 status = 'rejected'
914 status = 'rejected'
915
915
916 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 allowed_to_change_status = PullRequestModel().check_user_change_status(
917 pull_request, c.rhodecode_user)
917 pull_request, c.rhodecode_user)
918
918
919 if status and allowed_to_change_status:
919 if status and allowed_to_change_status:
920 message = (_('Status change %(transition_icon)s %(status)s')
920 message = (_('Status change %(transition_icon)s %(status)s')
921 % {'transition_icon': '>',
921 % {'transition_icon': '>',
922 'status': ChangesetStatus.get_status_lbl(status)})
922 'status': ChangesetStatus.get_status_lbl(status)})
923 if close_pr:
923 if close_pr:
924 message = _('Closing with') + ' ' + message
924 message = _('Closing with') + ' ' + message
925 text = text or message
925 text = text or message
926 comm = ChangesetCommentsModel().create(
926 comm = CommentsModel().create(
927 text=text,
927 text=text,
928 repo=c.rhodecode_db_repo.repo_id,
928 repo=c.rhodecode_db_repo.repo_id,
929 user=c.rhodecode_user.user_id,
929 user=c.rhodecode_user.user_id,
930 pull_request=pull_request_id,
930 pull_request=pull_request_id,
931 f_path=request.POST.get('f_path'),
931 f_path=request.POST.get('f_path'),
932 line_no=request.POST.get('line'),
932 line_no=request.POST.get('line'),
933 status_change=(ChangesetStatus.get_status_lbl(status)
933 status_change=(ChangesetStatus.get_status_lbl(status)
934 if status and allowed_to_change_status else None),
934 if status and allowed_to_change_status else None),
935 status_change_type=(status
935 status_change_type=(status
936 if status and allowed_to_change_status else None),
936 if status and allowed_to_change_status else None),
937 closing_pr=close_pr
937 closing_pr=close_pr
938 )
938 )
939
939
940 if allowed_to_change_status:
940 if allowed_to_change_status:
941 old_calculated_status = pull_request.calculated_review_status()
941 old_calculated_status = pull_request.calculated_review_status()
942 # get status if set !
942 # get status if set !
943 if status:
943 if status:
944 ChangesetStatusModel().set_status(
944 ChangesetStatusModel().set_status(
945 c.rhodecode_db_repo.repo_id,
945 c.rhodecode_db_repo.repo_id,
946 status,
946 status,
947 c.rhodecode_user.user_id,
947 c.rhodecode_user.user_id,
948 comm,
948 comm,
949 pull_request=pull_request_id
949 pull_request=pull_request_id
950 )
950 )
951
951
952 Session().flush()
952 Session().flush()
953 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
953 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
954 # we now calculate the status of pull request, and based on that
954 # we now calculate the status of pull request, and based on that
955 # calculation we set the commits status
955 # calculation we set the commits status
956 calculated_status = pull_request.calculated_review_status()
956 calculated_status = pull_request.calculated_review_status()
957 if old_calculated_status != calculated_status:
957 if old_calculated_status != calculated_status:
958 PullRequestModel()._trigger_pull_request_hook(
958 PullRequestModel()._trigger_pull_request_hook(
959 pull_request, c.rhodecode_user, 'review_status_change')
959 pull_request, c.rhodecode_user, 'review_status_change')
960
960
961 calculated_status_lbl = ChangesetStatus.get_status_lbl(
961 calculated_status_lbl = ChangesetStatus.get_status_lbl(
962 calculated_status)
962 calculated_status)
963
963
964 if close_pr:
964 if close_pr:
965 status_completed = (
965 status_completed = (
966 calculated_status in [ChangesetStatus.STATUS_APPROVED,
966 calculated_status in [ChangesetStatus.STATUS_APPROVED,
967 ChangesetStatus.STATUS_REJECTED])
967 ChangesetStatus.STATUS_REJECTED])
968 if forced or status_completed:
968 if forced or status_completed:
969 PullRequestModel().close_pull_request(
969 PullRequestModel().close_pull_request(
970 pull_request_id, c.rhodecode_user)
970 pull_request_id, c.rhodecode_user)
971 else:
971 else:
972 h.flash(_('Closing pull request on other statuses than '
972 h.flash(_('Closing pull request on other statuses than '
973 'rejected or approved is forbidden. '
973 'rejected or approved is forbidden. '
974 'Calculated status from all reviewers '
974 'Calculated status from all reviewers '
975 'is currently: %s') % calculated_status_lbl,
975 'is currently: %s') % calculated_status_lbl,
976 category='warning')
976 category='warning')
977
977
978 Session().commit()
978 Session().commit()
979
979
980 if not request.is_xhr:
980 if not request.is_xhr:
981 return redirect(h.url('pullrequest_show', repo_name=repo_name,
981 return redirect(h.url('pullrequest_show', repo_name=repo_name,
982 pull_request_id=pull_request_id))
982 pull_request_id=pull_request_id))
983
983
984 data = {
984 data = {
985 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
985 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
986 }
986 }
987 if comm:
987 if comm:
988 c.co = comm
988 c.co = comm
989 c.inline_comment = True if comm.line_no else False
989 c.inline_comment = True if comm.line_no else False
990 data.update(comm.get_dict())
990 data.update(comm.get_dict())
991 data.update({'rendered_text':
991 data.update({'rendered_text':
992 render('changeset/changeset_comment_block.mako')})
992 render('changeset/changeset_comment_block.mako')})
993
993
994 return data
994 return data
995
995
996 @LoginRequired()
996 @LoginRequired()
997 @NotAnonymous()
997 @NotAnonymous()
998 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
998 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
999 'repository.admin')
999 'repository.admin')
1000 @auth.CSRFRequired()
1000 @auth.CSRFRequired()
1001 @jsonify
1001 @jsonify
1002 def delete_comment(self, repo_name, comment_id):
1002 def delete_comment(self, repo_name, comment_id):
1003 return self._delete_comment(comment_id)
1003 return self._delete_comment(comment_id)
1004
1004
1005 def _delete_comment(self, comment_id):
1005 def _delete_comment(self, comment_id):
1006 comment_id = safe_int(comment_id)
1006 comment_id = safe_int(comment_id)
1007 co = ChangesetComment.get_or_404(comment_id)
1007 co = ChangesetComment.get_or_404(comment_id)
1008 if co.pull_request.is_closed():
1008 if co.pull_request.is_closed():
1009 # don't allow deleting comments on closed pull request
1009 # don't allow deleting comments on closed pull request
1010 raise HTTPForbidden()
1010 raise HTTPForbidden()
1011
1011
1012 is_owner = co.author.user_id == c.rhodecode_user.user_id
1012 is_owner = co.author.user_id == c.rhodecode_user.user_id
1013 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1013 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1014 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1014 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1015 old_calculated_status = co.pull_request.calculated_review_status()
1015 old_calculated_status = co.pull_request.calculated_review_status()
1016 ChangesetCommentsModel().delete(comment=co)
1016 CommentsModel().delete(comment=co)
1017 Session().commit()
1017 Session().commit()
1018 calculated_status = co.pull_request.calculated_review_status()
1018 calculated_status = co.pull_request.calculated_review_status()
1019 if old_calculated_status != calculated_status:
1019 if old_calculated_status != calculated_status:
1020 PullRequestModel()._trigger_pull_request_hook(
1020 PullRequestModel()._trigger_pull_request_hook(
1021 co.pull_request, c.rhodecode_user, 'review_status_change')
1021 co.pull_request, c.rhodecode_user, 'review_status_change')
1022 return True
1022 return True
1023 else:
1023 else:
1024 raise HTTPForbidden()
1024 raise HTTPForbidden()
@@ -1,131 +1,131 b''
1 # Copyright (C) 2016-2017 RhodeCode GmbH
1 # Copyright (C) 2016-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
19
20 from rhodecode.translation import lazy_ugettext
20 from rhodecode.translation import lazy_ugettext
21 from rhodecode.events.repo import (
21 from rhodecode.events.repo import (
22 RepoEvent, _commits_as_dict, _issues_as_dict)
22 RepoEvent, _commits_as_dict, _issues_as_dict)
23
23
24
24
25 class PullRequestEvent(RepoEvent):
25 class PullRequestEvent(RepoEvent):
26 """
26 """
27 Base class for pull request events.
27 Base class for pull request events.
28
28
29 :param pullrequest: a :class:`PullRequest` instance
29 :param pullrequest: a :class:`PullRequest` instance
30 """
30 """
31
31
32 def __init__(self, pullrequest):
32 def __init__(self, pullrequest):
33 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
33 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
34 self.pullrequest = pullrequest
34 self.pullrequest = pullrequest
35
35
36 def as_dict(self):
36 def as_dict(self):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 data = super(PullRequestEvent, self).as_dict()
38 data = super(PullRequestEvent, self).as_dict()
39
39
40 commits = _commits_as_dict(
40 commits = _commits_as_dict(
41 commit_ids=self.pullrequest.revisions,
41 commit_ids=self.pullrequest.revisions,
42 repos=[self.pullrequest.source_repo]
42 repos=[self.pullrequest.source_repo]
43 )
43 )
44 issues = _issues_as_dict(commits)
44 issues = _issues_as_dict(commits)
45
45
46 data.update({
46 data.update({
47 'pullrequest': {
47 'pullrequest': {
48 'title': self.pullrequest.title,
48 'title': self.pullrequest.title,
49 'issues': issues,
49 'issues': issues,
50 'pull_request_id': self.pullrequest.pull_request_id,
50 'pull_request_id': self.pullrequest.pull_request_id,
51 'url': PullRequestModel().get_url(self.pullrequest),
51 'url': PullRequestModel().get_url(self.pullrequest),
52 'status': self.pullrequest.calculated_review_status(),
52 'status': self.pullrequest.calculated_review_status(),
53 'commits': commits,
53 'commits': commits,
54 }
54 }
55 })
55 })
56 return data
56 return data
57
57
58
58
59 class PullRequestCreateEvent(PullRequestEvent):
59 class PullRequestCreateEvent(PullRequestEvent):
60 """
60 """
61 An instance of this class is emitted as an :term:`event` after a pull
61 An instance of this class is emitted as an :term:`event` after a pull
62 request is created.
62 request is created.
63 """
63 """
64 name = 'pullrequest-create'
64 name = 'pullrequest-create'
65 display_name = lazy_ugettext('pullrequest created')
65 display_name = lazy_ugettext('pullrequest created')
66
66
67
67
68 class PullRequestCloseEvent(PullRequestEvent):
68 class PullRequestCloseEvent(PullRequestEvent):
69 """
69 """
70 An instance of this class is emitted as an :term:`event` after a pull
70 An instance of this class is emitted as an :term:`event` after a pull
71 request is closed.
71 request is closed.
72 """
72 """
73 name = 'pullrequest-close'
73 name = 'pullrequest-close'
74 display_name = lazy_ugettext('pullrequest closed')
74 display_name = lazy_ugettext('pullrequest closed')
75
75
76
76
77 class PullRequestUpdateEvent(PullRequestEvent):
77 class PullRequestUpdateEvent(PullRequestEvent):
78 """
78 """
79 An instance of this class is emitted as an :term:`event` after a pull
79 An instance of this class is emitted as an :term:`event` after a pull
80 request's commits have been updated.
80 request's commits have been updated.
81 """
81 """
82 name = 'pullrequest-update'
82 name = 'pullrequest-update'
83 display_name = lazy_ugettext('pullrequest commits updated')
83 display_name = lazy_ugettext('pullrequest commits updated')
84
84
85
85
86 class PullRequestReviewEvent(PullRequestEvent):
86 class PullRequestReviewEvent(PullRequestEvent):
87 """
87 """
88 An instance of this class is emitted as an :term:`event` after a pull
88 An instance of this class is emitted as an :term:`event` after a pull
89 request review has changed.
89 request review has changed.
90 """
90 """
91 name = 'pullrequest-review'
91 name = 'pullrequest-review'
92 display_name = lazy_ugettext('pullrequest review changed')
92 display_name = lazy_ugettext('pullrequest review changed')
93
93
94
94
95 class PullRequestMergeEvent(PullRequestEvent):
95 class PullRequestMergeEvent(PullRequestEvent):
96 """
96 """
97 An instance of this class is emitted as an :term:`event` after a pull
97 An instance of this class is emitted as an :term:`event` after a pull
98 request is merged.
98 request is merged.
99 """
99 """
100 name = 'pullrequest-merge'
100 name = 'pullrequest-merge'
101 display_name = lazy_ugettext('pullrequest merged')
101 display_name = lazy_ugettext('pullrequest merged')
102
102
103
103
104 class PullRequestCommentEvent(PullRequestEvent):
104 class PullRequestCommentEvent(PullRequestEvent):
105 """
105 """
106 An instance of this class is emitted as an :term:`event` after a pull
106 An instance of this class is emitted as an :term:`event` after a pull
107 request comment is created.
107 request comment is created.
108 """
108 """
109 name = 'pullrequest-comment'
109 name = 'pullrequest-comment'
110 display_name = lazy_ugettext('pullrequest commented')
110 display_name = lazy_ugettext('pullrequest commented')
111
111
112 def __init__(self, pullrequest, comment):
112 def __init__(self, pullrequest, comment):
113 super(PullRequestCommentEvent, self).__init__(pullrequest)
113 super(PullRequestCommentEvent, self).__init__(pullrequest)
114 self.comment = comment
114 self.comment = comment
115
115
116 def as_dict(self):
116 def as_dict(self):
117 from rhodecode.model.comment import ChangesetCommentsModel
117 from rhodecode.model.comment import CommentsModel
118 data = super(PullRequestCommentEvent, self).as_dict()
118 data = super(PullRequestCommentEvent, self).as_dict()
119
119
120 status = None
120 status = None
121 if self.comment.status_change:
121 if self.comment.status_change:
122 status = self.comment.status_change[0].status
122 status = self.comment.status_change[0].status
123
123
124 data.update({
124 data.update({
125 'comment': {
125 'comment': {
126 'status': status,
126 'status': status,
127 'text': self.comment.text,
127 'text': self.comment.text,
128 'url': ChangesetCommentsModel().get_url(self.comment)
128 'url': CommentsModel().get_url(self.comment)
129 }
129 }
130 })
130 })
131 return data
131 return data
@@ -1,268 +1,268 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, 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 ChangesetCommentsModel
204 from rhodecode.model.comment import CommentsModel
205 comment = ChangesetCommentsModel().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 st = commit_statuses.get(o.user.username, None)
252 if st:
252 if st:
253 st = [(x, list(y)[0])
253 st = [(x, list(y)[0])
254 for x, y in (itertools.groupby(sorted(st, key=version),
254 for x, y in (itertools.groupby(sorted(st, key=version),
255 version))]
255 version))]
256
256
257 pull_request_reviewers.append((o.user, o.reasons, st))
257 pull_request_reviewers.append((o.user, o.reasons, st))
258 return pull_request_reviewers
258 return pull_request_reviewers
259
259
260 def calculated_review_status(self, pull_request, reviewers_statuses=None):
260 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 """
261 """
262 calculate pull request status based on reviewers, it should be a list
262 calculate pull request status based on reviewers, it should be a list
263 of two element lists.
263 of two element lists.
264
264
265 :param reviewers_statuses:
265 :param reviewers_statuses:
266 """
266 """
267 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
267 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 return self.calculate_status(reviewers)
268 return self.calculate_status(reviewers)
@@ -1,530 +1,530 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 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest)
42 ChangesetComment, User, Notification, PullRequest)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class ChangesetCommentsModel(BaseModel):
51 class CommentsModel(BaseModel):
52
52
53 cls = ChangesetComment
53 cls = ChangesetComment
54
54
55 DIFF_CONTEXT_BEFORE = 3
55 DIFF_CONTEXT_BEFORE = 3
56 DIFF_CONTEXT_AFTER = 3
56 DIFF_CONTEXT_AFTER = 3
57
57
58 def __get_commit_comment(self, changeset_comment):
58 def __get_commit_comment(self, changeset_comment):
59 return self._get_instance(ChangesetComment, changeset_comment)
59 return self._get_instance(ChangesetComment, changeset_comment)
60
60
61 def __get_pull_request(self, pull_request):
61 def __get_pull_request(self, pull_request):
62 return self._get_instance(PullRequest, pull_request)
62 return self._get_instance(PullRequest, pull_request)
63
63
64 def _extract_mentions(self, s):
64 def _extract_mentions(self, s):
65 user_objects = []
65 user_objects = []
66 for username in extract_mentioned_users(s):
66 for username in extract_mentioned_users(s):
67 user_obj = User.get_by_username(username, case_insensitive=True)
67 user_obj = User.get_by_username(username, case_insensitive=True)
68 if user_obj:
68 if user_obj:
69 user_objects.append(user_obj)
69 user_objects.append(user_obj)
70 return user_objects
70 return user_objects
71
71
72 def _get_renderer(self, global_renderer='rst'):
72 def _get_renderer(self, global_renderer='rst'):
73 try:
73 try:
74 # try reading from visual context
74 # try reading from visual context
75 from pylons import tmpl_context
75 from pylons import tmpl_context
76 global_renderer = tmpl_context.visual.default_renderer
76 global_renderer = tmpl_context.visual.default_renderer
77 except AttributeError:
77 except AttributeError:
78 log.debug("Renderer not set, falling back "
78 log.debug("Renderer not set, falling back "
79 "to default renderer '%s'", global_renderer)
79 "to default renderer '%s'", global_renderer)
80 except Exception:
80 except Exception:
81 log.error(traceback.format_exc())
81 log.error(traceback.format_exc())
82 return global_renderer
82 return global_renderer
83
83
84 def create(self, text, repo, user, commit_id=None, pull_request=None,
84 def create(self, text, repo, user, commit_id=None, pull_request=None,
85 f_path=None, line_no=None, status_change=None, comment_type=None,
85 f_path=None, line_no=None, status_change=None, comment_type=None,
86 status_change_type=None, closing_pr=False,
86 status_change_type=None, closing_pr=False,
87 send_email=True, renderer=None):
87 send_email=True, renderer=None):
88 """
88 """
89 Creates new comment for commit or pull request.
89 Creates new comment for commit or pull request.
90 IF status_change is not none this comment is associated with a
90 IF status_change is not none this comment is associated with a
91 status change of commit or commit associated with pull request
91 status change of commit or commit associated with pull request
92
92
93 :param text:
93 :param text:
94 :param repo:
94 :param repo:
95 :param user:
95 :param user:
96 :param commit_id:
96 :param commit_id:
97 :param pull_request:
97 :param pull_request:
98 :param f_path:
98 :param f_path:
99 :param line_no:
99 :param line_no:
100 :param status_change: Label for status change
100 :param status_change: Label for status change
101 :param comment_type: Type of comment
101 :param comment_type: Type of comment
102 :param status_change_type: type of status change
102 :param status_change_type: type of status change
103 :param closing_pr:
103 :param closing_pr:
104 :param send_email:
104 :param send_email:
105 :param renderer: pick renderer for this comment
105 :param renderer: pick renderer for this comment
106 """
106 """
107 if not text:
107 if not text:
108 log.warning('Missing text for comment, skipping...')
108 log.warning('Missing text for comment, skipping...')
109 return
109 return
110
110
111 if not renderer:
111 if not renderer:
112 renderer = self._get_renderer()
112 renderer = self._get_renderer()
113
113
114 repo = self._get_repo(repo)
114 repo = self._get_repo(repo)
115 user = self._get_user(user)
115 user = self._get_user(user)
116 comment = ChangesetComment()
116 comment = ChangesetComment()
117 comment.renderer = renderer
117 comment.renderer = renderer
118 comment.repo = repo
118 comment.repo = repo
119 comment.author = user
119 comment.author = user
120 comment.text = text
120 comment.text = text
121 comment.f_path = f_path
121 comment.f_path = f_path
122 comment.line_no = line_no
122 comment.line_no = line_no
123
123
124 pull_request_id = pull_request
124 pull_request_id = pull_request
125
125
126 commit_obj = None
126 commit_obj = None
127 pull_request_obj = None
127 pull_request_obj = None
128
128
129 if commit_id:
129 if commit_id:
130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
131 # do a lookup, so we don't pass something bad here
131 # do a lookup, so we don't pass something bad here
132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
133 comment.revision = commit_obj.raw_id
133 comment.revision = commit_obj.raw_id
134
134
135 elif pull_request_id:
135 elif pull_request_id:
136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
137 pull_request_obj = self.__get_pull_request(pull_request_id)
137 pull_request_obj = self.__get_pull_request(pull_request_id)
138 comment.pull_request = pull_request_obj
138 comment.pull_request = pull_request_obj
139 else:
139 else:
140 raise Exception('Please specify commit or pull_request_id')
140 raise Exception('Please specify commit or pull_request_id')
141
141
142 Session().add(comment)
142 Session().add(comment)
143 Session().flush()
143 Session().flush()
144 kwargs = {
144 kwargs = {
145 'user': user,
145 'user': user,
146 'renderer_type': renderer,
146 'renderer_type': renderer,
147 'repo_name': repo.repo_name,
147 'repo_name': repo.repo_name,
148 'status_change': status_change,
148 'status_change': status_change,
149 'status_change_type': status_change_type,
149 'status_change_type': status_change_type,
150 'comment_body': text,
150 'comment_body': text,
151 'comment_file': f_path,
151 'comment_file': f_path,
152 'comment_line': line_no,
152 'comment_line': line_no,
153 }
153 }
154
154
155 if commit_obj:
155 if commit_obj:
156 recipients = ChangesetComment.get_users(
156 recipients = ChangesetComment.get_users(
157 revision=commit_obj.raw_id)
157 revision=commit_obj.raw_id)
158 # add commit author if it's in RhodeCode system
158 # add commit author if it's in RhodeCode system
159 cs_author = User.get_from_cs_author(commit_obj.author)
159 cs_author = User.get_from_cs_author(commit_obj.author)
160 if not cs_author:
160 if not cs_author:
161 # use repo owner if we cannot extract the author correctly
161 # use repo owner if we cannot extract the author correctly
162 cs_author = repo.user
162 cs_author = repo.user
163 recipients += [cs_author]
163 recipients += [cs_author]
164
164
165 commit_comment_url = self.get_url(comment)
165 commit_comment_url = self.get_url(comment)
166
166
167 target_repo_url = h.link_to(
167 target_repo_url = h.link_to(
168 repo.repo_name,
168 repo.repo_name,
169 h.url('summary_home',
169 h.url('summary_home',
170 repo_name=repo.repo_name, qualified=True))
170 repo_name=repo.repo_name, qualified=True))
171
171
172 # commit specifics
172 # commit specifics
173 kwargs.update({
173 kwargs.update({
174 'commit': commit_obj,
174 'commit': commit_obj,
175 'commit_message': commit_obj.message,
175 'commit_message': commit_obj.message,
176 'commit_target_repo': target_repo_url,
176 'commit_target_repo': target_repo_url,
177 'commit_comment_url': commit_comment_url,
177 'commit_comment_url': commit_comment_url,
178 })
178 })
179
179
180 elif pull_request_obj:
180 elif pull_request_obj:
181 # get the current participants of this pull request
181 # get the current participants of this pull request
182 recipients = ChangesetComment.get_users(
182 recipients = ChangesetComment.get_users(
183 pull_request_id=pull_request_obj.pull_request_id)
183 pull_request_id=pull_request_obj.pull_request_id)
184 # add pull request author
184 # add pull request author
185 recipients += [pull_request_obj.author]
185 recipients += [pull_request_obj.author]
186
186
187 # add the reviewers to notification
187 # add the reviewers to notification
188 recipients += [x.user for x in pull_request_obj.reviewers]
188 recipients += [x.user for x in pull_request_obj.reviewers]
189
189
190 pr_target_repo = pull_request_obj.target_repo
190 pr_target_repo = pull_request_obj.target_repo
191 pr_source_repo = pull_request_obj.source_repo
191 pr_source_repo = pull_request_obj.source_repo
192
192
193 pr_comment_url = h.url(
193 pr_comment_url = h.url(
194 'pullrequest_show',
194 'pullrequest_show',
195 repo_name=pr_target_repo.repo_name,
195 repo_name=pr_target_repo.repo_name,
196 pull_request_id=pull_request_obj.pull_request_id,
196 pull_request_id=pull_request_obj.pull_request_id,
197 anchor='comment-%s' % comment.comment_id,
197 anchor='comment-%s' % comment.comment_id,
198 qualified=True,)
198 qualified=True,)
199
199
200 # set some variables for email notification
200 # set some variables for email notification
201 pr_target_repo_url = h.url(
201 pr_target_repo_url = h.url(
202 'summary_home', repo_name=pr_target_repo.repo_name,
202 'summary_home', repo_name=pr_target_repo.repo_name,
203 qualified=True)
203 qualified=True)
204
204
205 pr_source_repo_url = h.url(
205 pr_source_repo_url = h.url(
206 'summary_home', repo_name=pr_source_repo.repo_name,
206 'summary_home', repo_name=pr_source_repo.repo_name,
207 qualified=True)
207 qualified=True)
208
208
209 # pull request specifics
209 # pull request specifics
210 kwargs.update({
210 kwargs.update({
211 'pull_request': pull_request_obj,
211 'pull_request': pull_request_obj,
212 'pr_id': pull_request_obj.pull_request_id,
212 'pr_id': pull_request_obj.pull_request_id,
213 'pr_target_repo': pr_target_repo,
213 'pr_target_repo': pr_target_repo,
214 'pr_target_repo_url': pr_target_repo_url,
214 'pr_target_repo_url': pr_target_repo_url,
215 'pr_source_repo': pr_source_repo,
215 'pr_source_repo': pr_source_repo,
216 'pr_source_repo_url': pr_source_repo_url,
216 'pr_source_repo_url': pr_source_repo_url,
217 'pr_comment_url': pr_comment_url,
217 'pr_comment_url': pr_comment_url,
218 'pr_closing': closing_pr,
218 'pr_closing': closing_pr,
219 })
219 })
220 if send_email:
220 if send_email:
221 # pre-generate the subject for notification itself
221 # pre-generate the subject for notification itself
222 (subject,
222 (subject,
223 _h, _e, # we don't care about those
223 _h, _e, # we don't care about those
224 body_plaintext) = EmailNotificationModel().render_email(
224 body_plaintext) = EmailNotificationModel().render_email(
225 notification_type, **kwargs)
225 notification_type, **kwargs)
226
226
227 mention_recipients = set(
227 mention_recipients = set(
228 self._extract_mentions(text)).difference(recipients)
228 self._extract_mentions(text)).difference(recipients)
229
229
230 # create notification objects, and emails
230 # create notification objects, and emails
231 NotificationModel().create(
231 NotificationModel().create(
232 created_by=user,
232 created_by=user,
233 notification_subject=subject,
233 notification_subject=subject,
234 notification_body=body_plaintext,
234 notification_body=body_plaintext,
235 notification_type=notification_type,
235 notification_type=notification_type,
236 recipients=recipients,
236 recipients=recipients,
237 mention_recipients=mention_recipients,
237 mention_recipients=mention_recipients,
238 email_kwargs=kwargs,
238 email_kwargs=kwargs,
239 )
239 )
240
240
241 action = (
241 action = (
242 'user_commented_pull_request:{}'.format(
242 'user_commented_pull_request:{}'.format(
243 comment.pull_request.pull_request_id)
243 comment.pull_request.pull_request_id)
244 if comment.pull_request
244 if comment.pull_request
245 else 'user_commented_revision:{}'.format(comment.revision)
245 else 'user_commented_revision:{}'.format(comment.revision)
246 )
246 )
247 action_logger(user, action, comment.repo)
247 action_logger(user, action, comment.repo)
248
248
249 registry = get_current_registry()
249 registry = get_current_registry()
250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
251 channelstream_config = rhodecode_plugins.get('channelstream', {})
251 channelstream_config = rhodecode_plugins.get('channelstream', {})
252 msg_url = ''
252 msg_url = ''
253 if commit_obj:
253 if commit_obj:
254 msg_url = commit_comment_url
254 msg_url = commit_comment_url
255 repo_name = repo.repo_name
255 repo_name = repo.repo_name
256 elif pull_request_obj:
256 elif pull_request_obj:
257 msg_url = pr_comment_url
257 msg_url = pr_comment_url
258 repo_name = pr_target_repo.repo_name
258 repo_name = pr_target_repo.repo_name
259
259
260 if channelstream_config.get('enabled'):
260 if channelstream_config.get('enabled'):
261 message = '<strong>{}</strong> {} - ' \
261 message = '<strong>{}</strong> {} - ' \
262 '<a onclick="window.location=\'{}\';' \
262 '<a onclick="window.location=\'{}\';' \
263 'window.location.reload()">' \
263 'window.location.reload()">' \
264 '<strong>{}</strong></a>'
264 '<strong>{}</strong></a>'
265 message = message.format(
265 message = message.format(
266 user.username, _('made a comment'), msg_url,
266 user.username, _('made a comment'), msg_url,
267 _('Show it now'))
267 _('Show it now'))
268 channel = '/repo${}$/pr/{}'.format(
268 channel = '/repo${}$/pr/{}'.format(
269 repo_name,
269 repo_name,
270 pull_request_id
270 pull_request_id
271 )
271 )
272 payload = {
272 payload = {
273 'type': 'message',
273 'type': 'message',
274 'timestamp': datetime.utcnow(),
274 'timestamp': datetime.utcnow(),
275 'user': 'system',
275 'user': 'system',
276 'exclude_users': [user.username],
276 'exclude_users': [user.username],
277 'channel': channel,
277 'channel': channel,
278 'message': {
278 'message': {
279 'message': message,
279 'message': message,
280 'level': 'info',
280 'level': 'info',
281 'topic': '/notifications'
281 'topic': '/notifications'
282 }
282 }
283 }
283 }
284 channelstream_request(channelstream_config, [payload],
284 channelstream_request(channelstream_config, [payload],
285 '/message', raise_exc=False)
285 '/message', raise_exc=False)
286
286
287 return comment
287 return comment
288
288
289 def delete(self, comment):
289 def delete(self, comment):
290 """
290 """
291 Deletes given comment
291 Deletes given comment
292
292
293 :param comment_id:
293 :param comment_id:
294 """
294 """
295 comment = self.__get_commit_comment(comment)
295 comment = self.__get_commit_comment(comment)
296 Session().delete(comment)
296 Session().delete(comment)
297
297
298 return comment
298 return comment
299
299
300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
301 q = ChangesetComment.query()\
301 q = ChangesetComment.query()\
302 .filter(ChangesetComment.repo_id == repo_id)
302 .filter(ChangesetComment.repo_id == repo_id)
303 if revision:
303 if revision:
304 q = q.filter(ChangesetComment.revision == revision)
304 q = q.filter(ChangesetComment.revision == revision)
305 elif pull_request:
305 elif pull_request:
306 pull_request = self.__get_pull_request(pull_request)
306 pull_request = self.__get_pull_request(pull_request)
307 q = q.filter(ChangesetComment.pull_request == pull_request)
307 q = q.filter(ChangesetComment.pull_request == pull_request)
308 else:
308 else:
309 raise Exception('Please specify commit or pull_request')
309 raise Exception('Please specify commit or pull_request')
310 q = q.order_by(ChangesetComment.created_on)
310 q = q.order_by(ChangesetComment.created_on)
311 return q.all()
311 return q.all()
312
312
313 def get_url(self, comment):
313 def get_url(self, comment):
314 comment = self.__get_commit_comment(comment)
314 comment = self.__get_commit_comment(comment)
315 if comment.pull_request:
315 if comment.pull_request:
316 return h.url(
316 return h.url(
317 'pullrequest_show',
317 'pullrequest_show',
318 repo_name=comment.pull_request.target_repo.repo_name,
318 repo_name=comment.pull_request.target_repo.repo_name,
319 pull_request_id=comment.pull_request.pull_request_id,
319 pull_request_id=comment.pull_request.pull_request_id,
320 anchor='comment-%s' % comment.comment_id,
320 anchor='comment-%s' % comment.comment_id,
321 qualified=True,)
321 qualified=True,)
322 else:
322 else:
323 return h.url(
323 return h.url(
324 'changeset_home',
324 'changeset_home',
325 repo_name=comment.repo.repo_name,
325 repo_name=comment.repo.repo_name,
326 revision=comment.revision,
326 revision=comment.revision,
327 anchor='comment-%s' % comment.comment_id,
327 anchor='comment-%s' % comment.comment_id,
328 qualified=True,)
328 qualified=True,)
329
329
330 def get_comments(self, repo_id, revision=None, pull_request=None):
330 def get_comments(self, repo_id, revision=None, pull_request=None):
331 """
331 """
332 Gets main comments based on revision or pull_request_id
332 Gets main comments based on revision or pull_request_id
333
333
334 :param repo_id:
334 :param repo_id:
335 :param revision:
335 :param revision:
336 :param pull_request:
336 :param pull_request:
337 """
337 """
338
338
339 q = ChangesetComment.query()\
339 q = ChangesetComment.query()\
340 .filter(ChangesetComment.repo_id == repo_id)\
340 .filter(ChangesetComment.repo_id == repo_id)\
341 .filter(ChangesetComment.line_no == None)\
341 .filter(ChangesetComment.line_no == None)\
342 .filter(ChangesetComment.f_path == None)
342 .filter(ChangesetComment.f_path == None)
343 if revision:
343 if revision:
344 q = q.filter(ChangesetComment.revision == revision)
344 q = q.filter(ChangesetComment.revision == revision)
345 elif pull_request:
345 elif pull_request:
346 pull_request = self.__get_pull_request(pull_request)
346 pull_request = self.__get_pull_request(pull_request)
347 q = q.filter(ChangesetComment.pull_request == pull_request)
347 q = q.filter(ChangesetComment.pull_request == pull_request)
348 else:
348 else:
349 raise Exception('Please specify commit or pull_request')
349 raise Exception('Please specify commit or pull_request')
350 q = q.order_by(ChangesetComment.created_on)
350 q = q.order_by(ChangesetComment.created_on)
351 return q.all()
351 return q.all()
352
352
353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
355 return self._group_comments_by_path_and_line_number(q)
355 return self._group_comments_by_path_and_line_number(q)
356
356
357 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
357 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
358 version=None, include_aggregates=False):
358 version=None, include_aggregates=False):
359 version_aggregates = collections.defaultdict(list)
359 version_aggregates = collections.defaultdict(list)
360 inline_cnt = 0
360 inline_cnt = 0
361 for fname, per_line_comments in inline_comments.iteritems():
361 for fname, per_line_comments in inline_comments.iteritems():
362 for lno, comments in per_line_comments.iteritems():
362 for lno, comments in per_line_comments.iteritems():
363 for comm in comments:
363 for comm in comments:
364 version_aggregates[comm.pull_request_version_id].append(comm)
364 version_aggregates[comm.pull_request_version_id].append(comm)
365 if not comm.outdated_at_version(version) and skip_outdated:
365 if not comm.outdated_at_version(version) and skip_outdated:
366 inline_cnt += 1
366 inline_cnt += 1
367
367
368 if include_aggregates:
368 if include_aggregates:
369 return inline_cnt, version_aggregates
369 return inline_cnt, version_aggregates
370 return inline_cnt
370 return inline_cnt
371
371
372 def get_outdated_comments(self, repo_id, pull_request):
372 def get_outdated_comments(self, repo_id, pull_request):
373 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
373 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
374 # of a pull request.
374 # of a pull request.
375 q = self._all_inline_comments_of_pull_request(pull_request)
375 q = self._all_inline_comments_of_pull_request(pull_request)
376 q = q.filter(
376 q = q.filter(
377 ChangesetComment.display_state ==
377 ChangesetComment.display_state ==
378 ChangesetComment.COMMENT_OUTDATED
378 ChangesetComment.COMMENT_OUTDATED
379 ).order_by(ChangesetComment.comment_id.asc())
379 ).order_by(ChangesetComment.comment_id.asc())
380
380
381 return self._group_comments_by_path_and_line_number(q)
381 return self._group_comments_by_path_and_line_number(q)
382
382
383 def _get_inline_comments_query(self, repo_id, revision, pull_request):
383 def _get_inline_comments_query(self, repo_id, revision, pull_request):
384 # TODO: johbo: Split this into two methods: One for PR and one for
384 # TODO: johbo: Split this into two methods: One for PR and one for
385 # commit.
385 # commit.
386 if revision:
386 if revision:
387 q = Session().query(ChangesetComment).filter(
387 q = Session().query(ChangesetComment).filter(
388 ChangesetComment.repo_id == repo_id,
388 ChangesetComment.repo_id == repo_id,
389 ChangesetComment.line_no != null(),
389 ChangesetComment.line_no != null(),
390 ChangesetComment.f_path != null(),
390 ChangesetComment.f_path != null(),
391 ChangesetComment.revision == revision)
391 ChangesetComment.revision == revision)
392
392
393 elif pull_request:
393 elif pull_request:
394 pull_request = self.__get_pull_request(pull_request)
394 pull_request = self.__get_pull_request(pull_request)
395 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
395 if not CommentsModel.use_outdated_comments(pull_request):
396 q = self._visible_inline_comments_of_pull_request(pull_request)
396 q = self._visible_inline_comments_of_pull_request(pull_request)
397 else:
397 else:
398 q = self._all_inline_comments_of_pull_request(pull_request)
398 q = self._all_inline_comments_of_pull_request(pull_request)
399
399
400 else:
400 else:
401 raise Exception('Please specify commit or pull_request_id')
401 raise Exception('Please specify commit or pull_request_id')
402 q = q.order_by(ChangesetComment.comment_id.asc())
402 q = q.order_by(ChangesetComment.comment_id.asc())
403 return q
403 return q
404
404
405 def _group_comments_by_path_and_line_number(self, q):
405 def _group_comments_by_path_and_line_number(self, q):
406 comments = q.all()
406 comments = q.all()
407 paths = collections.defaultdict(lambda: collections.defaultdict(list))
407 paths = collections.defaultdict(lambda: collections.defaultdict(list))
408 for co in comments:
408 for co in comments:
409 paths[co.f_path][co.line_no].append(co)
409 paths[co.f_path][co.line_no].append(co)
410 return paths
410 return paths
411
411
412 @classmethod
412 @classmethod
413 def needed_extra_diff_context(cls):
413 def needed_extra_diff_context(cls):
414 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
414 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
415
415
416 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
416 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
417 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
417 if not CommentsModel.use_outdated_comments(pull_request):
418 return
418 return
419
419
420 comments = self._visible_inline_comments_of_pull_request(pull_request)
420 comments = self._visible_inline_comments_of_pull_request(pull_request)
421 comments_to_outdate = comments.all()
421 comments_to_outdate = comments.all()
422
422
423 for comment in comments_to_outdate:
423 for comment in comments_to_outdate:
424 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
424 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
425
425
426 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
426 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
427 diff_line = _parse_comment_line_number(comment.line_no)
427 diff_line = _parse_comment_line_number(comment.line_no)
428
428
429 try:
429 try:
430 old_context = old_diff_proc.get_context_of_line(
430 old_context = old_diff_proc.get_context_of_line(
431 path=comment.f_path, diff_line=diff_line)
431 path=comment.f_path, diff_line=diff_line)
432 new_context = new_diff_proc.get_context_of_line(
432 new_context = new_diff_proc.get_context_of_line(
433 path=comment.f_path, diff_line=diff_line)
433 path=comment.f_path, diff_line=diff_line)
434 except (diffs.LineNotInDiffException,
434 except (diffs.LineNotInDiffException,
435 diffs.FileNotInDiffException):
435 diffs.FileNotInDiffException):
436 comment.display_state = ChangesetComment.COMMENT_OUTDATED
436 comment.display_state = ChangesetComment.COMMENT_OUTDATED
437 return
437 return
438
438
439 if old_context == new_context:
439 if old_context == new_context:
440 return
440 return
441
441
442 if self._should_relocate_diff_line(diff_line):
442 if self._should_relocate_diff_line(diff_line):
443 new_diff_lines = new_diff_proc.find_context(
443 new_diff_lines = new_diff_proc.find_context(
444 path=comment.f_path, context=old_context,
444 path=comment.f_path, context=old_context,
445 offset=self.DIFF_CONTEXT_BEFORE)
445 offset=self.DIFF_CONTEXT_BEFORE)
446 if not new_diff_lines:
446 if not new_diff_lines:
447 comment.display_state = ChangesetComment.COMMENT_OUTDATED
447 comment.display_state = ChangesetComment.COMMENT_OUTDATED
448 else:
448 else:
449 new_diff_line = self._choose_closest_diff_line(
449 new_diff_line = self._choose_closest_diff_line(
450 diff_line, new_diff_lines)
450 diff_line, new_diff_lines)
451 comment.line_no = _diff_to_comment_line_number(new_diff_line)
451 comment.line_no = _diff_to_comment_line_number(new_diff_line)
452 else:
452 else:
453 comment.display_state = ChangesetComment.COMMENT_OUTDATED
453 comment.display_state = ChangesetComment.COMMENT_OUTDATED
454
454
455 def _should_relocate_diff_line(self, diff_line):
455 def _should_relocate_diff_line(self, diff_line):
456 """
456 """
457 Checks if relocation shall be tried for the given `diff_line`.
457 Checks if relocation shall be tried for the given `diff_line`.
458
458
459 If a comment points into the first lines, then we can have a situation
459 If a comment points into the first lines, then we can have a situation
460 that after an update another line has been added on top. In this case
460 that after an update another line has been added on top. In this case
461 we would find the context still and move the comment around. This
461 we would find the context still and move the comment around. This
462 would be wrong.
462 would be wrong.
463 """
463 """
464 should_relocate = (
464 should_relocate = (
465 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
465 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
466 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
466 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
467 return should_relocate
467 return should_relocate
468
468
469 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
469 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
470 candidate = new_diff_lines[0]
470 candidate = new_diff_lines[0]
471 best_delta = _diff_line_delta(diff_line, candidate)
471 best_delta = _diff_line_delta(diff_line, candidate)
472 for new_diff_line in new_diff_lines[1:]:
472 for new_diff_line in new_diff_lines[1:]:
473 delta = _diff_line_delta(diff_line, new_diff_line)
473 delta = _diff_line_delta(diff_line, new_diff_line)
474 if delta < best_delta:
474 if delta < best_delta:
475 candidate = new_diff_line
475 candidate = new_diff_line
476 best_delta = delta
476 best_delta = delta
477 return candidate
477 return candidate
478
478
479 def _visible_inline_comments_of_pull_request(self, pull_request):
479 def _visible_inline_comments_of_pull_request(self, pull_request):
480 comments = self._all_inline_comments_of_pull_request(pull_request)
480 comments = self._all_inline_comments_of_pull_request(pull_request)
481 comments = comments.filter(
481 comments = comments.filter(
482 coalesce(ChangesetComment.display_state, '') !=
482 coalesce(ChangesetComment.display_state, '') !=
483 ChangesetComment.COMMENT_OUTDATED)
483 ChangesetComment.COMMENT_OUTDATED)
484 return comments
484 return comments
485
485
486 def _all_inline_comments_of_pull_request(self, pull_request):
486 def _all_inline_comments_of_pull_request(self, pull_request):
487 comments = Session().query(ChangesetComment)\
487 comments = Session().query(ChangesetComment)\
488 .filter(ChangesetComment.line_no != None)\
488 .filter(ChangesetComment.line_no != None)\
489 .filter(ChangesetComment.f_path != None)\
489 .filter(ChangesetComment.f_path != None)\
490 .filter(ChangesetComment.pull_request == pull_request)
490 .filter(ChangesetComment.pull_request == pull_request)
491 return comments
491 return comments
492
492
493 @staticmethod
493 @staticmethod
494 def use_outdated_comments(pull_request):
494 def use_outdated_comments(pull_request):
495 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
495 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
496 settings = settings_model.get_general_settings()
496 settings = settings_model.get_general_settings()
497 return settings.get('rhodecode_use_outdated_comments', False)
497 return settings.get('rhodecode_use_outdated_comments', False)
498
498
499
499
500 def _parse_comment_line_number(line_no):
500 def _parse_comment_line_number(line_no):
501 """
501 """
502 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
502 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
503 """
503 """
504 old_line = None
504 old_line = None
505 new_line = None
505 new_line = None
506 if line_no.startswith('o'):
506 if line_no.startswith('o'):
507 old_line = int(line_no[1:])
507 old_line = int(line_no[1:])
508 elif line_no.startswith('n'):
508 elif line_no.startswith('n'):
509 new_line = int(line_no[1:])
509 new_line = int(line_no[1:])
510 else:
510 else:
511 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
511 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
512 return diffs.DiffLineNumber(old_line, new_line)
512 return diffs.DiffLineNumber(old_line, new_line)
513
513
514
514
515 def _diff_to_comment_line_number(diff_line):
515 def _diff_to_comment_line_number(diff_line):
516 if diff_line.new is not None:
516 if diff_line.new is not None:
517 return u'n{}'.format(diff_line.new)
517 return u'n{}'.format(diff_line.new)
518 elif diff_line.old is not None:
518 elif diff_line.old is not None:
519 return u'o{}'.format(diff_line.old)
519 return u'o{}'.format(diff_line.old)
520 return u''
520 return u''
521
521
522
522
523 def _diff_line_delta(a, b):
523 def _diff_line_delta(a, b):
524 if None not in (a.new, b.new):
524 if None not in (a.new, b.new):
525 return abs(a.new - b.new)
525 return abs(a.new - b.new)
526 elif None not in (a.old, b.old):
526 elif None not in (a.old, b.old):
527 return abs(a.old - b.old)
527 return abs(a.old - b.old)
528 else:
528 else:
529 raise ValueError(
529 raise ValueError(
530 "Cannot compute delta between {} and {}".format(a, b))
530 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1318 +1,1318 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 ChangesetCommentsModel
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)
53 PullRequestVersion, ChangesetComment)
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(
66 UpdateResponse = namedtuple(
67 'UpdateResponse', 'executed, reason, new, old, changes')
67 'UpdateResponse', 'executed, reason, new, old, changes')
68
68
69
69
70 class PullRequestModel(BaseModel):
70 class PullRequestModel(BaseModel):
71
71
72 cls = PullRequest
72 cls = PullRequest
73
73
74 DIFF_CONTEXT = 3
74 DIFF_CONTEXT = 3
75
75
76 MERGE_STATUS_MESSAGES = {
76 MERGE_STATUS_MESSAGES = {
77 MergeFailureReason.NONE: lazy_ugettext(
77 MergeFailureReason.NONE: lazy_ugettext(
78 'This pull request can be automatically merged.'),
78 'This pull request can be automatically merged.'),
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 'This pull request cannot be merged because of an unhandled'
80 'This pull request cannot be merged because of an unhandled'
81 ' exception.'),
81 ' exception.'),
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 'This pull request cannot be merged because of conflicts.'),
83 'This pull request cannot be merged because of conflicts.'),
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 'This pull request could not be merged because push to target'
85 'This pull request could not be merged because push to target'
86 ' failed.'),
86 ' failed.'),
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 'This pull request cannot be merged because the target is not a'
88 'This pull request cannot be merged because the target is not a'
89 ' head.'),
89 ' head.'),
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 'This pull request cannot be merged because the source contains'
91 'This pull request cannot be merged because the source contains'
92 ' more branches than the target.'),
92 ' more branches than the target.'),
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 'This pull request cannot be merged because the target has'
94 'This pull request cannot be merged because the target has'
95 ' multiple heads.'),
95 ' multiple heads.'),
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 'This pull request cannot be merged because the target repository'
97 'This pull request cannot be merged because the target repository'
98 ' is locked.'),
98 ' is locked.'),
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 'This pull request cannot be merged because the target or the '
100 'This pull request cannot be merged because the target or the '
101 'source reference is missing.'),
101 'source reference is missing.'),
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 'This pull request cannot be merged because the target '
103 'This pull request cannot be merged because the target '
104 'reference is missing.'),
104 'reference is missing.'),
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 'This pull request cannot be merged because the source '
106 'This pull request cannot be merged because the source '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 'This pull request cannot be merged because of conflicts related '
109 'This pull request cannot be merged because of conflicts related '
110 'to sub repositories.'),
110 'to sub repositories.'),
111 }
111 }
112
112
113 UPDATE_STATUS_MESSAGES = {
113 UPDATE_STATUS_MESSAGES = {
114 UpdateFailureReason.NONE: lazy_ugettext(
114 UpdateFailureReason.NONE: lazy_ugettext(
115 'Pull request update successful.'),
115 'Pull request update successful.'),
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 'Pull request update failed because of an unknown error.'),
117 'Pull request update failed because of an unknown error.'),
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 'No update needed because the source reference is already '
119 'No update needed because the source reference is already '
120 'up to date.'),
120 'up to date.'),
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TPYE: 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.'),
123 'not supported for an update.'),
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 created_by_user = self._get_user(created_by)
419 created_by_user = self._get_user(created_by)
420 source_repo = self._get_repo(source_repo)
420 source_repo = self._get_repo(source_repo)
421 target_repo = self._get_repo(target_repo)
421 target_repo = self._get_repo(target_repo)
422
422
423 pull_request = PullRequest()
423 pull_request = PullRequest()
424 pull_request.source_repo = source_repo
424 pull_request.source_repo = source_repo
425 pull_request.source_ref = source_ref
425 pull_request.source_ref = source_ref
426 pull_request.target_repo = target_repo
426 pull_request.target_repo = target_repo
427 pull_request.target_ref = target_ref
427 pull_request.target_ref = target_ref
428 pull_request.revisions = revisions
428 pull_request.revisions = revisions
429 pull_request.title = title
429 pull_request.title = title
430 pull_request.description = description
430 pull_request.description = description
431 pull_request.author = created_by_user
431 pull_request.author = created_by_user
432
432
433 Session().add(pull_request)
433 Session().add(pull_request)
434 Session().flush()
434 Session().flush()
435
435
436 reviewer_ids = set()
436 reviewer_ids = set()
437 # members / reviewers
437 # members / reviewers
438 for reviewer_object in reviewers:
438 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
439 if isinstance(reviewer_object, tuple):
440 user_id, reasons = reviewer_object
440 user_id, reasons = reviewer_object
441 else:
441 else:
442 user_id, reasons = reviewer_object, []
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(user, pull_request, reasons)
448 Session().add(reviewer)
448 Session().add(reviewer)
449
449
450 # Set approval status to "Under Review" for all commits which are
450 # Set approval status to "Under Review" for all commits which are
451 # part of this pull request.
451 # part of this pull request.
452 ChangesetStatusModel().set_status(
452 ChangesetStatusModel().set_status(
453 repo=target_repo,
453 repo=target_repo,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 user=created_by_user,
455 user=created_by_user,
456 pull_request=pull_request
456 pull_request=pull_request
457 )
457 )
458
458
459 self.notify_reviewers(pull_request, reviewer_ids)
459 self.notify_reviewers(pull_request, reviewer_ids)
460 self._trigger_pull_request_hook(
460 self._trigger_pull_request_hook(
461 pull_request, created_by_user, 'create')
461 pull_request, created_by_user, 'create')
462
462
463 return pull_request
463 return pull_request
464
464
465 def _trigger_pull_request_hook(self, pull_request, user, action):
465 def _trigger_pull_request_hook(self, pull_request, user, action):
466 pull_request = self.__get_pull_request(pull_request)
466 pull_request = self.__get_pull_request(pull_request)
467 target_scm = pull_request.target_repo.scm_instance()
467 target_scm = pull_request.target_repo.scm_instance()
468 if action == 'create':
468 if action == 'create':
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 elif action == 'merge':
470 elif action == 'merge':
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 elif action == 'close':
472 elif action == 'close':
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 elif action == 'review_status_change':
474 elif action == 'review_status_change':
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 elif action == 'update':
476 elif action == 'update':
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 else:
478 else:
479 return
479 return
480
480
481 trigger_hook(
481 trigger_hook(
482 username=user.username,
482 username=user.username,
483 repo_name=pull_request.target_repo.repo_name,
483 repo_name=pull_request.target_repo.repo_name,
484 repo_alias=target_scm.alias,
484 repo_alias=target_scm.alias,
485 pull_request=pull_request)
485 pull_request=pull_request)
486
486
487 def _get_commit_ids(self, pull_request):
487 def _get_commit_ids(self, pull_request):
488 """
488 """
489 Return the commit ids of the merged pull request.
489 Return the commit ids of the merged pull request.
490
490
491 This method is not dealing correctly yet with the lack of autoupdates
491 This method is not dealing correctly yet with the lack of autoupdates
492 nor with the implicit target updates.
492 nor with the implicit target updates.
493 For example: if a commit in the source repo is already in the target it
493 For example: if a commit in the source repo is already in the target it
494 will be reported anyways.
494 will be reported anyways.
495 """
495 """
496 merge_rev = pull_request.merge_rev
496 merge_rev = pull_request.merge_rev
497 if merge_rev is None:
497 if merge_rev is None:
498 raise ValueError('This pull request was not merged yet')
498 raise ValueError('This pull request was not merged yet')
499
499
500 commit_ids = list(pull_request.revisions)
500 commit_ids = list(pull_request.revisions)
501 if merge_rev not in commit_ids:
501 if merge_rev not in commit_ids:
502 commit_ids.append(merge_rev)
502 commit_ids.append(merge_rev)
503
503
504 return commit_ids
504 return commit_ids
505
505
506 def merge(self, pull_request, user, extras):
506 def merge(self, pull_request, user, extras):
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
509 if merge_state.executed:
509 if merge_state.executed:
510 log.debug(
510 log.debug(
511 "Merge was successful, updating the pull request comments.")
511 "Merge was successful, updating the pull request comments.")
512 self._comment_and_close_pr(pull_request, user, merge_state)
512 self._comment_and_close_pr(pull_request, user, merge_state)
513 self._log_action('user_merged_pull_request', user, pull_request)
513 self._log_action('user_merged_pull_request', user, pull_request)
514 else:
514 else:
515 log.warn("Merge failed, not updating the pull request.")
515 log.warn("Merge failed, not updating the pull request.")
516 return merge_state
516 return merge_state
517
517
518 def _merge_pull_request(self, pull_request, user, extras):
518 def _merge_pull_request(self, pull_request, user, extras):
519 target_vcs = pull_request.target_repo.scm_instance()
519 target_vcs = pull_request.target_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
521 target_ref = self._refresh_reference(
521 target_ref = self._refresh_reference(
522 pull_request.target_ref_parts, target_vcs)
522 pull_request.target_ref_parts, target_vcs)
523
523
524 message = _(
524 message = _(
525 'Merge pull request #%(pr_id)s from '
525 'Merge pull request #%(pr_id)s from '
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 'pr_id': pull_request.pull_request_id,
527 'pr_id': pull_request.pull_request_id,
528 'source_repo': source_vcs.name,
528 'source_repo': source_vcs.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
530 'pr_title': pull_request.title
530 'pr_title': pull_request.title
531 }
531 }
532
532
533 workspace_id = self._workspace_id(pull_request)
533 workspace_id = self._workspace_id(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
535
535
536 callback_daemon, extras = prepare_callback_daemon(
536 callback_daemon, extras = prepare_callback_daemon(
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539
539
540 with callback_daemon:
540 with callback_daemon:
541 # TODO: johbo: Implement a clean way to run a config_override
541 # TODO: johbo: Implement a clean way to run a config_override
542 # for a single call.
542 # for a single call.
543 target_vcs.config.set(
543 target_vcs.config.set(
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 merge_state = target_vcs.merge(
545 merge_state = target_vcs.merge(
546 target_ref, source_vcs, pull_request.source_ref_parts,
546 target_ref, source_vcs, pull_request.source_ref_parts,
547 workspace_id, user_name=user.username,
547 workspace_id, user_name=user.username,
548 user_email=user.email, message=message, use_rebase=use_rebase)
548 user_email=user.email, message=message, use_rebase=use_rebase)
549 return merge_state
549 return merge_state
550
550
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 pull_request.updated_on = datetime.datetime.now()
553 pull_request.updated_on = datetime.datetime.now()
554
554
555 ChangesetCommentsModel().create(
555 CommentsModel().create(
556 text=unicode(_('Pull request merged and closed')),
556 text=unicode(_('Pull request merged and closed')),
557 repo=pull_request.target_repo.repo_id,
557 repo=pull_request.target_repo.repo_id,
558 user=user.user_id,
558 user=user.user_id,
559 pull_request=pull_request.pull_request_id,
559 pull_request=pull_request.pull_request_id,
560 f_path=None,
560 f_path=None,
561 line_no=None,
561 line_no=None,
562 closing_pr=True
562 closing_pr=True
563 )
563 )
564
564
565 Session().add(pull_request)
565 Session().add(pull_request)
566 Session().flush()
566 Session().flush()
567 # TODO: paris: replace invalidation with less radical solution
567 # TODO: paris: replace invalidation with less radical solution
568 ScmModel().mark_for_invalidation(
568 ScmModel().mark_for_invalidation(
569 pull_request.target_repo.repo_name)
569 pull_request.target_repo.repo_name)
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
571
571
572 def has_valid_update_type(self, pull_request):
572 def has_valid_update_type(self, pull_request):
573 source_ref_type = pull_request.source_ref_parts.type
573 source_ref_type = pull_request.source_ref_parts.type
574 return source_ref_type in ['book', 'branch', 'tag']
574 return source_ref_type in ['book', 'branch', 'tag']
575
575
576 def update_commits(self, pull_request):
576 def update_commits(self, pull_request):
577 """
577 """
578 Get the updated list of commits for the pull request
578 Get the updated list of commits for the pull request
579 and return the new pull request version and the list
579 and return the new pull request version and the list
580 of commits processed by this update action
580 of commits processed by this update action
581 """
581 """
582 pull_request = self.__get_pull_request(pull_request)
582 pull_request = self.__get_pull_request(pull_request)
583 source_ref_type = pull_request.source_ref_parts.type
583 source_ref_type = pull_request.source_ref_parts.type
584 source_ref_name = pull_request.source_ref_parts.name
584 source_ref_name = pull_request.source_ref_parts.name
585 source_ref_id = pull_request.source_ref_parts.commit_id
585 source_ref_id = pull_request.source_ref_parts.commit_id
586
586
587 if not self.has_valid_update_type(pull_request):
587 if not self.has_valid_update_type(pull_request):
588 log.debug(
588 log.debug(
589 "Skipping update of pull request %s due to ref type: %s",
589 "Skipping update of pull request %s due to ref type: %s",
590 pull_request, source_ref_type)
590 pull_request, source_ref_type)
591 return UpdateResponse(
591 return UpdateResponse(
592 executed=False,
592 executed=False,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
594 old=pull_request, new=None, changes=None)
594 old=pull_request, new=None, changes=None)
595
595
596 source_repo = pull_request.source_repo.scm_instance()
596 source_repo = pull_request.source_repo.scm_instance()
597 try:
597 try:
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
599 except CommitDoesNotExistError:
599 except CommitDoesNotExistError:
600 return UpdateResponse(
600 return UpdateResponse(
601 executed=False,
601 executed=False,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
603 old=pull_request, new=None, changes=None)
603 old=pull_request, new=None, changes=None)
604
604
605 if source_ref_id == source_commit.raw_id:
605 if source_ref_id == source_commit.raw_id:
606 log.debug("Nothing changed in pull request %s", pull_request)
606 log.debug("Nothing changed in pull request %s", pull_request)
607 return UpdateResponse(
607 return UpdateResponse(
608 executed=False,
608 executed=False,
609 reason=UpdateFailureReason.NO_CHANGE,
609 reason=UpdateFailureReason.NO_CHANGE,
610 old=pull_request, new=None, changes=None)
610 old=pull_request, new=None, changes=None)
611
611
612 # Finally there is a need for an update
612 # Finally there is a need for an update
613 pull_request_version = self._create_version_from_snapshot(pull_request)
613 pull_request_version = self._create_version_from_snapshot(pull_request)
614 self._link_comments_to_version(pull_request_version)
614 self._link_comments_to_version(pull_request_version)
615
615
616 target_ref_type = pull_request.target_ref_parts.type
616 target_ref_type = pull_request.target_ref_parts.type
617 target_ref_name = pull_request.target_ref_parts.name
617 target_ref_name = pull_request.target_ref_parts.name
618 target_ref_id = pull_request.target_ref_parts.commit_id
618 target_ref_id = pull_request.target_ref_parts.commit_id
619 target_repo = pull_request.target_repo.scm_instance()
619 target_repo = pull_request.target_repo.scm_instance()
620
620
621 try:
621 try:
622 if target_ref_type in ('tag', 'branch', 'book'):
622 if target_ref_type in ('tag', 'branch', 'book'):
623 target_commit = target_repo.get_commit(target_ref_name)
623 target_commit = target_repo.get_commit(target_ref_name)
624 else:
624 else:
625 target_commit = target_repo.get_commit(target_ref_id)
625 target_commit = target_repo.get_commit(target_ref_id)
626 except CommitDoesNotExistError:
626 except CommitDoesNotExistError:
627 return UpdateResponse(
627 return UpdateResponse(
628 executed=False,
628 executed=False,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
630 old=pull_request, new=None, changes=None)
630 old=pull_request, new=None, changes=None)
631
631
632 # re-compute commit ids
632 # re-compute commit ids
633 old_commit_ids = set(pull_request.revisions)
633 old_commit_ids = set(pull_request.revisions)
634 pre_load = ["author", "branch", "date", "message"]
634 pre_load = ["author", "branch", "date", "message"]
635 commit_ranges = target_repo.compare(
635 commit_ranges = target_repo.compare(
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
637 pre_load=pre_load)
637 pre_load=pre_load)
638
638
639 ancestor = target_repo.get_common_ancestor(
639 ancestor = target_repo.get_common_ancestor(
640 target_commit.raw_id, source_commit.raw_id, source_repo)
640 target_commit.raw_id, source_commit.raw_id, source_repo)
641
641
642 pull_request.source_ref = '%s:%s:%s' % (
642 pull_request.source_ref = '%s:%s:%s' % (
643 source_ref_type, source_ref_name, source_commit.raw_id)
643 source_ref_type, source_ref_name, source_commit.raw_id)
644 pull_request.target_ref = '%s:%s:%s' % (
644 pull_request.target_ref = '%s:%s:%s' % (
645 target_ref_type, target_ref_name, ancestor)
645 target_ref_type, target_ref_name, ancestor)
646 pull_request.revisions = [
646 pull_request.revisions = [
647 commit.raw_id for commit in reversed(commit_ranges)]
647 commit.raw_id for commit in reversed(commit_ranges)]
648 pull_request.updated_on = datetime.datetime.now()
648 pull_request.updated_on = datetime.datetime.now()
649 Session().add(pull_request)
649 Session().add(pull_request)
650 new_commit_ids = set(pull_request.revisions)
650 new_commit_ids = set(pull_request.revisions)
651
651
652 changes = self._calculate_commit_id_changes(
652 changes = self._calculate_commit_id_changes(
653 old_commit_ids, new_commit_ids)
653 old_commit_ids, new_commit_ids)
654
654
655 old_diff_data, new_diff_data = self._generate_update_diffs(
655 old_diff_data, new_diff_data = self._generate_update_diffs(
656 pull_request, pull_request_version)
656 pull_request, pull_request_version)
657
657
658 ChangesetCommentsModel().outdate_comments(
658 CommentsModel().outdate_comments(
659 pull_request, old_diff_data=old_diff_data,
659 pull_request, old_diff_data=old_diff_data,
660 new_diff_data=new_diff_data)
660 new_diff_data=new_diff_data)
661
661
662 file_changes = self._calculate_file_changes(
662 file_changes = self._calculate_file_changes(
663 old_diff_data, new_diff_data)
663 old_diff_data, new_diff_data)
664
664
665 # Add an automatic comment to the pull request
665 # Add an automatic comment to the pull request
666 update_comment = ChangesetCommentsModel().create(
666 update_comment = CommentsModel().create(
667 text=self._render_update_message(changes, file_changes),
667 text=self._render_update_message(changes, file_changes),
668 repo=pull_request.target_repo,
668 repo=pull_request.target_repo,
669 user=pull_request.author,
669 user=pull_request.author,
670 pull_request=pull_request,
670 pull_request=pull_request,
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
672
672
673 # Update status to "Under Review" for added commits
673 # Update status to "Under Review" for added commits
674 for commit_id in changes.added:
674 for commit_id in changes.added:
675 ChangesetStatusModel().set_status(
675 ChangesetStatusModel().set_status(
676 repo=pull_request.source_repo,
676 repo=pull_request.source_repo,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
678 comment=update_comment,
678 comment=update_comment,
679 user=pull_request.author,
679 user=pull_request.author,
680 pull_request=pull_request,
680 pull_request=pull_request,
681 revision=commit_id)
681 revision=commit_id)
682
682
683 log.debug(
683 log.debug(
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
685 'removed_ids: %s', pull_request.pull_request_id,
685 'removed_ids: %s', pull_request.pull_request_id,
686 changes.added, changes.common, changes.removed)
686 changes.added, changes.common, changes.removed)
687 log.debug('Updated pull request with the following file changes: %s',
687 log.debug('Updated pull request with the following file changes: %s',
688 file_changes)
688 file_changes)
689
689
690 log.info(
690 log.info(
691 "Updated pull request %s from commit %s to commit %s, "
691 "Updated pull request %s from commit %s to commit %s, "
692 "stored new version %s of this pull request.",
692 "stored new version %s of this pull request.",
693 pull_request.pull_request_id, source_ref_id,
693 pull_request.pull_request_id, source_ref_id,
694 pull_request.source_ref_parts.commit_id,
694 pull_request.source_ref_parts.commit_id,
695 pull_request_version.pull_request_version_id)
695 pull_request_version.pull_request_version_id)
696 Session().commit()
696 Session().commit()
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
698 'update')
698 'update')
699
699
700 return UpdateResponse(
700 return UpdateResponse(
701 executed=True, reason=UpdateFailureReason.NONE,
701 executed=True, reason=UpdateFailureReason.NONE,
702 old=pull_request, new=pull_request_version, changes=changes)
702 old=pull_request, new=pull_request_version, changes=changes)
703
703
704 def _create_version_from_snapshot(self, pull_request):
704 def _create_version_from_snapshot(self, pull_request):
705 version = PullRequestVersion()
705 version = PullRequestVersion()
706 version.title = pull_request.title
706 version.title = pull_request.title
707 version.description = pull_request.description
707 version.description = pull_request.description
708 version.status = pull_request.status
708 version.status = pull_request.status
709 version.created_on = datetime.datetime.now()
709 version.created_on = datetime.datetime.now()
710 version.updated_on = pull_request.updated_on
710 version.updated_on = pull_request.updated_on
711 version.user_id = pull_request.user_id
711 version.user_id = pull_request.user_id
712 version.source_repo = pull_request.source_repo
712 version.source_repo = pull_request.source_repo
713 version.source_ref = pull_request.source_ref
713 version.source_ref = pull_request.source_ref
714 version.target_repo = pull_request.target_repo
714 version.target_repo = pull_request.target_repo
715 version.target_ref = pull_request.target_ref
715 version.target_ref = pull_request.target_ref
716
716
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
719 version._last_merge_status = pull_request._last_merge_status
719 version._last_merge_status = pull_request._last_merge_status
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
721 version.merge_rev = pull_request.merge_rev
721 version.merge_rev = pull_request.merge_rev
722
722
723 version.revisions = pull_request.revisions
723 version.revisions = pull_request.revisions
724 version.pull_request = pull_request
724 version.pull_request = pull_request
725 Session().add(version)
725 Session().add(version)
726 Session().flush()
726 Session().flush()
727
727
728 return version
728 return version
729
729
730 def _generate_update_diffs(self, pull_request, pull_request_version):
730 def _generate_update_diffs(self, pull_request, pull_request_version):
731 diff_context = (
731 diff_context = (
732 self.DIFF_CONTEXT +
732 self.DIFF_CONTEXT +
733 ChangesetCommentsModel.needed_extra_diff_context())
733 CommentsModel.needed_extra_diff_context())
734 old_diff = self._get_diff_from_pr_or_version(
734 old_diff = self._get_diff_from_pr_or_version(
735 pull_request_version, context=diff_context)
735 pull_request_version, context=diff_context)
736 new_diff = self._get_diff_from_pr_or_version(
736 new_diff = self._get_diff_from_pr_or_version(
737 pull_request, context=diff_context)
737 pull_request, context=diff_context)
738
738
739 old_diff_data = diffs.DiffProcessor(old_diff)
739 old_diff_data = diffs.DiffProcessor(old_diff)
740 old_diff_data.prepare()
740 old_diff_data.prepare()
741 new_diff_data = diffs.DiffProcessor(new_diff)
741 new_diff_data = diffs.DiffProcessor(new_diff)
742 new_diff_data.prepare()
742 new_diff_data.prepare()
743
743
744 return old_diff_data, new_diff_data
744 return old_diff_data, new_diff_data
745
745
746 def _link_comments_to_version(self, pull_request_version):
746 def _link_comments_to_version(self, pull_request_version):
747 """
747 """
748 Link all unlinked comments of this pull request to the given version.
748 Link all unlinked comments of this pull request to the given version.
749
749
750 :param pull_request_version: The `PullRequestVersion` to which
750 :param pull_request_version: The `PullRequestVersion` to which
751 the comments shall be linked.
751 the comments shall be linked.
752
752
753 """
753 """
754 pull_request = pull_request_version.pull_request
754 pull_request = pull_request_version.pull_request
755 comments = ChangesetComment.query().filter(
755 comments = ChangesetComment.query().filter(
756 # TODO: johbo: Should we query for the repo at all here?
756 # TODO: johbo: Should we query for the repo at all here?
757 # Pending decision on how comments of PRs are to be related
757 # Pending decision on how comments of PRs are to be related
758 # to either the source repo, the target repo or no repo at all.
758 # to either the source repo, the target repo or no repo at all.
759 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
759 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
760 ChangesetComment.pull_request == pull_request,
760 ChangesetComment.pull_request == pull_request,
761 ChangesetComment.pull_request_version == None)
761 ChangesetComment.pull_request_version == None)
762
762
763 # TODO: johbo: Find out why this breaks if it is done in a bulk
763 # TODO: johbo: Find out why this breaks if it is done in a bulk
764 # operation.
764 # operation.
765 for comment in comments:
765 for comment in comments:
766 comment.pull_request_version_id = (
766 comment.pull_request_version_id = (
767 pull_request_version.pull_request_version_id)
767 pull_request_version.pull_request_version_id)
768 Session().add(comment)
768 Session().add(comment)
769
769
770 def _calculate_commit_id_changes(self, old_ids, new_ids):
770 def _calculate_commit_id_changes(self, old_ids, new_ids):
771 added = new_ids.difference(old_ids)
771 added = new_ids.difference(old_ids)
772 common = old_ids.intersection(new_ids)
772 common = old_ids.intersection(new_ids)
773 removed = old_ids.difference(new_ids)
773 removed = old_ids.difference(new_ids)
774 return ChangeTuple(added, common, removed)
774 return ChangeTuple(added, common, removed)
775
775
776 def _calculate_file_changes(self, old_diff_data, new_diff_data):
776 def _calculate_file_changes(self, old_diff_data, new_diff_data):
777
777
778 old_files = OrderedDict()
778 old_files = OrderedDict()
779 for diff_data in old_diff_data.parsed_diff:
779 for diff_data in old_diff_data.parsed_diff:
780 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
780 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
781
781
782 added_files = []
782 added_files = []
783 modified_files = []
783 modified_files = []
784 removed_files = []
784 removed_files = []
785 for diff_data in new_diff_data.parsed_diff:
785 for diff_data in new_diff_data.parsed_diff:
786 new_filename = diff_data['filename']
786 new_filename = diff_data['filename']
787 new_hash = md5_safe(diff_data['raw_diff'])
787 new_hash = md5_safe(diff_data['raw_diff'])
788
788
789 old_hash = old_files.get(new_filename)
789 old_hash = old_files.get(new_filename)
790 if not old_hash:
790 if not old_hash:
791 # file is not present in old diff, means it's added
791 # file is not present in old diff, means it's added
792 added_files.append(new_filename)
792 added_files.append(new_filename)
793 else:
793 else:
794 if new_hash != old_hash:
794 if new_hash != old_hash:
795 modified_files.append(new_filename)
795 modified_files.append(new_filename)
796 # now remove a file from old, since we have seen it already
796 # now remove a file from old, since we have seen it already
797 del old_files[new_filename]
797 del old_files[new_filename]
798
798
799 # removed files is when there are present in old, but not in NEW,
799 # removed files is when there are present in old, but not in NEW,
800 # since we remove old files that are present in new diff, left-overs
800 # since we remove old files that are present in new diff, left-overs
801 # if any should be the removed files
801 # if any should be the removed files
802 removed_files.extend(old_files.keys())
802 removed_files.extend(old_files.keys())
803
803
804 return FileChangeTuple(added_files, modified_files, removed_files)
804 return FileChangeTuple(added_files, modified_files, removed_files)
805
805
806 def _render_update_message(self, changes, file_changes):
806 def _render_update_message(self, changes, file_changes):
807 """
807 """
808 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
808 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
809 so it's always looking the same disregarding on which default
809 so it's always looking the same disregarding on which default
810 renderer system is using.
810 renderer system is using.
811
811
812 :param changes: changes named tuple
812 :param changes: changes named tuple
813 :param file_changes: file changes named tuple
813 :param file_changes: file changes named tuple
814
814
815 """
815 """
816 new_status = ChangesetStatus.get_status_lbl(
816 new_status = ChangesetStatus.get_status_lbl(
817 ChangesetStatus.STATUS_UNDER_REVIEW)
817 ChangesetStatus.STATUS_UNDER_REVIEW)
818
818
819 changed_files = (
819 changed_files = (
820 file_changes.added + file_changes.modified + file_changes.removed)
820 file_changes.added + file_changes.modified + file_changes.removed)
821
821
822 params = {
822 params = {
823 'under_review_label': new_status,
823 'under_review_label': new_status,
824 'added_commits': changes.added,
824 'added_commits': changes.added,
825 'removed_commits': changes.removed,
825 'removed_commits': changes.removed,
826 'changed_files': changed_files,
826 'changed_files': changed_files,
827 'added_files': file_changes.added,
827 'added_files': file_changes.added,
828 'modified_files': file_changes.modified,
828 'modified_files': file_changes.modified,
829 'removed_files': file_changes.removed,
829 'removed_files': file_changes.removed,
830 }
830 }
831 renderer = RstTemplateRenderer()
831 renderer = RstTemplateRenderer()
832 return renderer.render('pull_request_update.mako', **params)
832 return renderer.render('pull_request_update.mako', **params)
833
833
834 def edit(self, pull_request, title, description):
834 def edit(self, pull_request, title, description):
835 pull_request = self.__get_pull_request(pull_request)
835 pull_request = self.__get_pull_request(pull_request)
836 if pull_request.is_closed():
836 if pull_request.is_closed():
837 raise ValueError('This pull request is closed')
837 raise ValueError('This pull request is closed')
838 if title:
838 if title:
839 pull_request.title = title
839 pull_request.title = title
840 pull_request.description = description
840 pull_request.description = description
841 pull_request.updated_on = datetime.datetime.now()
841 pull_request.updated_on = datetime.datetime.now()
842 Session().add(pull_request)
842 Session().add(pull_request)
843
843
844 def update_reviewers(self, pull_request, reviewer_data):
844 def update_reviewers(self, pull_request, reviewer_data):
845 """
845 """
846 Update the reviewers in the pull request
846 Update the reviewers in the pull request
847
847
848 :param pull_request: the pr to update
848 :param pull_request: the pr to update
849 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
849 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
850 """
850 """
851
851
852 reviewers_reasons = {}
852 reviewers_reasons = {}
853 for user_id, reasons in reviewer_data:
853 for user_id, reasons in reviewer_data:
854 if isinstance(user_id, (int, basestring)):
854 if isinstance(user_id, (int, basestring)):
855 user_id = self._get_user(user_id).user_id
855 user_id = self._get_user(user_id).user_id
856 reviewers_reasons[user_id] = reasons
856 reviewers_reasons[user_id] = reasons
857
857
858 reviewers_ids = set(reviewers_reasons.keys())
858 reviewers_ids = set(reviewers_reasons.keys())
859 pull_request = self.__get_pull_request(pull_request)
859 pull_request = self.__get_pull_request(pull_request)
860 current_reviewers = PullRequestReviewers.query()\
860 current_reviewers = PullRequestReviewers.query()\
861 .filter(PullRequestReviewers.pull_request ==
861 .filter(PullRequestReviewers.pull_request ==
862 pull_request).all()
862 pull_request).all()
863 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
863 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
864
864
865 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
865 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
866 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
866 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
867
867
868 log.debug("Adding %s reviewers", ids_to_add)
868 log.debug("Adding %s reviewers", ids_to_add)
869 log.debug("Removing %s reviewers", ids_to_remove)
869 log.debug("Removing %s reviewers", ids_to_remove)
870 changed = False
870 changed = False
871 for uid in ids_to_add:
871 for uid in ids_to_add:
872 changed = True
872 changed = True
873 _usr = self._get_user(uid)
873 _usr = self._get_user(uid)
874 reasons = reviewers_reasons[uid]
874 reasons = reviewers_reasons[uid]
875 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
875 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
876 Session().add(reviewer)
876 Session().add(reviewer)
877
877
878 self.notify_reviewers(pull_request, ids_to_add)
878 self.notify_reviewers(pull_request, ids_to_add)
879
879
880 for uid in ids_to_remove:
880 for uid in ids_to_remove:
881 changed = True
881 changed = True
882 reviewer = PullRequestReviewers.query()\
882 reviewer = PullRequestReviewers.query()\
883 .filter(PullRequestReviewers.user_id == uid,
883 .filter(PullRequestReviewers.user_id == uid,
884 PullRequestReviewers.pull_request == pull_request)\
884 PullRequestReviewers.pull_request == pull_request)\
885 .scalar()
885 .scalar()
886 if reviewer:
886 if reviewer:
887 Session().delete(reviewer)
887 Session().delete(reviewer)
888 if changed:
888 if changed:
889 pull_request.updated_on = datetime.datetime.now()
889 pull_request.updated_on = datetime.datetime.now()
890 Session().add(pull_request)
890 Session().add(pull_request)
891
891
892 return ids_to_add, ids_to_remove
892 return ids_to_add, ids_to_remove
893
893
894 def get_url(self, pull_request):
894 def get_url(self, pull_request):
895 return h.url('pullrequest_show',
895 return h.url('pullrequest_show',
896 repo_name=safe_str(pull_request.target_repo.repo_name),
896 repo_name=safe_str(pull_request.target_repo.repo_name),
897 pull_request_id=pull_request.pull_request_id,
897 pull_request_id=pull_request.pull_request_id,
898 qualified=True)
898 qualified=True)
899
899
900 def get_shadow_clone_url(self, pull_request):
900 def get_shadow_clone_url(self, pull_request):
901 """
901 """
902 Returns qualified url pointing to the shadow repository. If this pull
902 Returns qualified url pointing to the shadow repository. If this pull
903 request is closed there is no shadow repository and ``None`` will be
903 request is closed there is no shadow repository and ``None`` will be
904 returned.
904 returned.
905 """
905 """
906 if pull_request.is_closed():
906 if pull_request.is_closed():
907 return None
907 return None
908 else:
908 else:
909 pr_url = urllib.unquote(self.get_url(pull_request))
909 pr_url = urllib.unquote(self.get_url(pull_request))
910 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
910 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
911
911
912 def notify_reviewers(self, pull_request, reviewers_ids):
912 def notify_reviewers(self, pull_request, reviewers_ids):
913 # notification to reviewers
913 # notification to reviewers
914 if not reviewers_ids:
914 if not reviewers_ids:
915 return
915 return
916
916
917 pull_request_obj = pull_request
917 pull_request_obj = pull_request
918 # get the current participants of this pull request
918 # get the current participants of this pull request
919 recipients = reviewers_ids
919 recipients = reviewers_ids
920 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
920 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
921
921
922 pr_source_repo = pull_request_obj.source_repo
922 pr_source_repo = pull_request_obj.source_repo
923 pr_target_repo = pull_request_obj.target_repo
923 pr_target_repo = pull_request_obj.target_repo
924
924
925 pr_url = h.url(
925 pr_url = h.url(
926 'pullrequest_show',
926 'pullrequest_show',
927 repo_name=pr_target_repo.repo_name,
927 repo_name=pr_target_repo.repo_name,
928 pull_request_id=pull_request_obj.pull_request_id,
928 pull_request_id=pull_request_obj.pull_request_id,
929 qualified=True,)
929 qualified=True,)
930
930
931 # set some variables for email notification
931 # set some variables for email notification
932 pr_target_repo_url = h.url(
932 pr_target_repo_url = h.url(
933 'summary_home',
933 'summary_home',
934 repo_name=pr_target_repo.repo_name,
934 repo_name=pr_target_repo.repo_name,
935 qualified=True)
935 qualified=True)
936
936
937 pr_source_repo_url = h.url(
937 pr_source_repo_url = h.url(
938 'summary_home',
938 'summary_home',
939 repo_name=pr_source_repo.repo_name,
939 repo_name=pr_source_repo.repo_name,
940 qualified=True)
940 qualified=True)
941
941
942 # pull request specifics
942 # pull request specifics
943 pull_request_commits = [
943 pull_request_commits = [
944 (x.raw_id, x.message)
944 (x.raw_id, x.message)
945 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
945 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
946
946
947 kwargs = {
947 kwargs = {
948 'user': pull_request.author,
948 'user': pull_request.author,
949 'pull_request': pull_request_obj,
949 'pull_request': pull_request_obj,
950 'pull_request_commits': pull_request_commits,
950 'pull_request_commits': pull_request_commits,
951
951
952 'pull_request_target_repo': pr_target_repo,
952 'pull_request_target_repo': pr_target_repo,
953 'pull_request_target_repo_url': pr_target_repo_url,
953 'pull_request_target_repo_url': pr_target_repo_url,
954
954
955 'pull_request_source_repo': pr_source_repo,
955 'pull_request_source_repo': pr_source_repo,
956 'pull_request_source_repo_url': pr_source_repo_url,
956 'pull_request_source_repo_url': pr_source_repo_url,
957
957
958 'pull_request_url': pr_url,
958 'pull_request_url': pr_url,
959 }
959 }
960
960
961 # pre-generate the subject for notification itself
961 # pre-generate the subject for notification itself
962 (subject,
962 (subject,
963 _h, _e, # we don't care about those
963 _h, _e, # we don't care about those
964 body_plaintext) = EmailNotificationModel().render_email(
964 body_plaintext) = EmailNotificationModel().render_email(
965 notification_type, **kwargs)
965 notification_type, **kwargs)
966
966
967 # create notification objects, and emails
967 # create notification objects, and emails
968 NotificationModel().create(
968 NotificationModel().create(
969 created_by=pull_request.author,
969 created_by=pull_request.author,
970 notification_subject=subject,
970 notification_subject=subject,
971 notification_body=body_plaintext,
971 notification_body=body_plaintext,
972 notification_type=notification_type,
972 notification_type=notification_type,
973 recipients=recipients,
973 recipients=recipients,
974 email_kwargs=kwargs,
974 email_kwargs=kwargs,
975 )
975 )
976
976
977 def delete(self, pull_request):
977 def delete(self, pull_request):
978 pull_request = self.__get_pull_request(pull_request)
978 pull_request = self.__get_pull_request(pull_request)
979 self._cleanup_merge_workspace(pull_request)
979 self._cleanup_merge_workspace(pull_request)
980 Session().delete(pull_request)
980 Session().delete(pull_request)
981
981
982 def close_pull_request(self, pull_request, user):
982 def close_pull_request(self, pull_request, user):
983 pull_request = self.__get_pull_request(pull_request)
983 pull_request = self.__get_pull_request(pull_request)
984 self._cleanup_merge_workspace(pull_request)
984 self._cleanup_merge_workspace(pull_request)
985 pull_request.status = PullRequest.STATUS_CLOSED
985 pull_request.status = PullRequest.STATUS_CLOSED
986 pull_request.updated_on = datetime.datetime.now()
986 pull_request.updated_on = datetime.datetime.now()
987 Session().add(pull_request)
987 Session().add(pull_request)
988 self._trigger_pull_request_hook(
988 self._trigger_pull_request_hook(
989 pull_request, pull_request.author, 'close')
989 pull_request, pull_request.author, 'close')
990 self._log_action('user_closed_pull_request', user, pull_request)
990 self._log_action('user_closed_pull_request', user, pull_request)
991
991
992 def close_pull_request_with_comment(self, pull_request, user, repo,
992 def close_pull_request_with_comment(self, pull_request, user, repo,
993 message=None):
993 message=None):
994 status = ChangesetStatus.STATUS_REJECTED
994 status = ChangesetStatus.STATUS_REJECTED
995
995
996 if not message:
996 if not message:
997 message = (
997 message = (
998 _('Status change %(transition_icon)s %(status)s') % {
998 _('Status change %(transition_icon)s %(status)s') % {
999 'transition_icon': '>',
999 'transition_icon': '>',
1000 'status': ChangesetStatus.get_status_lbl(status)})
1000 'status': ChangesetStatus.get_status_lbl(status)})
1001
1001
1002 internal_message = _('Closing with') + ' ' + message
1002 internal_message = _('Closing with') + ' ' + message
1003
1003
1004 comm = ChangesetCommentsModel().create(
1004 comm = CommentsModel().create(
1005 text=internal_message,
1005 text=internal_message,
1006 repo=repo.repo_id,
1006 repo=repo.repo_id,
1007 user=user.user_id,
1007 user=user.user_id,
1008 pull_request=pull_request.pull_request_id,
1008 pull_request=pull_request.pull_request_id,
1009 f_path=None,
1009 f_path=None,
1010 line_no=None,
1010 line_no=None,
1011 status_change=ChangesetStatus.get_status_lbl(status),
1011 status_change=ChangesetStatus.get_status_lbl(status),
1012 status_change_type=status,
1012 status_change_type=status,
1013 closing_pr=True
1013 closing_pr=True
1014 )
1014 )
1015
1015
1016 ChangesetStatusModel().set_status(
1016 ChangesetStatusModel().set_status(
1017 repo.repo_id,
1017 repo.repo_id,
1018 status,
1018 status,
1019 user.user_id,
1019 user.user_id,
1020 comm,
1020 comm,
1021 pull_request=pull_request.pull_request_id
1021 pull_request=pull_request.pull_request_id
1022 )
1022 )
1023 Session().flush()
1023 Session().flush()
1024
1024
1025 PullRequestModel().close_pull_request(
1025 PullRequestModel().close_pull_request(
1026 pull_request.pull_request_id, user)
1026 pull_request.pull_request_id, user)
1027
1027
1028 def merge_status(self, pull_request):
1028 def merge_status(self, pull_request):
1029 if not self._is_merge_enabled(pull_request):
1029 if not self._is_merge_enabled(pull_request):
1030 return False, _('Server-side pull request merging is disabled.')
1030 return False, _('Server-side pull request merging is disabled.')
1031 if pull_request.is_closed():
1031 if pull_request.is_closed():
1032 return False, _('This pull request is closed.')
1032 return False, _('This pull request is closed.')
1033 merge_possible, msg = self._check_repo_requirements(
1033 merge_possible, msg = self._check_repo_requirements(
1034 target=pull_request.target_repo, source=pull_request.source_repo)
1034 target=pull_request.target_repo, source=pull_request.source_repo)
1035 if not merge_possible:
1035 if not merge_possible:
1036 return merge_possible, msg
1036 return merge_possible, msg
1037
1037
1038 try:
1038 try:
1039 resp = self._try_merge(pull_request)
1039 resp = self._try_merge(pull_request)
1040 log.debug("Merge response: %s", resp)
1040 log.debug("Merge response: %s", resp)
1041 status = resp.possible, self.merge_status_message(
1041 status = resp.possible, self.merge_status_message(
1042 resp.failure_reason)
1042 resp.failure_reason)
1043 except NotImplementedError:
1043 except NotImplementedError:
1044 status = False, _('Pull request merging is not supported.')
1044 status = False, _('Pull request merging is not supported.')
1045
1045
1046 return status
1046 return status
1047
1047
1048 def _check_repo_requirements(self, target, source):
1048 def _check_repo_requirements(self, target, source):
1049 """
1049 """
1050 Check if `target` and `source` have compatible requirements.
1050 Check if `target` and `source` have compatible requirements.
1051
1051
1052 Currently this is just checking for largefiles.
1052 Currently this is just checking for largefiles.
1053 """
1053 """
1054 target_has_largefiles = self._has_largefiles(target)
1054 target_has_largefiles = self._has_largefiles(target)
1055 source_has_largefiles = self._has_largefiles(source)
1055 source_has_largefiles = self._has_largefiles(source)
1056 merge_possible = True
1056 merge_possible = True
1057 message = u''
1057 message = u''
1058
1058
1059 if target_has_largefiles != source_has_largefiles:
1059 if target_has_largefiles != source_has_largefiles:
1060 merge_possible = False
1060 merge_possible = False
1061 if source_has_largefiles:
1061 if source_has_largefiles:
1062 message = _(
1062 message = _(
1063 'Target repository large files support is disabled.')
1063 'Target repository large files support is disabled.')
1064 else:
1064 else:
1065 message = _(
1065 message = _(
1066 'Source repository large files support is disabled.')
1066 'Source repository large files support is disabled.')
1067
1067
1068 return merge_possible, message
1068 return merge_possible, message
1069
1069
1070 def _has_largefiles(self, repo):
1070 def _has_largefiles(self, repo):
1071 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1071 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1072 'extensions', 'largefiles')
1072 'extensions', 'largefiles')
1073 return largefiles_ui and largefiles_ui[0].active
1073 return largefiles_ui and largefiles_ui[0].active
1074
1074
1075 def _try_merge(self, pull_request):
1075 def _try_merge(self, pull_request):
1076 """
1076 """
1077 Try to merge the pull request and return the merge status.
1077 Try to merge the pull request and return the merge status.
1078 """
1078 """
1079 log.debug(
1079 log.debug(
1080 "Trying out if the pull request %s can be merged.",
1080 "Trying out if the pull request %s can be merged.",
1081 pull_request.pull_request_id)
1081 pull_request.pull_request_id)
1082 target_vcs = pull_request.target_repo.scm_instance()
1082 target_vcs = pull_request.target_repo.scm_instance()
1083
1083
1084 # Refresh the target reference.
1084 # Refresh the target reference.
1085 try:
1085 try:
1086 target_ref = self._refresh_reference(
1086 target_ref = self._refresh_reference(
1087 pull_request.target_ref_parts, target_vcs)
1087 pull_request.target_ref_parts, target_vcs)
1088 except CommitDoesNotExistError:
1088 except CommitDoesNotExistError:
1089 merge_state = MergeResponse(
1089 merge_state = MergeResponse(
1090 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1090 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1091 return merge_state
1091 return merge_state
1092
1092
1093 target_locked = pull_request.target_repo.locked
1093 target_locked = pull_request.target_repo.locked
1094 if target_locked and target_locked[0]:
1094 if target_locked and target_locked[0]:
1095 log.debug("The target repository is locked.")
1095 log.debug("The target repository is locked.")
1096 merge_state = MergeResponse(
1096 merge_state = MergeResponse(
1097 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1097 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1098 elif self._needs_merge_state_refresh(pull_request, target_ref):
1098 elif self._needs_merge_state_refresh(pull_request, target_ref):
1099 log.debug("Refreshing the merge status of the repository.")
1099 log.debug("Refreshing the merge status of the repository.")
1100 merge_state = self._refresh_merge_state(
1100 merge_state = self._refresh_merge_state(
1101 pull_request, target_vcs, target_ref)
1101 pull_request, target_vcs, target_ref)
1102 else:
1102 else:
1103 possible = pull_request.\
1103 possible = pull_request.\
1104 _last_merge_status == MergeFailureReason.NONE
1104 _last_merge_status == MergeFailureReason.NONE
1105 merge_state = MergeResponse(
1105 merge_state = MergeResponse(
1106 possible, False, None, pull_request._last_merge_status)
1106 possible, False, None, pull_request._last_merge_status)
1107
1107
1108 return merge_state
1108 return merge_state
1109
1109
1110 def _refresh_reference(self, reference, vcs_repository):
1110 def _refresh_reference(self, reference, vcs_repository):
1111 if reference.type in ('branch', 'book'):
1111 if reference.type in ('branch', 'book'):
1112 name_or_id = reference.name
1112 name_or_id = reference.name
1113 else:
1113 else:
1114 name_or_id = reference.commit_id
1114 name_or_id = reference.commit_id
1115 refreshed_commit = vcs_repository.get_commit(name_or_id)
1115 refreshed_commit = vcs_repository.get_commit(name_or_id)
1116 refreshed_reference = Reference(
1116 refreshed_reference = Reference(
1117 reference.type, reference.name, refreshed_commit.raw_id)
1117 reference.type, reference.name, refreshed_commit.raw_id)
1118 return refreshed_reference
1118 return refreshed_reference
1119
1119
1120 def _needs_merge_state_refresh(self, pull_request, target_reference):
1120 def _needs_merge_state_refresh(self, pull_request, target_reference):
1121 return not(
1121 return not(
1122 pull_request.revisions and
1122 pull_request.revisions and
1123 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1123 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1124 target_reference.commit_id == pull_request._last_merge_target_rev)
1124 target_reference.commit_id == pull_request._last_merge_target_rev)
1125
1125
1126 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1126 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1127 workspace_id = self._workspace_id(pull_request)
1127 workspace_id = self._workspace_id(pull_request)
1128 source_vcs = pull_request.source_repo.scm_instance()
1128 source_vcs = pull_request.source_repo.scm_instance()
1129 use_rebase = self._use_rebase_for_merging(pull_request)
1129 use_rebase = self._use_rebase_for_merging(pull_request)
1130 merge_state = target_vcs.merge(
1130 merge_state = target_vcs.merge(
1131 target_reference, source_vcs, pull_request.source_ref_parts,
1131 target_reference, source_vcs, pull_request.source_ref_parts,
1132 workspace_id, dry_run=True, use_rebase=use_rebase)
1132 workspace_id, dry_run=True, use_rebase=use_rebase)
1133
1133
1134 # Do not store the response if there was an unknown error.
1134 # Do not store the response if there was an unknown error.
1135 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1135 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1136 pull_request._last_merge_source_rev = \
1136 pull_request._last_merge_source_rev = \
1137 pull_request.source_ref_parts.commit_id
1137 pull_request.source_ref_parts.commit_id
1138 pull_request._last_merge_target_rev = target_reference.commit_id
1138 pull_request._last_merge_target_rev = target_reference.commit_id
1139 pull_request._last_merge_status = merge_state.failure_reason
1139 pull_request._last_merge_status = merge_state.failure_reason
1140 pull_request.shadow_merge_ref = merge_state.merge_ref
1140 pull_request.shadow_merge_ref = merge_state.merge_ref
1141 Session().add(pull_request)
1141 Session().add(pull_request)
1142 Session().commit()
1142 Session().commit()
1143
1143
1144 return merge_state
1144 return merge_state
1145
1145
1146 def _workspace_id(self, pull_request):
1146 def _workspace_id(self, pull_request):
1147 workspace_id = 'pr-%s' % pull_request.pull_request_id
1147 workspace_id = 'pr-%s' % pull_request.pull_request_id
1148 return workspace_id
1148 return workspace_id
1149
1149
1150 def merge_status_message(self, status_code):
1150 def merge_status_message(self, status_code):
1151 """
1151 """
1152 Return a human friendly error message for the given merge status code.
1152 Return a human friendly error message for the given merge status code.
1153 """
1153 """
1154 return self.MERGE_STATUS_MESSAGES[status_code]
1154 return self.MERGE_STATUS_MESSAGES[status_code]
1155
1155
1156 def generate_repo_data(self, repo, commit_id=None, branch=None,
1156 def generate_repo_data(self, repo, commit_id=None, branch=None,
1157 bookmark=None):
1157 bookmark=None):
1158 all_refs, selected_ref = \
1158 all_refs, selected_ref = \
1159 self._get_repo_pullrequest_sources(
1159 self._get_repo_pullrequest_sources(
1160 repo.scm_instance(), commit_id=commit_id,
1160 repo.scm_instance(), commit_id=commit_id,
1161 branch=branch, bookmark=bookmark)
1161 branch=branch, bookmark=bookmark)
1162
1162
1163 refs_select2 = []
1163 refs_select2 = []
1164 for element in all_refs:
1164 for element in all_refs:
1165 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1165 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1166 refs_select2.append({'text': element[1], 'children': children})
1166 refs_select2.append({'text': element[1], 'children': children})
1167
1167
1168 return {
1168 return {
1169 'user': {
1169 'user': {
1170 'user_id': repo.user.user_id,
1170 'user_id': repo.user.user_id,
1171 'username': repo.user.username,
1171 'username': repo.user.username,
1172 'firstname': repo.user.firstname,
1172 'firstname': repo.user.firstname,
1173 'lastname': repo.user.lastname,
1173 'lastname': repo.user.lastname,
1174 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1174 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1175 },
1175 },
1176 'description': h.chop_at_smart(repo.description, '\n'),
1176 'description': h.chop_at_smart(repo.description, '\n'),
1177 'refs': {
1177 'refs': {
1178 'all_refs': all_refs,
1178 'all_refs': all_refs,
1179 'selected_ref': selected_ref,
1179 'selected_ref': selected_ref,
1180 'select2_refs': refs_select2
1180 'select2_refs': refs_select2
1181 }
1181 }
1182 }
1182 }
1183
1183
1184 def generate_pullrequest_title(self, source, source_ref, target):
1184 def generate_pullrequest_title(self, source, source_ref, target):
1185 return u'{source}#{at_ref} to {target}'.format(
1185 return u'{source}#{at_ref} to {target}'.format(
1186 source=source,
1186 source=source,
1187 at_ref=source_ref,
1187 at_ref=source_ref,
1188 target=target,
1188 target=target,
1189 )
1189 )
1190
1190
1191 def _cleanup_merge_workspace(self, pull_request):
1191 def _cleanup_merge_workspace(self, pull_request):
1192 # Merging related cleanup
1192 # Merging related cleanup
1193 target_scm = pull_request.target_repo.scm_instance()
1193 target_scm = pull_request.target_repo.scm_instance()
1194 workspace_id = 'pr-%s' % pull_request.pull_request_id
1194 workspace_id = 'pr-%s' % pull_request.pull_request_id
1195
1195
1196 try:
1196 try:
1197 target_scm.cleanup_merge_workspace(workspace_id)
1197 target_scm.cleanup_merge_workspace(workspace_id)
1198 except NotImplementedError:
1198 except NotImplementedError:
1199 pass
1199 pass
1200
1200
1201 def _get_repo_pullrequest_sources(
1201 def _get_repo_pullrequest_sources(
1202 self, repo, commit_id=None, branch=None, bookmark=None):
1202 self, repo, commit_id=None, branch=None, bookmark=None):
1203 """
1203 """
1204 Return a structure with repo's interesting commits, suitable for
1204 Return a structure with repo's interesting commits, suitable for
1205 the selectors in pullrequest controller
1205 the selectors in pullrequest controller
1206
1206
1207 :param commit_id: a commit that must be in the list somehow
1207 :param commit_id: a commit that must be in the list somehow
1208 and selected by default
1208 and selected by default
1209 :param branch: a branch that must be in the list and selected
1209 :param branch: a branch that must be in the list and selected
1210 by default - even if closed
1210 by default - even if closed
1211 :param bookmark: a bookmark that must be in the list and selected
1211 :param bookmark: a bookmark that must be in the list and selected
1212 """
1212 """
1213
1213
1214 commit_id = safe_str(commit_id) if commit_id else None
1214 commit_id = safe_str(commit_id) if commit_id else None
1215 branch = safe_str(branch) if branch else None
1215 branch = safe_str(branch) if branch else None
1216 bookmark = safe_str(bookmark) if bookmark else None
1216 bookmark = safe_str(bookmark) if bookmark else None
1217
1217
1218 selected = None
1218 selected = None
1219
1219
1220 # order matters: first source that has commit_id in it will be selected
1220 # order matters: first source that has commit_id in it will be selected
1221 sources = []
1221 sources = []
1222 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1222 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1223 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1223 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1224
1224
1225 if commit_id:
1225 if commit_id:
1226 ref_commit = (h.short_id(commit_id), commit_id)
1226 ref_commit = (h.short_id(commit_id), commit_id)
1227 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1227 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1228
1228
1229 sources.append(
1229 sources.append(
1230 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1230 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1231 )
1231 )
1232
1232
1233 groups = []
1233 groups = []
1234 for group_key, ref_list, group_name, match in sources:
1234 for group_key, ref_list, group_name, match in sources:
1235 group_refs = []
1235 group_refs = []
1236 for ref_name, ref_id in ref_list:
1236 for ref_name, ref_id in ref_list:
1237 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1237 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1238 group_refs.append((ref_key, ref_name))
1238 group_refs.append((ref_key, ref_name))
1239
1239
1240 if not selected:
1240 if not selected:
1241 if set([commit_id, match]) & set([ref_id, ref_name]):
1241 if set([commit_id, match]) & set([ref_id, ref_name]):
1242 selected = ref_key
1242 selected = ref_key
1243
1243
1244 if group_refs:
1244 if group_refs:
1245 groups.append((group_refs, group_name))
1245 groups.append((group_refs, group_name))
1246
1246
1247 if not selected:
1247 if not selected:
1248 ref = commit_id or branch or bookmark
1248 ref = commit_id or branch or bookmark
1249 if ref:
1249 if ref:
1250 raise CommitDoesNotExistError(
1250 raise CommitDoesNotExistError(
1251 'No commit refs could be found matching: %s' % ref)
1251 'No commit refs could be found matching: %s' % ref)
1252 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1252 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1253 selected = 'branch:%s:%s' % (
1253 selected = 'branch:%s:%s' % (
1254 repo.DEFAULT_BRANCH_NAME,
1254 repo.DEFAULT_BRANCH_NAME,
1255 repo.branches[repo.DEFAULT_BRANCH_NAME]
1255 repo.branches[repo.DEFAULT_BRANCH_NAME]
1256 )
1256 )
1257 elif repo.commit_ids:
1257 elif repo.commit_ids:
1258 rev = repo.commit_ids[0]
1258 rev = repo.commit_ids[0]
1259 selected = 'rev:%s:%s' % (rev, rev)
1259 selected = 'rev:%s:%s' % (rev, rev)
1260 else:
1260 else:
1261 raise EmptyRepositoryError()
1261 raise EmptyRepositoryError()
1262 return groups, selected
1262 return groups, selected
1263
1263
1264 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1264 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1265 pull_request = self.__get_pull_request(pull_request)
1265 pull_request = self.__get_pull_request(pull_request)
1266 return self._get_diff_from_pr_or_version(pull_request, context=context)
1266 return self._get_diff_from_pr_or_version(pull_request, context=context)
1267
1267
1268 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1268 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1269 source_repo = pr_or_version.source_repo
1269 source_repo = pr_or_version.source_repo
1270
1270
1271 # we swap org/other ref since we run a simple diff on one repo
1271 # we swap org/other ref since we run a simple diff on one repo
1272 target_ref_id = pr_or_version.target_ref_parts.commit_id
1272 target_ref_id = pr_or_version.target_ref_parts.commit_id
1273 source_ref_id = pr_or_version.source_ref_parts.commit_id
1273 source_ref_id = pr_or_version.source_ref_parts.commit_id
1274 target_commit = source_repo.get_commit(
1274 target_commit = source_repo.get_commit(
1275 commit_id=safe_str(target_ref_id))
1275 commit_id=safe_str(target_ref_id))
1276 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1276 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1277 vcs_repo = source_repo.scm_instance()
1277 vcs_repo = source_repo.scm_instance()
1278
1278
1279 # TODO: johbo: In the context of an update, we cannot reach
1279 # TODO: johbo: In the context of an update, we cannot reach
1280 # the old commit anymore with our normal mechanisms. It needs
1280 # the old commit anymore with our normal mechanisms. It needs
1281 # some sort of special support in the vcs layer to avoid this
1281 # some sort of special support in the vcs layer to avoid this
1282 # workaround.
1282 # workaround.
1283 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1283 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1284 vcs_repo.alias == 'git'):
1284 vcs_repo.alias == 'git'):
1285 source_commit.raw_id = safe_str(source_ref_id)
1285 source_commit.raw_id = safe_str(source_ref_id)
1286
1286
1287 log.debug('calculating diff between '
1287 log.debug('calculating diff between '
1288 'source_ref:%s and target_ref:%s for repo `%s`',
1288 'source_ref:%s and target_ref:%s for repo `%s`',
1289 target_ref_id, source_ref_id,
1289 target_ref_id, source_ref_id,
1290 safe_unicode(vcs_repo.path))
1290 safe_unicode(vcs_repo.path))
1291
1291
1292 vcs_diff = vcs_repo.get_diff(
1292 vcs_diff = vcs_repo.get_diff(
1293 commit1=target_commit, commit2=source_commit, context=context)
1293 commit1=target_commit, commit2=source_commit, context=context)
1294 return vcs_diff
1294 return vcs_diff
1295
1295
1296 def _is_merge_enabled(self, pull_request):
1296 def _is_merge_enabled(self, pull_request):
1297 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1297 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1298 settings = settings_model.get_general_settings()
1298 settings = settings_model.get_general_settings()
1299 return settings.get('rhodecode_pr_merge_enabled', False)
1299 return settings.get('rhodecode_pr_merge_enabled', False)
1300
1300
1301 def _use_rebase_for_merging(self, pull_request):
1301 def _use_rebase_for_merging(self, pull_request):
1302 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1302 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1303 settings = settings_model.get_general_settings()
1303 settings = settings_model.get_general_settings()
1304 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1304 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1305
1305
1306 def _log_action(self, action, user, pull_request):
1306 def _log_action(self, action, user, pull_request):
1307 action_logger(
1307 action_logger(
1308 user,
1308 user,
1309 '{action}:{pr_id}'.format(
1309 '{action}:{pr_id}'.format(
1310 action=action, pr_id=pull_request.pull_request_id),
1310 action=action, pr_id=pull_request.pull_request_id),
1311 pull_request.target_repo)
1311 pull_request.target_repo)
1312
1312
1313
1313
1314 ChangeTuple = namedtuple('ChangeTuple',
1314 ChangeTuple = namedtuple('ChangeTuple',
1315 ['added', 'common', 'removed'])
1315 ['added', 'common', 'removed'])
1316
1316
1317 FileChangeTuple = namedtuple('FileChangeTuple',
1317 FileChangeTuple = namedtuple('FileChangeTuple',
1318 ['added', 'modified', 'removed'])
1318 ['added', 'modified', 'removed'])
@@ -1,93 +1,93 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.tests.events.conftest import EventCatcher
23 from rhodecode.tests.events.conftest import EventCatcher
24
24
25 from rhodecode.model.comment import ChangesetCommentsModel
25 from rhodecode.model.comment import CommentsModel
26 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.model.pull_request import PullRequestModel
27 from rhodecode.events import (
27 from rhodecode.events import (
28 PullRequestCreateEvent,
28 PullRequestCreateEvent,
29 PullRequestUpdateEvent,
29 PullRequestUpdateEvent,
30 PullRequestCommentEvent,
30 PullRequestCommentEvent,
31 PullRequestReviewEvent,
31 PullRequestReviewEvent,
32 PullRequestMergeEvent,
32 PullRequestMergeEvent,
33 PullRequestCloseEvent,
33 PullRequestCloseEvent,
34 )
34 )
35
35
36 # TODO: dan: make the serialization tests complete json comparisons
36 # TODO: dan: make the serialization tests complete json comparisons
37 @pytest.mark.backends("git", "hg")
37 @pytest.mark.backends("git", "hg")
38 @pytest.mark.parametrize('EventClass', [
38 @pytest.mark.parametrize('EventClass', [
39 PullRequestCreateEvent,
39 PullRequestCreateEvent,
40 PullRequestUpdateEvent,
40 PullRequestUpdateEvent,
41 PullRequestReviewEvent,
41 PullRequestReviewEvent,
42 PullRequestMergeEvent,
42 PullRequestMergeEvent,
43 PullRequestCloseEvent,
43 PullRequestCloseEvent,
44 ])
44 ])
45 def test_pullrequest_events_serialized(pr_util, EventClass):
45 def test_pullrequest_events_serialized(pr_util, EventClass):
46 pr = pr_util.create_pull_request()
46 pr = pr_util.create_pull_request()
47 event = EventClass(pr)
47 event = EventClass(pr)
48 data = event.as_dict()
48 data = event.as_dict()
49 assert data['name'] == EventClass.name
49 assert data['name'] == EventClass.name
50 assert data['repo']['repo_name'] == pr.target_repo.repo_name
50 assert data['repo']['repo_name'] == pr.target_repo.repo_name
51 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
51 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
52 assert data['pullrequest']['url']
52 assert data['pullrequest']['url']
53
53
54 @pytest.mark.backends("git", "hg")
54 @pytest.mark.backends("git", "hg")
55 def test_create_pull_request_events(pr_util):
55 def test_create_pull_request_events(pr_util):
56 with EventCatcher() as event_catcher:
56 with EventCatcher() as event_catcher:
57 pr_util.create_pull_request()
57 pr_util.create_pull_request()
58
58
59 assert PullRequestCreateEvent in event_catcher.events_types
59 assert PullRequestCreateEvent in event_catcher.events_types
60
60
61 @pytest.mark.backends("git", "hg")
61 @pytest.mark.backends("git", "hg")
62 def test_pullrequest_comment_events_serialized(pr_util):
62 def test_pullrequest_comment_events_serialized(pr_util):
63 pr = pr_util.create_pull_request()
63 pr = pr_util.create_pull_request()
64 comment = ChangesetCommentsModel().get_comments(
64 comment = CommentsModel().get_comments(
65 pr.target_repo.repo_id, pull_request=pr)[0]
65 pr.target_repo.repo_id, pull_request=pr)[0]
66 event = PullRequestCommentEvent(pr, comment)
66 event = PullRequestCommentEvent(pr, comment)
67 data = event.as_dict()
67 data = event.as_dict()
68 assert data['name'] == PullRequestCommentEvent.name
68 assert data['name'] == PullRequestCommentEvent.name
69 assert data['repo']['repo_name'] == pr.target_repo.repo_name
69 assert data['repo']['repo_name'] == pr.target_repo.repo_name
70 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
70 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
71 assert data['pullrequest']['url']
71 assert data['pullrequest']['url']
72 assert data['comment']['text'] == comment.text
72 assert data['comment']['text'] == comment.text
73
73
74
74
75 @pytest.mark.backends("git", "hg")
75 @pytest.mark.backends("git", "hg")
76 def test_close_pull_request_events(pr_util, user_admin):
76 def test_close_pull_request_events(pr_util, user_admin):
77 pr = pr_util.create_pull_request()
77 pr = pr_util.create_pull_request()
78
78
79 with EventCatcher() as event_catcher:
79 with EventCatcher() as event_catcher:
80 PullRequestModel().close_pull_request(pr, user_admin)
80 PullRequestModel().close_pull_request(pr, user_admin)
81
81
82 assert PullRequestCloseEvent in event_catcher.events_types
82 assert PullRequestCloseEvent in event_catcher.events_types
83
83
84
84
85 @pytest.mark.backends("git", "hg")
85 @pytest.mark.backends("git", "hg")
86 def test_close_pull_request_with_comment_events(pr_util, user_admin):
86 def test_close_pull_request_with_comment_events(pr_util, user_admin):
87 pr = pr_util.create_pull_request()
87 pr = pr_util.create_pull_request()
88
88
89 with EventCatcher() as event_catcher:
89 with EventCatcher() as event_catcher:
90 PullRequestModel().close_pull_request_with_comment(
90 PullRequestModel().close_pull_request_with_comment(
91 pr, user_admin, pr.target_repo)
91 pr, user_admin, pr.target_repo)
92
92
93 assert PullRequestCloseEvent in event_catcher.events_types
93 assert PullRequestCloseEvent in event_catcher.events_types
@@ -1,159 +1,159 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.diffs import DiffLineNumber
23 from rhodecode.lib.diffs import DiffLineNumber
24 from rhodecode.model import comment
24 from rhodecode.model import comment
25
25
26
26
27 @pytest.mark.parametrize('value, expected', [
27 @pytest.mark.parametrize('value, expected', [
28 ('o1', DiffLineNumber(1, None)),
28 ('o1', DiffLineNumber(1, None)),
29 ('o798', DiffLineNumber(798, None)),
29 ('o798', DiffLineNumber(798, None)),
30 ('n1', DiffLineNumber(None, 1)),
30 ('n1', DiffLineNumber(None, 1)),
31 ('n100000', DiffLineNumber(None, 100000)),
31 ('n100000', DiffLineNumber(None, 100000)),
32 ])
32 ])
33 def test_parse_comment_line_number(value, expected):
33 def test_parse_comment_line_number(value, expected):
34 result = comment._parse_comment_line_number(value)
34 result = comment._parse_comment_line_number(value)
35 assert result == expected
35 assert result == expected
36
36
37
37
38 @pytest.mark.parametrize('value, should_raise', [
38 @pytest.mark.parametrize('value, should_raise', [
39 (None, AttributeError),
39 (None, AttributeError),
40 ('2', ValueError),
40 ('2', ValueError),
41 ('first_line', ValueError),
41 ('first_line', ValueError),
42 ('olast_line', ValueError),
42 ('olast_line', ValueError),
43 ])
43 ])
44 def test_parse_comment_line_number_raises(value, should_raise):
44 def test_parse_comment_line_number_raises(value, should_raise):
45 with pytest.raises(should_raise):
45 with pytest.raises(should_raise):
46 comment._parse_comment_line_number(value)
46 comment._parse_comment_line_number(value)
47
47
48
48
49 @pytest.mark.parametrize('old, new, expected', [
49 @pytest.mark.parametrize('old, new, expected', [
50 (None, 1, 'n1'),
50 (None, 1, 'n1'),
51 (None, 2, 'n2'),
51 (None, 2, 'n2'),
52 (None, 10, 'n10'),
52 (None, 10, 'n10'),
53 (1, None, 'o1'),
53 (1, None, 'o1'),
54 (10, None, 'o10'),
54 (10, None, 'o10'),
55 # Ensuring consistent behavior, although there is no line 0 used.
55 # Ensuring consistent behavior, although there is no line 0 used.
56 (None, 0, 'n0'),
56 (None, 0, 'n0'),
57 (0, None, 'o0'),
57 (0, None, 'o0'),
58 # Using the empty string to reflect what the comment model does
58 # Using the empty string to reflect what the comment model does
59 (None, None, ''),
59 (None, None, ''),
60 # Give a preference to the new line number if both are given
60 # Give a preference to the new line number if both are given
61 (1, 1, 'n1'),
61 (1, 1, 'n1'),
62 ])
62 ])
63 def test_diff_to_comment_line_number(old, new, expected):
63 def test_diff_to_comment_line_number(old, new, expected):
64 diff_line = DiffLineNumber(old=old, new=new)
64 diff_line = DiffLineNumber(old=old, new=new)
65 comment_line = comment._diff_to_comment_line_number(diff_line)
65 comment_line = comment._diff_to_comment_line_number(diff_line)
66 assert comment_line == expected
66 assert comment_line == expected
67
67
68
68
69 @pytest.mark.parametrize('diff_line, expected', [
69 @pytest.mark.parametrize('diff_line, expected', [
70 (DiffLineNumber(old=1, new=None), DiffLineNumber(old=2, new=None)),
70 (DiffLineNumber(old=1, new=None), DiffLineNumber(old=2, new=None)),
71 (DiffLineNumber(old=11, new=None), DiffLineNumber(old=2, new=None)),
71 (DiffLineNumber(old=11, new=None), DiffLineNumber(old=2, new=None)),
72 (DiffLineNumber(old=12, new=None), DiffLineNumber(old=21, new=None)),
72 (DiffLineNumber(old=12, new=None), DiffLineNumber(old=21, new=None)),
73 ])
73 ])
74 def test_choose_closest_diff_line_normal(diff_line, expected):
74 def test_choose_closest_diff_line_normal(diff_line, expected):
75 comment_model = comment.ChangesetCommentsModel()
75 comment_model = comment.CommentsModel()
76 candidates = [
76 candidates = [
77 DiffLineNumber(old=2, new=None),
77 DiffLineNumber(old=2, new=None),
78 DiffLineNumber(old=21, new=None),
78 DiffLineNumber(old=21, new=None),
79 ]
79 ]
80 result = comment_model._choose_closest_diff_line(diff_line, candidates)
80 result = comment_model._choose_closest_diff_line(diff_line, candidates)
81 assert result == expected
81 assert result == expected
82
82
83
83
84 def test_revision_comments_are_sorted():
84 def test_revision_comments_are_sorted():
85 comment_model = comment.ChangesetCommentsModel()
85 comment_model = comment.CommentsModel()
86 query = comment_model._get_inline_comments_query(
86 query = comment_model._get_inline_comments_query(
87 repo_id='fake_repo_name',
87 repo_id='fake_repo_name',
88 revision='fake_revision',
88 revision='fake_revision',
89 pull_request=None)
89 pull_request=None)
90 assert_inline_comments_order(query)
90 assert_inline_comments_order(query)
91
91
92
92
93 @pytest.mark.parametrize('use_outdated', [True, False])
93 @pytest.mark.parametrize('use_outdated', [True, False])
94 def test_pull_request_comments_are_sorted(use_outdated):
94 def test_pull_request_comments_are_sorted(use_outdated):
95 comment_model = comment.ChangesetCommentsModel()
95 comment_model = comment.CommentsModel()
96 pull_request = mock.Mock()
96 pull_request = mock.Mock()
97 # TODO: johbo: Had to do this since we have an inline call to
97 # TODO: johbo: Had to do this since we have an inline call to
98 # self.__get_pull_request. Should be moved out at some point.
98 # self.__get_pull_request. Should be moved out at some point.
99 get_instance_patcher = mock.patch.object(
99 get_instance_patcher = mock.patch.object(
100 comment.ChangesetCommentsModel, '_get_instance',
100 comment.CommentsModel, '_get_instance',
101 return_value=pull_request)
101 return_value=pull_request)
102 config_patcher = mock.patch.object(
102 config_patcher = mock.patch.object(
103 comment.ChangesetCommentsModel, 'use_outdated_comments',
103 comment.CommentsModel, 'use_outdated_comments',
104 return_value=use_outdated)
104 return_value=use_outdated)
105
105
106 with get_instance_patcher, config_patcher as config_mock:
106 with get_instance_patcher, config_patcher as config_mock:
107 query = comment_model._get_inline_comments_query(
107 query = comment_model._get_inline_comments_query(
108 repo_id='fake_repo_name',
108 repo_id='fake_repo_name',
109 revision=None,
109 revision=None,
110 pull_request=pull_request)
110 pull_request=pull_request)
111 config_mock.assert_called_once_with(pull_request)
111 config_mock.assert_called_once_with(pull_request)
112 assert_inline_comments_order(query)
112 assert_inline_comments_order(query)
113
113
114
114
115 def assert_inline_comments_order(query):
115 def assert_inline_comments_order(query):
116 """
116 """
117 Sorting by ID will make sure that the latest comments are at the bottom.
117 Sorting by ID will make sure that the latest comments are at the bottom.
118 """
118 """
119 order_by = query._order_by
119 order_by = query._order_by
120 assert order_by
120 assert order_by
121 assert len(order_by) == 1
121 assert len(order_by) == 1
122 assert str(order_by[0]) == 'changeset_comments.comment_id ASC'
122 assert str(order_by[0]) == 'changeset_comments.comment_id ASC'
123
123
124
124
125 def test_get_renderer():
125 def test_get_renderer():
126 model = comment.ChangesetCommentsModel()
126 model = comment.CommentsModel()
127 renderer = model._get_renderer()
127 renderer = model._get_renderer()
128 assert renderer == "rst"
128 assert renderer == "rst"
129
129
130
130
131 class TestUseOutdatedComments(object):
131 class TestUseOutdatedComments(object):
132 @pytest.mark.parametrize('use_outdated', [True, False])
132 @pytest.mark.parametrize('use_outdated', [True, False])
133 def test_returns_value_from_db(self, use_outdated):
133 def test_returns_value_from_db(self, use_outdated):
134 pull_request = mock.Mock()
134 pull_request = mock.Mock()
135
135
136 general_settings = {
136 general_settings = {
137 'rhodecode_use_outdated_comments': use_outdated
137 'rhodecode_use_outdated_comments': use_outdated
138 }
138 }
139 with self._patch_settings(general_settings) as settings_mock:
139 with self._patch_settings(general_settings) as settings_mock:
140 result = comment.ChangesetCommentsModel.use_outdated_comments(
140 result = comment.CommentsModel.use_outdated_comments(
141 pull_request)
141 pull_request)
142 settings_mock.assert_called_once_with(repo=pull_request.target_repo)
142 settings_mock.assert_called_once_with(repo=pull_request.target_repo)
143 assert result == use_outdated
143 assert result == use_outdated
144
144
145 def test_default_value(self):
145 def test_default_value(self):
146 pull_request = mock.Mock()
146 pull_request = mock.Mock()
147
147
148 general_settings = {}
148 general_settings = {}
149 with self._patch_settings(general_settings) as settings_mock:
149 with self._patch_settings(general_settings) as settings_mock:
150 result = comment.ChangesetCommentsModel.use_outdated_comments(
150 result = comment.CommentsModel.use_outdated_comments(
151 pull_request)
151 pull_request)
152 settings_mock.assert_called_once_with(repo=pull_request.target_repo)
152 settings_mock.assert_called_once_with(repo=pull_request.target_repo)
153 assert result is False
153 assert result is False
154
154
155 def _patch_settings(self, value):
155 def _patch_settings(self, value):
156 vcs_settings_mock = mock.Mock()
156 vcs_settings_mock = mock.Mock()
157 vcs_settings_mock.get_general_settings.return_value = value
157 vcs_settings_mock.get_general_settings.return_value = value
158 return mock.patch.object(
158 return mock.patch.object(
159 comment, 'VcsSettingsModel', return_value=vcs_settings_mock)
159 comment, 'VcsSettingsModel', return_value=vcs_settings_mock)
@@ -1,846 +1,846 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 mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import ChangesetCommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 class TestPullRequestModel:
44 class TestPullRequestModel:
45
45
46 @pytest.fixture
46 @pytest.fixture
47 def pull_request(self, request, backend, pr_util):
47 def pull_request(self, request, backend, pr_util):
48 """
48 """
49 A pull request combined with multiples patches.
49 A pull request combined with multiples patches.
50 """
50 """
51 BackendClass = get_backend(backend.alias)
51 BackendClass = get_backend(backend.alias)
52 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
52 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
53 self.workspace_remove_patcher = mock.patch.object(
53 self.workspace_remove_patcher = mock.patch.object(
54 BackendClass, 'cleanup_merge_workspace')
54 BackendClass, 'cleanup_merge_workspace')
55
55
56 self.workspace_remove_mock = self.workspace_remove_patcher.start()
56 self.workspace_remove_mock = self.workspace_remove_patcher.start()
57 self.merge_mock = self.merge_patcher.start()
57 self.merge_mock = self.merge_patcher.start()
58 self.comment_patcher = mock.patch(
58 self.comment_patcher = mock.patch(
59 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
59 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
60 self.comment_patcher.start()
60 self.comment_patcher.start()
61 self.notification_patcher = mock.patch(
61 self.notification_patcher = mock.patch(
62 'rhodecode.model.notification.NotificationModel.create')
62 'rhodecode.model.notification.NotificationModel.create')
63 self.notification_patcher.start()
63 self.notification_patcher.start()
64 self.helper_patcher = mock.patch(
64 self.helper_patcher = mock.patch(
65 'rhodecode.lib.helpers.url')
65 'rhodecode.lib.helpers.url')
66 self.helper_patcher.start()
66 self.helper_patcher.start()
67
67
68 self.hook_patcher = mock.patch.object(PullRequestModel,
68 self.hook_patcher = mock.patch.object(PullRequestModel,
69 '_trigger_pull_request_hook')
69 '_trigger_pull_request_hook')
70 self.hook_mock = self.hook_patcher.start()
70 self.hook_mock = self.hook_patcher.start()
71
71
72 self.invalidation_patcher = mock.patch(
72 self.invalidation_patcher = mock.patch(
73 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
73 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
74 self.invalidation_mock = self.invalidation_patcher.start()
74 self.invalidation_mock = self.invalidation_patcher.start()
75
75
76 self.pull_request = pr_util.create_pull_request(
76 self.pull_request = pr_util.create_pull_request(
77 mergeable=True, name_suffix=u'Δ…Δ‡')
77 mergeable=True, name_suffix=u'Δ…Δ‡')
78 self.source_commit = self.pull_request.source_ref_parts.commit_id
78 self.source_commit = self.pull_request.source_ref_parts.commit_id
79 self.target_commit = self.pull_request.target_ref_parts.commit_id
79 self.target_commit = self.pull_request.target_ref_parts.commit_id
80 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
80 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
81
81
82 @request.addfinalizer
82 @request.addfinalizer
83 def cleanup_pull_request():
83 def cleanup_pull_request():
84 calls = [mock.call(
84 calls = [mock.call(
85 self.pull_request, self.pull_request.author, 'create')]
85 self.pull_request, self.pull_request.author, 'create')]
86 self.hook_mock.assert_has_calls(calls)
86 self.hook_mock.assert_has_calls(calls)
87
87
88 self.workspace_remove_patcher.stop()
88 self.workspace_remove_patcher.stop()
89 self.merge_patcher.stop()
89 self.merge_patcher.stop()
90 self.comment_patcher.stop()
90 self.comment_patcher.stop()
91 self.notification_patcher.stop()
91 self.notification_patcher.stop()
92 self.helper_patcher.stop()
92 self.helper_patcher.stop()
93 self.hook_patcher.stop()
93 self.hook_patcher.stop()
94 self.invalidation_patcher.stop()
94 self.invalidation_patcher.stop()
95
95
96 return self.pull_request
96 return self.pull_request
97
97
98 def test_get_all(self, pull_request):
98 def test_get_all(self, pull_request):
99 prs = PullRequestModel().get_all(pull_request.target_repo)
99 prs = PullRequestModel().get_all(pull_request.target_repo)
100 assert isinstance(prs, list)
100 assert isinstance(prs, list)
101 assert len(prs) == 1
101 assert len(prs) == 1
102
102
103 def test_count_all(self, pull_request):
103 def test_count_all(self, pull_request):
104 pr_count = PullRequestModel().count_all(pull_request.target_repo)
104 pr_count = PullRequestModel().count_all(pull_request.target_repo)
105 assert pr_count == 1
105 assert pr_count == 1
106
106
107 def test_get_awaiting_review(self, pull_request):
107 def test_get_awaiting_review(self, pull_request):
108 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
108 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
109 assert isinstance(prs, list)
109 assert isinstance(prs, list)
110 assert len(prs) == 1
110 assert len(prs) == 1
111
111
112 def test_count_awaiting_review(self, pull_request):
112 def test_count_awaiting_review(self, pull_request):
113 pr_count = PullRequestModel().count_awaiting_review(
113 pr_count = PullRequestModel().count_awaiting_review(
114 pull_request.target_repo)
114 pull_request.target_repo)
115 assert pr_count == 1
115 assert pr_count == 1
116
116
117 def test_get_awaiting_my_review(self, pull_request):
117 def test_get_awaiting_my_review(self, pull_request):
118 PullRequestModel().update_reviewers(
118 PullRequestModel().update_reviewers(
119 pull_request, [(pull_request.author, ['author'])])
119 pull_request, [(pull_request.author, ['author'])])
120 prs = PullRequestModel().get_awaiting_my_review(
120 prs = PullRequestModel().get_awaiting_my_review(
121 pull_request.target_repo, user_id=pull_request.author.user_id)
121 pull_request.target_repo, user_id=pull_request.author.user_id)
122 assert isinstance(prs, list)
122 assert isinstance(prs, list)
123 assert len(prs) == 1
123 assert len(prs) == 1
124
124
125 def test_count_awaiting_my_review(self, pull_request):
125 def test_count_awaiting_my_review(self, pull_request):
126 PullRequestModel().update_reviewers(
126 PullRequestModel().update_reviewers(
127 pull_request, [(pull_request.author, ['author'])])
127 pull_request, [(pull_request.author, ['author'])])
128 pr_count = PullRequestModel().count_awaiting_my_review(
128 pr_count = PullRequestModel().count_awaiting_my_review(
129 pull_request.target_repo, user_id=pull_request.author.user_id)
129 pull_request.target_repo, user_id=pull_request.author.user_id)
130 assert pr_count == 1
130 assert pr_count == 1
131
131
132 def test_delete_calls_cleanup_merge(self, pull_request):
132 def test_delete_calls_cleanup_merge(self, pull_request):
133 PullRequestModel().delete(pull_request)
133 PullRequestModel().delete(pull_request)
134
134
135 self.workspace_remove_mock.assert_called_once_with(
135 self.workspace_remove_mock.assert_called_once_with(
136 self.workspace_id)
136 self.workspace_id)
137
137
138 def test_close_calls_cleanup_and_hook(self, pull_request):
138 def test_close_calls_cleanup_and_hook(self, pull_request):
139 PullRequestModel().close_pull_request(
139 PullRequestModel().close_pull_request(
140 pull_request, pull_request.author)
140 pull_request, pull_request.author)
141
141
142 self.workspace_remove_mock.assert_called_once_with(
142 self.workspace_remove_mock.assert_called_once_with(
143 self.workspace_id)
143 self.workspace_id)
144 self.hook_mock.assert_called_with(
144 self.hook_mock.assert_called_with(
145 self.pull_request, self.pull_request.author, 'close')
145 self.pull_request, self.pull_request.author, 'close')
146
146
147 def test_merge_status(self, pull_request):
147 def test_merge_status(self, pull_request):
148 self.merge_mock.return_value = MergeResponse(
148 self.merge_mock.return_value = MergeResponse(
149 True, False, None, MergeFailureReason.NONE)
149 True, False, None, MergeFailureReason.NONE)
150
150
151 assert pull_request._last_merge_source_rev is None
151 assert pull_request._last_merge_source_rev is None
152 assert pull_request._last_merge_target_rev is None
152 assert pull_request._last_merge_target_rev is None
153 assert pull_request._last_merge_status is None
153 assert pull_request._last_merge_status is None
154
154
155 status, msg = PullRequestModel().merge_status(pull_request)
155 status, msg = PullRequestModel().merge_status(pull_request)
156 assert status is True
156 assert status is True
157 assert msg.eval() == 'This pull request can be automatically merged.'
157 assert msg.eval() == 'This pull request can be automatically merged.'
158 self.merge_mock.assert_called_once_with(
158 self.merge_mock.assert_called_once_with(
159 pull_request.target_ref_parts,
159 pull_request.target_ref_parts,
160 pull_request.source_repo.scm_instance(),
160 pull_request.source_repo.scm_instance(),
161 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
161 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
162 use_rebase=False)
162 use_rebase=False)
163
163
164 assert pull_request._last_merge_source_rev == self.source_commit
164 assert pull_request._last_merge_source_rev == self.source_commit
165 assert pull_request._last_merge_target_rev == self.target_commit
165 assert pull_request._last_merge_target_rev == self.target_commit
166 assert pull_request._last_merge_status is MergeFailureReason.NONE
166 assert pull_request._last_merge_status is MergeFailureReason.NONE
167
167
168 self.merge_mock.reset_mock()
168 self.merge_mock.reset_mock()
169 status, msg = PullRequestModel().merge_status(pull_request)
169 status, msg = PullRequestModel().merge_status(pull_request)
170 assert status is True
170 assert status is True
171 assert msg.eval() == 'This pull request can be automatically merged.'
171 assert msg.eval() == 'This pull request can be automatically merged.'
172 assert self.merge_mock.called is False
172 assert self.merge_mock.called is False
173
173
174 def test_merge_status_known_failure(self, pull_request):
174 def test_merge_status_known_failure(self, pull_request):
175 self.merge_mock.return_value = MergeResponse(
175 self.merge_mock.return_value = MergeResponse(
176 False, False, None, MergeFailureReason.MERGE_FAILED)
176 False, False, None, MergeFailureReason.MERGE_FAILED)
177
177
178 assert pull_request._last_merge_source_rev is None
178 assert pull_request._last_merge_source_rev is None
179 assert pull_request._last_merge_target_rev is None
179 assert pull_request._last_merge_target_rev is None
180 assert pull_request._last_merge_status is None
180 assert pull_request._last_merge_status is None
181
181
182 status, msg = PullRequestModel().merge_status(pull_request)
182 status, msg = PullRequestModel().merge_status(pull_request)
183 assert status is False
183 assert status is False
184 assert (
184 assert (
185 msg.eval() ==
185 msg.eval() ==
186 'This pull request cannot be merged because of conflicts.')
186 'This pull request cannot be merged because of conflicts.')
187 self.merge_mock.assert_called_once_with(
187 self.merge_mock.assert_called_once_with(
188 pull_request.target_ref_parts,
188 pull_request.target_ref_parts,
189 pull_request.source_repo.scm_instance(),
189 pull_request.source_repo.scm_instance(),
190 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
190 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
191 use_rebase=False)
191 use_rebase=False)
192
192
193 assert pull_request._last_merge_source_rev == self.source_commit
193 assert pull_request._last_merge_source_rev == self.source_commit
194 assert pull_request._last_merge_target_rev == self.target_commit
194 assert pull_request._last_merge_target_rev == self.target_commit
195 assert (
195 assert (
196 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
196 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
197
197
198 self.merge_mock.reset_mock()
198 self.merge_mock.reset_mock()
199 status, msg = PullRequestModel().merge_status(pull_request)
199 status, msg = PullRequestModel().merge_status(pull_request)
200 assert status is False
200 assert status is False
201 assert (
201 assert (
202 msg.eval() ==
202 msg.eval() ==
203 'This pull request cannot be merged because of conflicts.')
203 'This pull request cannot be merged because of conflicts.')
204 assert self.merge_mock.called is False
204 assert self.merge_mock.called is False
205
205
206 def test_merge_status_unknown_failure(self, pull_request):
206 def test_merge_status_unknown_failure(self, pull_request):
207 self.merge_mock.return_value = MergeResponse(
207 self.merge_mock.return_value = MergeResponse(
208 False, False, None, MergeFailureReason.UNKNOWN)
208 False, False, None, MergeFailureReason.UNKNOWN)
209
209
210 assert pull_request._last_merge_source_rev is None
210 assert pull_request._last_merge_source_rev is None
211 assert pull_request._last_merge_target_rev is None
211 assert pull_request._last_merge_target_rev is None
212 assert pull_request._last_merge_status is None
212 assert pull_request._last_merge_status is None
213
213
214 status, msg = PullRequestModel().merge_status(pull_request)
214 status, msg = PullRequestModel().merge_status(pull_request)
215 assert status is False
215 assert status is False
216 assert msg.eval() == (
216 assert msg.eval() == (
217 'This pull request cannot be merged because of an unhandled'
217 'This pull request cannot be merged because of an unhandled'
218 ' exception.')
218 ' exception.')
219 self.merge_mock.assert_called_once_with(
219 self.merge_mock.assert_called_once_with(
220 pull_request.target_ref_parts,
220 pull_request.target_ref_parts,
221 pull_request.source_repo.scm_instance(),
221 pull_request.source_repo.scm_instance(),
222 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
222 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
223 use_rebase=False)
223 use_rebase=False)
224
224
225 assert pull_request._last_merge_source_rev is None
225 assert pull_request._last_merge_source_rev is None
226 assert pull_request._last_merge_target_rev is None
226 assert pull_request._last_merge_target_rev is None
227 assert pull_request._last_merge_status is None
227 assert pull_request._last_merge_status is None
228
228
229 self.merge_mock.reset_mock()
229 self.merge_mock.reset_mock()
230 status, msg = PullRequestModel().merge_status(pull_request)
230 status, msg = PullRequestModel().merge_status(pull_request)
231 assert status is False
231 assert status is False
232 assert msg.eval() == (
232 assert msg.eval() == (
233 'This pull request cannot be merged because of an unhandled'
233 'This pull request cannot be merged because of an unhandled'
234 ' exception.')
234 ' exception.')
235 assert self.merge_mock.called is True
235 assert self.merge_mock.called is True
236
236
237 def test_merge_status_when_target_is_locked(self, pull_request):
237 def test_merge_status_when_target_is_locked(self, pull_request):
238 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
238 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
239 status, msg = PullRequestModel().merge_status(pull_request)
239 status, msg = PullRequestModel().merge_status(pull_request)
240 assert status is False
240 assert status is False
241 assert msg.eval() == (
241 assert msg.eval() == (
242 'This pull request cannot be merged because the target repository'
242 'This pull request cannot be merged because the target repository'
243 ' is locked.')
243 ' is locked.')
244
244
245 def test_merge_status_requirements_check_target(self, pull_request):
245 def test_merge_status_requirements_check_target(self, pull_request):
246
246
247 def has_largefiles(self, repo):
247 def has_largefiles(self, repo):
248 return repo == pull_request.source_repo
248 return repo == pull_request.source_repo
249
249
250 patcher = mock.patch.object(
250 patcher = mock.patch.object(
251 PullRequestModel, '_has_largefiles', has_largefiles)
251 PullRequestModel, '_has_largefiles', has_largefiles)
252 with patcher:
252 with patcher:
253 status, msg = PullRequestModel().merge_status(pull_request)
253 status, msg = PullRequestModel().merge_status(pull_request)
254
254
255 assert status is False
255 assert status is False
256 assert msg == 'Target repository large files support is disabled.'
256 assert msg == 'Target repository large files support is disabled.'
257
257
258 def test_merge_status_requirements_check_source(self, pull_request):
258 def test_merge_status_requirements_check_source(self, pull_request):
259
259
260 def has_largefiles(self, repo):
260 def has_largefiles(self, repo):
261 return repo == pull_request.target_repo
261 return repo == pull_request.target_repo
262
262
263 patcher = mock.patch.object(
263 patcher = mock.patch.object(
264 PullRequestModel, '_has_largefiles', has_largefiles)
264 PullRequestModel, '_has_largefiles', has_largefiles)
265 with patcher:
265 with patcher:
266 status, msg = PullRequestModel().merge_status(pull_request)
266 status, msg = PullRequestModel().merge_status(pull_request)
267
267
268 assert status is False
268 assert status is False
269 assert msg == 'Source repository large files support is disabled.'
269 assert msg == 'Source repository large files support is disabled.'
270
270
271 def test_merge(self, pull_request, merge_extras):
271 def test_merge(self, pull_request, merge_extras):
272 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
272 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
273 merge_ref = Reference(
273 merge_ref = Reference(
274 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
274 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
275 self.merge_mock.return_value = MergeResponse(
275 self.merge_mock.return_value = MergeResponse(
276 True, True, merge_ref, MergeFailureReason.NONE)
276 True, True, merge_ref, MergeFailureReason.NONE)
277
277
278 merge_extras['repository'] = pull_request.target_repo.repo_name
278 merge_extras['repository'] = pull_request.target_repo.repo_name
279 PullRequestModel().merge(
279 PullRequestModel().merge(
280 pull_request, pull_request.author, extras=merge_extras)
280 pull_request, pull_request.author, extras=merge_extras)
281
281
282 message = (
282 message = (
283 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
283 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
284 u'\n\n {pr_title}'.format(
284 u'\n\n {pr_title}'.format(
285 pr_id=pull_request.pull_request_id,
285 pr_id=pull_request.pull_request_id,
286 source_repo=safe_unicode(
286 source_repo=safe_unicode(
287 pull_request.source_repo.scm_instance().name),
287 pull_request.source_repo.scm_instance().name),
288 source_ref_name=pull_request.source_ref_parts.name,
288 source_ref_name=pull_request.source_ref_parts.name,
289 pr_title=safe_unicode(pull_request.title)
289 pr_title=safe_unicode(pull_request.title)
290 )
290 )
291 )
291 )
292 self.merge_mock.assert_called_once_with(
292 self.merge_mock.assert_called_once_with(
293 pull_request.target_ref_parts,
293 pull_request.target_ref_parts,
294 pull_request.source_repo.scm_instance(),
294 pull_request.source_repo.scm_instance(),
295 pull_request.source_ref_parts, self.workspace_id,
295 pull_request.source_ref_parts, self.workspace_id,
296 user_name=user.username, user_email=user.email, message=message,
296 user_name=user.username, user_email=user.email, message=message,
297 use_rebase=False
297 use_rebase=False
298 )
298 )
299 self.invalidation_mock.assert_called_once_with(
299 self.invalidation_mock.assert_called_once_with(
300 pull_request.target_repo.repo_name)
300 pull_request.target_repo.repo_name)
301
301
302 self.hook_mock.assert_called_with(
302 self.hook_mock.assert_called_with(
303 self.pull_request, self.pull_request.author, 'merge')
303 self.pull_request, self.pull_request.author, 'merge')
304
304
305 pull_request = PullRequest.get(pull_request.pull_request_id)
305 pull_request = PullRequest.get(pull_request.pull_request_id)
306 assert (
306 assert (
307 pull_request.merge_rev ==
307 pull_request.merge_rev ==
308 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
308 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
309
309
310 def test_merge_failed(self, pull_request, merge_extras):
310 def test_merge_failed(self, pull_request, merge_extras):
311 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
311 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
312 merge_ref = Reference(
312 merge_ref = Reference(
313 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314 self.merge_mock.return_value = MergeResponse(
314 self.merge_mock.return_value = MergeResponse(
315 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
315 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
316
316
317 merge_extras['repository'] = pull_request.target_repo.repo_name
317 merge_extras['repository'] = pull_request.target_repo.repo_name
318 PullRequestModel().merge(
318 PullRequestModel().merge(
319 pull_request, pull_request.author, extras=merge_extras)
319 pull_request, pull_request.author, extras=merge_extras)
320
320
321 message = (
321 message = (
322 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
322 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
323 u'\n\n {pr_title}'.format(
323 u'\n\n {pr_title}'.format(
324 pr_id=pull_request.pull_request_id,
324 pr_id=pull_request.pull_request_id,
325 source_repo=safe_unicode(
325 source_repo=safe_unicode(
326 pull_request.source_repo.scm_instance().name),
326 pull_request.source_repo.scm_instance().name),
327 source_ref_name=pull_request.source_ref_parts.name,
327 source_ref_name=pull_request.source_ref_parts.name,
328 pr_title=safe_unicode(pull_request.title)
328 pr_title=safe_unicode(pull_request.title)
329 )
329 )
330 )
330 )
331 self.merge_mock.assert_called_once_with(
331 self.merge_mock.assert_called_once_with(
332 pull_request.target_ref_parts,
332 pull_request.target_ref_parts,
333 pull_request.source_repo.scm_instance(),
333 pull_request.source_repo.scm_instance(),
334 pull_request.source_ref_parts, self.workspace_id,
334 pull_request.source_ref_parts, self.workspace_id,
335 user_name=user.username, user_email=user.email, message=message,
335 user_name=user.username, user_email=user.email, message=message,
336 use_rebase=False
336 use_rebase=False
337 )
337 )
338
338
339 pull_request = PullRequest.get(pull_request.pull_request_id)
339 pull_request = PullRequest.get(pull_request.pull_request_id)
340 assert self.invalidation_mock.called is False
340 assert self.invalidation_mock.called is False
341 assert pull_request.merge_rev is None
341 assert pull_request.merge_rev is None
342
342
343 def test_get_commit_ids(self, pull_request):
343 def test_get_commit_ids(self, pull_request):
344 # The PR has been not merget yet, so expect an exception
344 # The PR has been not merget yet, so expect an exception
345 with pytest.raises(ValueError):
345 with pytest.raises(ValueError):
346 PullRequestModel()._get_commit_ids(pull_request)
346 PullRequestModel()._get_commit_ids(pull_request)
347
347
348 # Merge revision is in the revisions list
348 # Merge revision is in the revisions list
349 pull_request.merge_rev = pull_request.revisions[0]
349 pull_request.merge_rev = pull_request.revisions[0]
350 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
350 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
351 assert commit_ids == pull_request.revisions
351 assert commit_ids == pull_request.revisions
352
352
353 # Merge revision is not in the revisions list
353 # Merge revision is not in the revisions list
354 pull_request.merge_rev = 'f000' * 10
354 pull_request.merge_rev = 'f000' * 10
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
356 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
357
357
358 def test_get_diff_from_pr_version(self, pull_request):
358 def test_get_diff_from_pr_version(self, pull_request):
359 diff = PullRequestModel()._get_diff_from_pr_or_version(
359 diff = PullRequestModel()._get_diff_from_pr_or_version(
360 pull_request, context=6)
360 pull_request, context=6)
361 assert 'file_1' in diff.raw
361 assert 'file_1' in diff.raw
362
362
363 def test_generate_title_returns_unicode(self):
363 def test_generate_title_returns_unicode(self):
364 title = PullRequestModel().generate_pullrequest_title(
364 title = PullRequestModel().generate_pullrequest_title(
365 source='source-dummy',
365 source='source-dummy',
366 source_ref='source-ref-dummy',
366 source_ref='source-ref-dummy',
367 target='target-dummy',
367 target='target-dummy',
368 )
368 )
369 assert type(title) == unicode
369 assert type(title) == unicode
370
370
371
371
372 class TestIntegrationMerge(object):
372 class TestIntegrationMerge(object):
373 @pytest.mark.parametrize('extra_config', (
373 @pytest.mark.parametrize('extra_config', (
374 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
374 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
375 {'vcs.hooks.protocol': 'Pyro4', 'vcs.hooks.direct_calls': False},
375 {'vcs.hooks.protocol': 'Pyro4', 'vcs.hooks.direct_calls': False},
376 ))
376 ))
377 def test_merge_triggers_push_hooks(
377 def test_merge_triggers_push_hooks(
378 self, pr_util, user_admin, capture_rcextensions, merge_extras,
378 self, pr_util, user_admin, capture_rcextensions, merge_extras,
379 extra_config):
379 extra_config):
380 pull_request = pr_util.create_pull_request(
380 pull_request = pr_util.create_pull_request(
381 approved=True, mergeable=True)
381 approved=True, mergeable=True)
382 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
382 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
383 merge_extras['repository'] = pull_request.target_repo.repo_name
383 merge_extras['repository'] = pull_request.target_repo.repo_name
384 Session().commit()
384 Session().commit()
385
385
386 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
386 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
387 merge_state = PullRequestModel().merge(
387 merge_state = PullRequestModel().merge(
388 pull_request, user_admin, extras=merge_extras)
388 pull_request, user_admin, extras=merge_extras)
389
389
390 assert merge_state.executed
390 assert merge_state.executed
391 assert 'pre_push' in capture_rcextensions
391 assert 'pre_push' in capture_rcextensions
392 assert 'post_push' in capture_rcextensions
392 assert 'post_push' in capture_rcextensions
393
393
394 def test_merge_can_be_rejected_by_pre_push_hook(
394 def test_merge_can_be_rejected_by_pre_push_hook(
395 self, pr_util, user_admin, capture_rcextensions, merge_extras):
395 self, pr_util, user_admin, capture_rcextensions, merge_extras):
396 pull_request = pr_util.create_pull_request(
396 pull_request = pr_util.create_pull_request(
397 approved=True, mergeable=True)
397 approved=True, mergeable=True)
398 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
398 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
399 merge_extras['repository'] = pull_request.target_repo.repo_name
399 merge_extras['repository'] = pull_request.target_repo.repo_name
400 Session().commit()
400 Session().commit()
401
401
402 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
402 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
403 pre_pull.side_effect = RepositoryError("Disallow push!")
403 pre_pull.side_effect = RepositoryError("Disallow push!")
404 merge_status = PullRequestModel().merge(
404 merge_status = PullRequestModel().merge(
405 pull_request, user_admin, extras=merge_extras)
405 pull_request, user_admin, extras=merge_extras)
406
406
407 assert not merge_status.executed
407 assert not merge_status.executed
408 assert 'pre_push' not in capture_rcextensions
408 assert 'pre_push' not in capture_rcextensions
409 assert 'post_push' not in capture_rcextensions
409 assert 'post_push' not in capture_rcextensions
410
410
411 def test_merge_fails_if_target_is_locked(
411 def test_merge_fails_if_target_is_locked(
412 self, pr_util, user_regular, merge_extras):
412 self, pr_util, user_regular, merge_extras):
413 pull_request = pr_util.create_pull_request(
413 pull_request = pr_util.create_pull_request(
414 approved=True, mergeable=True)
414 approved=True, mergeable=True)
415 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
415 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
416 pull_request.target_repo.locked = locked_by
416 pull_request.target_repo.locked = locked_by
417 # TODO: johbo: Check if this can work based on the database, currently
417 # TODO: johbo: Check if this can work based on the database, currently
418 # all data is pre-computed, that's why just updating the DB is not
418 # all data is pre-computed, that's why just updating the DB is not
419 # enough.
419 # enough.
420 merge_extras['locked_by'] = locked_by
420 merge_extras['locked_by'] = locked_by
421 merge_extras['repository'] = pull_request.target_repo.repo_name
421 merge_extras['repository'] = pull_request.target_repo.repo_name
422 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
422 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
423 Session().commit()
423 Session().commit()
424 merge_status = PullRequestModel().merge(
424 merge_status = PullRequestModel().merge(
425 pull_request, user_regular, extras=merge_extras)
425 pull_request, user_regular, extras=merge_extras)
426 assert not merge_status.executed
426 assert not merge_status.executed
427
427
428
428
429 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
429 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
430 (False, 1, 0),
430 (False, 1, 0),
431 (True, 0, 1),
431 (True, 0, 1),
432 ])
432 ])
433 def test_outdated_comments(
433 def test_outdated_comments(
434 pr_util, use_outdated, inlines_count, outdated_count):
434 pr_util, use_outdated, inlines_count, outdated_count):
435 pull_request = pr_util.create_pull_request()
435 pull_request = pr_util.create_pull_request()
436 pr_util.create_inline_comment(file_path='not_in_updated_diff')
436 pr_util.create_inline_comment(file_path='not_in_updated_diff')
437
437
438 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
438 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
439 pr_util.add_one_commit()
439 pr_util.add_one_commit()
440 assert_inline_comments(
440 assert_inline_comments(
441 pull_request, visible=inlines_count, outdated=outdated_count)
441 pull_request, visible=inlines_count, outdated=outdated_count)
442 outdated_comment_mock.assert_called_with(pull_request)
442 outdated_comment_mock.assert_called_with(pull_request)
443
443
444
444
445 @pytest.fixture
445 @pytest.fixture
446 def merge_extras(user_regular):
446 def merge_extras(user_regular):
447 """
447 """
448 Context for the vcs operation when running a merge.
448 Context for the vcs operation when running a merge.
449 """
449 """
450 extras = {
450 extras = {
451 'ip': '127.0.0.1',
451 'ip': '127.0.0.1',
452 'username': user_regular.username,
452 'username': user_regular.username,
453 'action': 'push',
453 'action': 'push',
454 'repository': 'fake_target_repo_name',
454 'repository': 'fake_target_repo_name',
455 'scm': 'git',
455 'scm': 'git',
456 'config': 'fake_config_ini_path',
456 'config': 'fake_config_ini_path',
457 'make_lock': None,
457 'make_lock': None,
458 'locked_by': [None, None, None],
458 'locked_by': [None, None, None],
459 'server_url': 'http://test.example.com:5000',
459 'server_url': 'http://test.example.com:5000',
460 'hooks': ['push', 'pull'],
460 'hooks': ['push', 'pull'],
461 'is_shadow_repo': False,
461 'is_shadow_repo': False,
462 }
462 }
463 return extras
463 return extras
464
464
465
465
466 class TestUpdateCommentHandling(object):
466 class TestUpdateCommentHandling(object):
467
467
468 @pytest.fixture(autouse=True, scope='class')
468 @pytest.fixture(autouse=True, scope='class')
469 def enable_outdated_comments(self, request, pylonsapp):
469 def enable_outdated_comments(self, request, pylonsapp):
470 config_patch = mock.patch.dict(
470 config_patch = mock.patch.dict(
471 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
471 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
472 config_patch.start()
472 config_patch.start()
473
473
474 @request.addfinalizer
474 @request.addfinalizer
475 def cleanup():
475 def cleanup():
476 config_patch.stop()
476 config_patch.stop()
477
477
478 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
478 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
479 commits = [
479 commits = [
480 {'message': 'a'},
480 {'message': 'a'},
481 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
481 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
482 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
482 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
483 ]
483 ]
484 pull_request = pr_util.create_pull_request(
484 pull_request = pr_util.create_pull_request(
485 commits=commits, target_head='a', source_head='b', revisions=['b'])
485 commits=commits, target_head='a', source_head='b', revisions=['b'])
486 pr_util.create_inline_comment(file_path='file_b')
486 pr_util.create_inline_comment(file_path='file_b')
487 pr_util.add_one_commit(head='c')
487 pr_util.add_one_commit(head='c')
488
488
489 assert_inline_comments(pull_request, visible=1, outdated=0)
489 assert_inline_comments(pull_request, visible=1, outdated=0)
490
490
491 def test_comment_stays_unflagged_on_change_above(self, pr_util):
491 def test_comment_stays_unflagged_on_change_above(self, pr_util):
492 original_content = ''.join(
492 original_content = ''.join(
493 ['line {}\n'.format(x) for x in range(1, 11)])
493 ['line {}\n'.format(x) for x in range(1, 11)])
494 updated_content = 'new_line_at_top\n' + original_content
494 updated_content = 'new_line_at_top\n' + original_content
495 commits = [
495 commits = [
496 {'message': 'a'},
496 {'message': 'a'},
497 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
497 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
498 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
498 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
499 ]
499 ]
500 pull_request = pr_util.create_pull_request(
500 pull_request = pr_util.create_pull_request(
501 commits=commits, target_head='a', source_head='b', revisions=['b'])
501 commits=commits, target_head='a', source_head='b', revisions=['b'])
502
502
503 with outdated_comments_patcher():
503 with outdated_comments_patcher():
504 comment = pr_util.create_inline_comment(
504 comment = pr_util.create_inline_comment(
505 line_no=u'n8', file_path='file_b')
505 line_no=u'n8', file_path='file_b')
506 pr_util.add_one_commit(head='c')
506 pr_util.add_one_commit(head='c')
507
507
508 assert_inline_comments(pull_request, visible=1, outdated=0)
508 assert_inline_comments(pull_request, visible=1, outdated=0)
509 assert comment.line_no == u'n9'
509 assert comment.line_no == u'n9'
510
510
511 def test_comment_stays_unflagged_on_change_below(self, pr_util):
511 def test_comment_stays_unflagged_on_change_below(self, pr_util):
512 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
512 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
513 updated_content = original_content + 'new_line_at_end\n'
513 updated_content = original_content + 'new_line_at_end\n'
514 commits = [
514 commits = [
515 {'message': 'a'},
515 {'message': 'a'},
516 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
516 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
517 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
517 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
518 ]
518 ]
519 pull_request = pr_util.create_pull_request(
519 pull_request = pr_util.create_pull_request(
520 commits=commits, target_head='a', source_head='b', revisions=['b'])
520 commits=commits, target_head='a', source_head='b', revisions=['b'])
521 pr_util.create_inline_comment(file_path='file_b')
521 pr_util.create_inline_comment(file_path='file_b')
522 pr_util.add_one_commit(head='c')
522 pr_util.add_one_commit(head='c')
523
523
524 assert_inline_comments(pull_request, visible=1, outdated=0)
524 assert_inline_comments(pull_request, visible=1, outdated=0)
525
525
526 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
526 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
527 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
527 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
528 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
528 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
529 change_lines = list(base_lines)
529 change_lines = list(base_lines)
530 change_lines.insert(6, 'line 6a added\n')
530 change_lines.insert(6, 'line 6a added\n')
531
531
532 # Changes on the last line of sight
532 # Changes on the last line of sight
533 update_lines = list(change_lines)
533 update_lines = list(change_lines)
534 update_lines[0] = 'line 1 changed\n'
534 update_lines[0] = 'line 1 changed\n'
535 update_lines[-1] = 'line 12 changed\n'
535 update_lines[-1] = 'line 12 changed\n'
536
536
537 def file_b(lines):
537 def file_b(lines):
538 return FileNode('file_b', ''.join(lines))
538 return FileNode('file_b', ''.join(lines))
539
539
540 commits = [
540 commits = [
541 {'message': 'a', 'added': [file_b(base_lines)]},
541 {'message': 'a', 'added': [file_b(base_lines)]},
542 {'message': 'b', 'changed': [file_b(change_lines)]},
542 {'message': 'b', 'changed': [file_b(change_lines)]},
543 {'message': 'c', 'changed': [file_b(update_lines)]},
543 {'message': 'c', 'changed': [file_b(update_lines)]},
544 ]
544 ]
545
545
546 pull_request = pr_util.create_pull_request(
546 pull_request = pr_util.create_pull_request(
547 commits=commits, target_head='a', source_head='b', revisions=['b'])
547 commits=commits, target_head='a', source_head='b', revisions=['b'])
548 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
548 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
549
549
550 with outdated_comments_patcher():
550 with outdated_comments_patcher():
551 pr_util.add_one_commit(head='c')
551 pr_util.add_one_commit(head='c')
552 assert_inline_comments(pull_request, visible=0, outdated=1)
552 assert_inline_comments(pull_request, visible=0, outdated=1)
553
553
554 @pytest.mark.parametrize("change, content", [
554 @pytest.mark.parametrize("change, content", [
555 ('changed', 'changed\n'),
555 ('changed', 'changed\n'),
556 ('removed', ''),
556 ('removed', ''),
557 ], ids=['changed', 'removed'])
557 ], ids=['changed', 'removed'])
558 def test_comment_flagged_on_change(self, pr_util, change, content):
558 def test_comment_flagged_on_change(self, pr_util, change, content):
559 commits = [
559 commits = [
560 {'message': 'a'},
560 {'message': 'a'},
561 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
561 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
562 {'message': 'c', change: [FileNode('file_b', content)]},
562 {'message': 'c', change: [FileNode('file_b', content)]},
563 ]
563 ]
564 pull_request = pr_util.create_pull_request(
564 pull_request = pr_util.create_pull_request(
565 commits=commits, target_head='a', source_head='b', revisions=['b'])
565 commits=commits, target_head='a', source_head='b', revisions=['b'])
566 pr_util.create_inline_comment(file_path='file_b')
566 pr_util.create_inline_comment(file_path='file_b')
567
567
568 with outdated_comments_patcher():
568 with outdated_comments_patcher():
569 pr_util.add_one_commit(head='c')
569 pr_util.add_one_commit(head='c')
570 assert_inline_comments(pull_request, visible=0, outdated=1)
570 assert_inline_comments(pull_request, visible=0, outdated=1)
571
571
572
572
573 class TestUpdateChangedFiles(object):
573 class TestUpdateChangedFiles(object):
574
574
575 def test_no_changes_on_unchanged_diff(self, pr_util):
575 def test_no_changes_on_unchanged_diff(self, pr_util):
576 commits = [
576 commits = [
577 {'message': 'a'},
577 {'message': 'a'},
578 {'message': 'b',
578 {'message': 'b',
579 'added': [FileNode('file_b', 'test_content b\n')]},
579 'added': [FileNode('file_b', 'test_content b\n')]},
580 {'message': 'c',
580 {'message': 'c',
581 'added': [FileNode('file_c', 'test_content c\n')]},
581 'added': [FileNode('file_c', 'test_content c\n')]},
582 ]
582 ]
583 # open a PR from a to b, adding file_b
583 # open a PR from a to b, adding file_b
584 pull_request = pr_util.create_pull_request(
584 pull_request = pr_util.create_pull_request(
585 commits=commits, target_head='a', source_head='b', revisions=['b'],
585 commits=commits, target_head='a', source_head='b', revisions=['b'],
586 name_suffix='per-file-review')
586 name_suffix='per-file-review')
587
587
588 # modify PR adding new file file_c
588 # modify PR adding new file file_c
589 pr_util.add_one_commit(head='c')
589 pr_util.add_one_commit(head='c')
590
590
591 assert_pr_file_changes(
591 assert_pr_file_changes(
592 pull_request,
592 pull_request,
593 added=['file_c'],
593 added=['file_c'],
594 modified=[],
594 modified=[],
595 removed=[])
595 removed=[])
596
596
597 def test_modify_and_undo_modification_diff(self, pr_util):
597 def test_modify_and_undo_modification_diff(self, pr_util):
598 commits = [
598 commits = [
599 {'message': 'a'},
599 {'message': 'a'},
600 {'message': 'b',
600 {'message': 'b',
601 'added': [FileNode('file_b', 'test_content b\n')]},
601 'added': [FileNode('file_b', 'test_content b\n')]},
602 {'message': 'c',
602 {'message': 'c',
603 'changed': [FileNode('file_b', 'test_content b modified\n')]},
603 'changed': [FileNode('file_b', 'test_content b modified\n')]},
604 {'message': 'd',
604 {'message': 'd',
605 'changed': [FileNode('file_b', 'test_content b\n')]},
605 'changed': [FileNode('file_b', 'test_content b\n')]},
606 ]
606 ]
607 # open a PR from a to b, adding file_b
607 # open a PR from a to b, adding file_b
608 pull_request = pr_util.create_pull_request(
608 pull_request = pr_util.create_pull_request(
609 commits=commits, target_head='a', source_head='b', revisions=['b'],
609 commits=commits, target_head='a', source_head='b', revisions=['b'],
610 name_suffix='per-file-review')
610 name_suffix='per-file-review')
611
611
612 # modify PR modifying file file_b
612 # modify PR modifying file file_b
613 pr_util.add_one_commit(head='c')
613 pr_util.add_one_commit(head='c')
614
614
615 assert_pr_file_changes(
615 assert_pr_file_changes(
616 pull_request,
616 pull_request,
617 added=[],
617 added=[],
618 modified=['file_b'],
618 modified=['file_b'],
619 removed=[])
619 removed=[])
620
620
621 # move the head again to d, which rollbacks change,
621 # move the head again to d, which rollbacks change,
622 # meaning we should indicate no changes
622 # meaning we should indicate no changes
623 pr_util.add_one_commit(head='d')
623 pr_util.add_one_commit(head='d')
624
624
625 assert_pr_file_changes(
625 assert_pr_file_changes(
626 pull_request,
626 pull_request,
627 added=[],
627 added=[],
628 modified=[],
628 modified=[],
629 removed=[])
629 removed=[])
630
630
631 def test_updated_all_files_in_pr(self, pr_util):
631 def test_updated_all_files_in_pr(self, pr_util):
632 commits = [
632 commits = [
633 {'message': 'a'},
633 {'message': 'a'},
634 {'message': 'b', 'added': [
634 {'message': 'b', 'added': [
635 FileNode('file_a', 'test_content a\n'),
635 FileNode('file_a', 'test_content a\n'),
636 FileNode('file_b', 'test_content b\n'),
636 FileNode('file_b', 'test_content b\n'),
637 FileNode('file_c', 'test_content c\n')]},
637 FileNode('file_c', 'test_content c\n')]},
638 {'message': 'c', 'changed': [
638 {'message': 'c', 'changed': [
639 FileNode('file_a', 'test_content a changed\n'),
639 FileNode('file_a', 'test_content a changed\n'),
640 FileNode('file_b', 'test_content b changed\n'),
640 FileNode('file_b', 'test_content b changed\n'),
641 FileNode('file_c', 'test_content c changed\n')]},
641 FileNode('file_c', 'test_content c changed\n')]},
642 ]
642 ]
643 # open a PR from a to b, changing 3 files
643 # open a PR from a to b, changing 3 files
644 pull_request = pr_util.create_pull_request(
644 pull_request = pr_util.create_pull_request(
645 commits=commits, target_head='a', source_head='b', revisions=['b'],
645 commits=commits, target_head='a', source_head='b', revisions=['b'],
646 name_suffix='per-file-review')
646 name_suffix='per-file-review')
647
647
648 pr_util.add_one_commit(head='c')
648 pr_util.add_one_commit(head='c')
649
649
650 assert_pr_file_changes(
650 assert_pr_file_changes(
651 pull_request,
651 pull_request,
652 added=[],
652 added=[],
653 modified=['file_a', 'file_b', 'file_c'],
653 modified=['file_a', 'file_b', 'file_c'],
654 removed=[])
654 removed=[])
655
655
656 def test_updated_and_removed_all_files_in_pr(self, pr_util):
656 def test_updated_and_removed_all_files_in_pr(self, pr_util):
657 commits = [
657 commits = [
658 {'message': 'a'},
658 {'message': 'a'},
659 {'message': 'b', 'added': [
659 {'message': 'b', 'added': [
660 FileNode('file_a', 'test_content a\n'),
660 FileNode('file_a', 'test_content a\n'),
661 FileNode('file_b', 'test_content b\n'),
661 FileNode('file_b', 'test_content b\n'),
662 FileNode('file_c', 'test_content c\n')]},
662 FileNode('file_c', 'test_content c\n')]},
663 {'message': 'c', 'removed': [
663 {'message': 'c', 'removed': [
664 FileNode('file_a', 'test_content a changed\n'),
664 FileNode('file_a', 'test_content a changed\n'),
665 FileNode('file_b', 'test_content b changed\n'),
665 FileNode('file_b', 'test_content b changed\n'),
666 FileNode('file_c', 'test_content c changed\n')]},
666 FileNode('file_c', 'test_content c changed\n')]},
667 ]
667 ]
668 # open a PR from a to b, removing 3 files
668 # open a PR from a to b, removing 3 files
669 pull_request = pr_util.create_pull_request(
669 pull_request = pr_util.create_pull_request(
670 commits=commits, target_head='a', source_head='b', revisions=['b'],
670 commits=commits, target_head='a', source_head='b', revisions=['b'],
671 name_suffix='per-file-review')
671 name_suffix='per-file-review')
672
672
673 pr_util.add_one_commit(head='c')
673 pr_util.add_one_commit(head='c')
674
674
675 assert_pr_file_changes(
675 assert_pr_file_changes(
676 pull_request,
676 pull_request,
677 added=[],
677 added=[],
678 modified=[],
678 modified=[],
679 removed=['file_a', 'file_b', 'file_c'])
679 removed=['file_a', 'file_b', 'file_c'])
680
680
681
681
682 def test_update_writes_snapshot_into_pull_request_version(pr_util):
682 def test_update_writes_snapshot_into_pull_request_version(pr_util):
683 model = PullRequestModel()
683 model = PullRequestModel()
684 pull_request = pr_util.create_pull_request()
684 pull_request = pr_util.create_pull_request()
685 pr_util.update_source_repository()
685 pr_util.update_source_repository()
686
686
687 model.update_commits(pull_request)
687 model.update_commits(pull_request)
688
688
689 # Expect that it has a version entry now
689 # Expect that it has a version entry now
690 assert len(model.get_versions(pull_request)) == 1
690 assert len(model.get_versions(pull_request)) == 1
691
691
692
692
693 def test_update_skips_new_version_if_unchanged(pr_util):
693 def test_update_skips_new_version_if_unchanged(pr_util):
694 pull_request = pr_util.create_pull_request()
694 pull_request = pr_util.create_pull_request()
695 model = PullRequestModel()
695 model = PullRequestModel()
696 model.update_commits(pull_request)
696 model.update_commits(pull_request)
697
697
698 # Expect that it still has no versions
698 # Expect that it still has no versions
699 assert len(model.get_versions(pull_request)) == 0
699 assert len(model.get_versions(pull_request)) == 0
700
700
701
701
702 def test_update_assigns_comments_to_the_new_version(pr_util):
702 def test_update_assigns_comments_to_the_new_version(pr_util):
703 model = PullRequestModel()
703 model = PullRequestModel()
704 pull_request = pr_util.create_pull_request()
704 pull_request = pr_util.create_pull_request()
705 comment = pr_util.create_comment()
705 comment = pr_util.create_comment()
706 pr_util.update_source_repository()
706 pr_util.update_source_repository()
707
707
708 model.update_commits(pull_request)
708 model.update_commits(pull_request)
709
709
710 # Expect that the comment is linked to the pr version now
710 # Expect that the comment is linked to the pr version now
711 assert comment.pull_request_version == model.get_versions(pull_request)[0]
711 assert comment.pull_request_version == model.get_versions(pull_request)[0]
712
712
713
713
714 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util):
714 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util):
715 model = PullRequestModel()
715 model = PullRequestModel()
716 pull_request = pr_util.create_pull_request()
716 pull_request = pr_util.create_pull_request()
717 pr_util.update_source_repository()
717 pr_util.update_source_repository()
718 pr_util.update_source_repository()
718 pr_util.update_source_repository()
719
719
720 model.update_commits(pull_request)
720 model.update_commits(pull_request)
721
721
722 # Expect to find a new comment about the change
722 # Expect to find a new comment about the change
723 expected_message = textwrap.dedent(
723 expected_message = textwrap.dedent(
724 """\
724 """\
725 Pull request updated. Auto status change to |under_review|
725 Pull request updated. Auto status change to |under_review|
726
726
727 .. role:: added
727 .. role:: added
728 .. role:: removed
728 .. role:: removed
729 .. parsed-literal::
729 .. parsed-literal::
730
730
731 Changed commits:
731 Changed commits:
732 * :added:`1 added`
732 * :added:`1 added`
733 * :removed:`0 removed`
733 * :removed:`0 removed`
734
734
735 Changed files:
735 Changed files:
736 * `A file_2 <#a_c--92ed3b5f07b4>`_
736 * `A file_2 <#a_c--92ed3b5f07b4>`_
737
737
738 .. |under_review| replace:: *"Under Review"*"""
738 .. |under_review| replace:: *"Under Review"*"""
739 )
739 )
740 pull_request_comments = sorted(
740 pull_request_comments = sorted(
741 pull_request.comments, key=lambda c: c.modified_at)
741 pull_request.comments, key=lambda c: c.modified_at)
742 update_comment = pull_request_comments[-1]
742 update_comment = pull_request_comments[-1]
743 assert update_comment.text == expected_message
743 assert update_comment.text == expected_message
744
744
745
745
746 def test_create_version_from_snapshot_updates_attributes(pr_util):
746 def test_create_version_from_snapshot_updates_attributes(pr_util):
747 pull_request = pr_util.create_pull_request()
747 pull_request = pr_util.create_pull_request()
748
748
749 # Avoiding default values
749 # Avoiding default values
750 pull_request.status = PullRequest.STATUS_CLOSED
750 pull_request.status = PullRequest.STATUS_CLOSED
751 pull_request._last_merge_source_rev = "0" * 40
751 pull_request._last_merge_source_rev = "0" * 40
752 pull_request._last_merge_target_rev = "1" * 40
752 pull_request._last_merge_target_rev = "1" * 40
753 pull_request._last_merge_status = 1
753 pull_request._last_merge_status = 1
754 pull_request.merge_rev = "2" * 40
754 pull_request.merge_rev = "2" * 40
755
755
756 # Remember automatic values
756 # Remember automatic values
757 created_on = pull_request.created_on
757 created_on = pull_request.created_on
758 updated_on = pull_request.updated_on
758 updated_on = pull_request.updated_on
759
759
760 # Create a new version of the pull request
760 # Create a new version of the pull request
761 version = PullRequestModel()._create_version_from_snapshot(pull_request)
761 version = PullRequestModel()._create_version_from_snapshot(pull_request)
762
762
763 # Check attributes
763 # Check attributes
764 assert version.title == pr_util.create_parameters['title']
764 assert version.title == pr_util.create_parameters['title']
765 assert version.description == pr_util.create_parameters['description']
765 assert version.description == pr_util.create_parameters['description']
766 assert version.status == PullRequest.STATUS_CLOSED
766 assert version.status == PullRequest.STATUS_CLOSED
767
767
768 # versions get updated created_on
768 # versions get updated created_on
769 assert version.created_on != created_on
769 assert version.created_on != created_on
770
770
771 assert version.updated_on == updated_on
771 assert version.updated_on == updated_on
772 assert version.user_id == pull_request.user_id
772 assert version.user_id == pull_request.user_id
773 assert version.revisions == pr_util.create_parameters['revisions']
773 assert version.revisions == pr_util.create_parameters['revisions']
774 assert version.source_repo == pr_util.source_repository
774 assert version.source_repo == pr_util.source_repository
775 assert version.source_ref == pr_util.create_parameters['source_ref']
775 assert version.source_ref == pr_util.create_parameters['source_ref']
776 assert version.target_repo == pr_util.target_repository
776 assert version.target_repo == pr_util.target_repository
777 assert version.target_ref == pr_util.create_parameters['target_ref']
777 assert version.target_ref == pr_util.create_parameters['target_ref']
778 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
778 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
779 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
779 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
780 assert version._last_merge_status == pull_request._last_merge_status
780 assert version._last_merge_status == pull_request._last_merge_status
781 assert version.merge_rev == pull_request.merge_rev
781 assert version.merge_rev == pull_request.merge_rev
782 assert version.pull_request == pull_request
782 assert version.pull_request == pull_request
783
783
784
784
785 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util):
785 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util):
786 version1 = pr_util.create_version_of_pull_request()
786 version1 = pr_util.create_version_of_pull_request()
787 comment_linked = pr_util.create_comment(linked_to=version1)
787 comment_linked = pr_util.create_comment(linked_to=version1)
788 comment_unlinked = pr_util.create_comment()
788 comment_unlinked = pr_util.create_comment()
789 version2 = pr_util.create_version_of_pull_request()
789 version2 = pr_util.create_version_of_pull_request()
790
790
791 PullRequestModel()._link_comments_to_version(version2)
791 PullRequestModel()._link_comments_to_version(version2)
792
792
793 # Expect that only the new comment is linked to version2
793 # Expect that only the new comment is linked to version2
794 assert (
794 assert (
795 comment_unlinked.pull_request_version_id ==
795 comment_unlinked.pull_request_version_id ==
796 version2.pull_request_version_id)
796 version2.pull_request_version_id)
797 assert (
797 assert (
798 comment_linked.pull_request_version_id ==
798 comment_linked.pull_request_version_id ==
799 version1.pull_request_version_id)
799 version1.pull_request_version_id)
800 assert (
800 assert (
801 comment_unlinked.pull_request_version_id !=
801 comment_unlinked.pull_request_version_id !=
802 comment_linked.pull_request_version_id)
802 comment_linked.pull_request_version_id)
803
803
804
804
805 def test_calculate_commits():
805 def test_calculate_commits():
806 change = PullRequestModel()._calculate_commit_id_changes(
806 change = PullRequestModel()._calculate_commit_id_changes(
807 set([1, 2, 3]), set([1, 3, 4, 5]))
807 set([1, 2, 3]), set([1, 3, 4, 5]))
808 assert (set([4, 5]), set([1, 3]), set([2])) == (
808 assert (set([4, 5]), set([1, 3]), set([2])) == (
809 change.added, change.common, change.removed)
809 change.added, change.common, change.removed)
810
810
811
811
812 def assert_inline_comments(pull_request, visible=None, outdated=None):
812 def assert_inline_comments(pull_request, visible=None, outdated=None):
813 if visible is not None:
813 if visible is not None:
814 inline_comments = ChangesetCommentsModel().get_inline_comments(
814 inline_comments = CommentsModel().get_inline_comments(
815 pull_request.target_repo.repo_id, pull_request=pull_request)
815 pull_request.target_repo.repo_id, pull_request=pull_request)
816 inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
816 inline_cnt = CommentsModel().get_inline_comments_count(
817 inline_comments)
817 inline_comments)
818 assert inline_cnt == visible
818 assert inline_cnt == visible
819 if outdated is not None:
819 if outdated is not None:
820 outdated_comments = ChangesetCommentsModel().get_outdated_comments(
820 outdated_comments = CommentsModel().get_outdated_comments(
821 pull_request.target_repo.repo_id, pull_request)
821 pull_request.target_repo.repo_id, pull_request)
822 assert len(outdated_comments) == outdated
822 assert len(outdated_comments) == outdated
823
823
824
824
825 def assert_pr_file_changes(
825 def assert_pr_file_changes(
826 pull_request, added=None, modified=None, removed=None):
826 pull_request, added=None, modified=None, removed=None):
827 pr_versions = PullRequestModel().get_versions(pull_request)
827 pr_versions = PullRequestModel().get_versions(pull_request)
828 # always use first version, ie original PR to calculate changes
828 # always use first version, ie original PR to calculate changes
829 pull_request_version = pr_versions[0]
829 pull_request_version = pr_versions[0]
830 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
830 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
831 pull_request, pull_request_version)
831 pull_request, pull_request_version)
832 file_changes = PullRequestModel()._calculate_file_changes(
832 file_changes = PullRequestModel()._calculate_file_changes(
833 old_diff_data, new_diff_data)
833 old_diff_data, new_diff_data)
834
834
835 assert added == file_changes.added, \
835 assert added == file_changes.added, \
836 'expected added:%s vs value:%s' % (added, file_changes.added)
836 'expected added:%s vs value:%s' % (added, file_changes.added)
837 assert modified == file_changes.modified, \
837 assert modified == file_changes.modified, \
838 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
838 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
839 assert removed == file_changes.removed, \
839 assert removed == file_changes.removed, \
840 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
840 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
841
841
842
842
843 def outdated_comments_patcher(use_outdated=True):
843 def outdated_comments_patcher(use_outdated=True):
844 return mock.patch.object(
844 return mock.patch.object(
845 ChangesetCommentsModel, 'use_outdated_comments',
845 CommentsModel, 'use_outdated_comments',
846 return_value=use_outdated)
846 return_value=use_outdated)
@@ -1,1816 +1,1816 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 collections
21 import collections
22 import datetime
22 import datetime
23 import hashlib
23 import hashlib
24 import os
24 import os
25 import re
25 import re
26 import pprint
26 import pprint
27 import shutil
27 import shutil
28 import socket
28 import socket
29 import subprocess32
29 import subprocess32
30 import time
30 import time
31 import uuid
31 import uuid
32
32
33 import mock
33 import mock
34 import pyramid.testing
34 import pyramid.testing
35 import pytest
35 import pytest
36 import colander
36 import colander
37 import requests
37 import requests
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.lib.utils2 import AttributeDict
40 from rhodecode.lib.utils2 import AttributeDict
41 from rhodecode.model.changeset_status import ChangesetStatusModel
41 from rhodecode.model.changeset_status import ChangesetStatusModel
42 from rhodecode.model.comment import ChangesetCommentsModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
45 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
45 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.repo import RepoModel
48 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo_group import RepoGroupModel
49 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
51 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.integration import IntegrationModel
53 from rhodecode.model.integration import IntegrationModel
54 from rhodecode.integrations import integration_type_registry
54 from rhodecode.integrations import integration_type_registry
55 from rhodecode.integrations.types.base import IntegrationTypeBase
55 from rhodecode.integrations.types.base import IntegrationTypeBase
56 from rhodecode.lib.utils import repo2db_mapper
56 from rhodecode.lib.utils import repo2db_mapper
57 from rhodecode.lib.vcs import create_vcsserver_proxy
57 from rhodecode.lib.vcs import create_vcsserver_proxy
58 from rhodecode.lib.vcs.backends import get_backend
58 from rhodecode.lib.vcs.backends import get_backend
59 from rhodecode.lib.vcs.nodes import FileNode
59 from rhodecode.lib.vcs.nodes import FileNode
60 from rhodecode.tests import (
60 from rhodecode.tests import (
61 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
61 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
62 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
62 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
63 TEST_USER_REGULAR_PASS)
63 TEST_USER_REGULAR_PASS)
64 from rhodecode.tests.utils import CustomTestApp
64 from rhodecode.tests.utils import CustomTestApp
65 from rhodecode.tests.fixture import Fixture
65 from rhodecode.tests.fixture import Fixture
66
66
67
67
68 def _split_comma(value):
68 def _split_comma(value):
69 return value.split(',')
69 return value.split(',')
70
70
71
71
72 def pytest_addoption(parser):
72 def pytest_addoption(parser):
73 parser.addoption(
73 parser.addoption(
74 '--keep-tmp-path', action='store_true',
74 '--keep-tmp-path', action='store_true',
75 help="Keep the test temporary directories")
75 help="Keep the test temporary directories")
76 parser.addoption(
76 parser.addoption(
77 '--backends', action='store', type=_split_comma,
77 '--backends', action='store', type=_split_comma,
78 default=['git', 'hg', 'svn'],
78 default=['git', 'hg', 'svn'],
79 help="Select which backends to test for backend specific tests.")
79 help="Select which backends to test for backend specific tests.")
80 parser.addoption(
80 parser.addoption(
81 '--dbs', action='store', type=_split_comma,
81 '--dbs', action='store', type=_split_comma,
82 default=['sqlite'],
82 default=['sqlite'],
83 help="Select which database to test for database specific tests. "
83 help="Select which database to test for database specific tests. "
84 "Possible options are sqlite,postgres,mysql")
84 "Possible options are sqlite,postgres,mysql")
85 parser.addoption(
85 parser.addoption(
86 '--appenlight', '--ae', action='store_true',
86 '--appenlight', '--ae', action='store_true',
87 help="Track statistics in appenlight.")
87 help="Track statistics in appenlight.")
88 parser.addoption(
88 parser.addoption(
89 '--appenlight-api-key', '--ae-key',
89 '--appenlight-api-key', '--ae-key',
90 help="API key for Appenlight.")
90 help="API key for Appenlight.")
91 parser.addoption(
91 parser.addoption(
92 '--appenlight-url', '--ae-url',
92 '--appenlight-url', '--ae-url',
93 default="https://ae.rhodecode.com",
93 default="https://ae.rhodecode.com",
94 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
94 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
95 parser.addoption(
95 parser.addoption(
96 '--sqlite-connection-string', action='store',
96 '--sqlite-connection-string', action='store',
97 default='', help="Connection string for the dbs tests with SQLite")
97 default='', help="Connection string for the dbs tests with SQLite")
98 parser.addoption(
98 parser.addoption(
99 '--postgres-connection-string', action='store',
99 '--postgres-connection-string', action='store',
100 default='', help="Connection string for the dbs tests with Postgres")
100 default='', help="Connection string for the dbs tests with Postgres")
101 parser.addoption(
101 parser.addoption(
102 '--mysql-connection-string', action='store',
102 '--mysql-connection-string', action='store',
103 default='', help="Connection string for the dbs tests with MySQL")
103 default='', help="Connection string for the dbs tests with MySQL")
104 parser.addoption(
104 parser.addoption(
105 '--repeat', type=int, default=100,
105 '--repeat', type=int, default=100,
106 help="Number of repetitions in performance tests.")
106 help="Number of repetitions in performance tests.")
107
107
108
108
109 def pytest_configure(config):
109 def pytest_configure(config):
110 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
110 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
111 from rhodecode.config import patches
111 from rhodecode.config import patches
112 patches.kombu_1_5_1_python_2_7_11()
112 patches.kombu_1_5_1_python_2_7_11()
113
113
114
114
115 def pytest_collection_modifyitems(session, config, items):
115 def pytest_collection_modifyitems(session, config, items):
116 # nottest marked, compare nose, used for transition from nose to pytest
116 # nottest marked, compare nose, used for transition from nose to pytest
117 remaining = [
117 remaining = [
118 i for i in items if getattr(i.obj, '__test__', True)]
118 i for i in items if getattr(i.obj, '__test__', True)]
119 items[:] = remaining
119 items[:] = remaining
120
120
121
121
122 def pytest_generate_tests(metafunc):
122 def pytest_generate_tests(metafunc):
123 # Support test generation based on --backend parameter
123 # Support test generation based on --backend parameter
124 if 'backend_alias' in metafunc.fixturenames:
124 if 'backend_alias' in metafunc.fixturenames:
125 backends = get_backends_from_metafunc(metafunc)
125 backends = get_backends_from_metafunc(metafunc)
126 scope = None
126 scope = None
127 if not backends:
127 if not backends:
128 pytest.skip("Not enabled for any of selected backends")
128 pytest.skip("Not enabled for any of selected backends")
129 metafunc.parametrize('backend_alias', backends, scope=scope)
129 metafunc.parametrize('backend_alias', backends, scope=scope)
130 elif hasattr(metafunc.function, 'backends'):
130 elif hasattr(metafunc.function, 'backends'):
131 backends = get_backends_from_metafunc(metafunc)
131 backends = get_backends_from_metafunc(metafunc)
132 if not backends:
132 if not backends:
133 pytest.skip("Not enabled for any of selected backends")
133 pytest.skip("Not enabled for any of selected backends")
134
134
135
135
136 def get_backends_from_metafunc(metafunc):
136 def get_backends_from_metafunc(metafunc):
137 requested_backends = set(metafunc.config.getoption('--backends'))
137 requested_backends = set(metafunc.config.getoption('--backends'))
138 if hasattr(metafunc.function, 'backends'):
138 if hasattr(metafunc.function, 'backends'):
139 # Supported backends by this test function, created from
139 # Supported backends by this test function, created from
140 # pytest.mark.backends
140 # pytest.mark.backends
141 backends = metafunc.function.backends.args
141 backends = metafunc.function.backends.args
142 elif hasattr(metafunc.cls, 'backend_alias'):
142 elif hasattr(metafunc.cls, 'backend_alias'):
143 # Support class attribute "backend_alias", this is mainly
143 # Support class attribute "backend_alias", this is mainly
144 # for legacy reasons for tests not yet using pytest.mark.backends
144 # for legacy reasons for tests not yet using pytest.mark.backends
145 backends = [metafunc.cls.backend_alias]
145 backends = [metafunc.cls.backend_alias]
146 else:
146 else:
147 backends = metafunc.config.getoption('--backends')
147 backends = metafunc.config.getoption('--backends')
148 return requested_backends.intersection(backends)
148 return requested_backends.intersection(backends)
149
149
150
150
151 @pytest.fixture(scope='session', autouse=True)
151 @pytest.fixture(scope='session', autouse=True)
152 def activate_example_rcextensions(request):
152 def activate_example_rcextensions(request):
153 """
153 """
154 Patch in an example rcextensions module which verifies passed in kwargs.
154 Patch in an example rcextensions module which verifies passed in kwargs.
155 """
155 """
156 from rhodecode.tests.other import example_rcextensions
156 from rhodecode.tests.other import example_rcextensions
157
157
158 old_extensions = rhodecode.EXTENSIONS
158 old_extensions = rhodecode.EXTENSIONS
159 rhodecode.EXTENSIONS = example_rcextensions
159 rhodecode.EXTENSIONS = example_rcextensions
160
160
161 @request.addfinalizer
161 @request.addfinalizer
162 def cleanup():
162 def cleanup():
163 rhodecode.EXTENSIONS = old_extensions
163 rhodecode.EXTENSIONS = old_extensions
164
164
165
165
166 @pytest.fixture
166 @pytest.fixture
167 def capture_rcextensions():
167 def capture_rcextensions():
168 """
168 """
169 Returns the recorded calls to entry points in rcextensions.
169 Returns the recorded calls to entry points in rcextensions.
170 """
170 """
171 calls = rhodecode.EXTENSIONS.calls
171 calls = rhodecode.EXTENSIONS.calls
172 calls.clear()
172 calls.clear()
173 # Note: At this moment, it is still the empty dict, but that will
173 # Note: At this moment, it is still the empty dict, but that will
174 # be filled during the test run and since it is a reference this
174 # be filled during the test run and since it is a reference this
175 # is enough to make it work.
175 # is enough to make it work.
176 return calls
176 return calls
177
177
178
178
179 @pytest.fixture(scope='session')
179 @pytest.fixture(scope='session')
180 def http_environ_session():
180 def http_environ_session():
181 """
181 """
182 Allow to use "http_environ" in session scope.
182 Allow to use "http_environ" in session scope.
183 """
183 """
184 return http_environ(
184 return http_environ(
185 http_host_stub=http_host_stub())
185 http_host_stub=http_host_stub())
186
186
187
187
188 @pytest.fixture
188 @pytest.fixture
189 def http_host_stub():
189 def http_host_stub():
190 """
190 """
191 Value of HTTP_HOST in the test run.
191 Value of HTTP_HOST in the test run.
192 """
192 """
193 return 'test.example.com:80'
193 return 'test.example.com:80'
194
194
195
195
196 @pytest.fixture
196 @pytest.fixture
197 def http_environ(http_host_stub):
197 def http_environ(http_host_stub):
198 """
198 """
199 HTTP extra environ keys.
199 HTTP extra environ keys.
200
200
201 User by the test application and as well for setting up the pylons
201 User by the test application and as well for setting up the pylons
202 environment. In the case of the fixture "app" it should be possible
202 environment. In the case of the fixture "app" it should be possible
203 to override this for a specific test case.
203 to override this for a specific test case.
204 """
204 """
205 return {
205 return {
206 'SERVER_NAME': http_host_stub.split(':')[0],
206 'SERVER_NAME': http_host_stub.split(':')[0],
207 'SERVER_PORT': http_host_stub.split(':')[1],
207 'SERVER_PORT': http_host_stub.split(':')[1],
208 'HTTP_HOST': http_host_stub,
208 'HTTP_HOST': http_host_stub,
209 }
209 }
210
210
211
211
212 @pytest.fixture(scope='function')
212 @pytest.fixture(scope='function')
213 def app(request, pylonsapp, http_environ):
213 def app(request, pylonsapp, http_environ):
214
214
215
215
216 app = CustomTestApp(
216 app = CustomTestApp(
217 pylonsapp,
217 pylonsapp,
218 extra_environ=http_environ)
218 extra_environ=http_environ)
219 if request.cls:
219 if request.cls:
220 request.cls.app = app
220 request.cls.app = app
221 return app
221 return app
222
222
223
223
224 @pytest.fixture(scope='session')
224 @pytest.fixture(scope='session')
225 def app_settings(pylonsapp, pylons_config):
225 def app_settings(pylonsapp, pylons_config):
226 """
226 """
227 Settings dictionary used to create the app.
227 Settings dictionary used to create the app.
228
228
229 Parses the ini file and passes the result through the sanitize and apply
229 Parses the ini file and passes the result through the sanitize and apply
230 defaults mechanism in `rhodecode.config.middleware`.
230 defaults mechanism in `rhodecode.config.middleware`.
231 """
231 """
232 from paste.deploy.loadwsgi import loadcontext, APP
232 from paste.deploy.loadwsgi import loadcontext, APP
233 from rhodecode.config.middleware import (
233 from rhodecode.config.middleware import (
234 sanitize_settings_and_apply_defaults)
234 sanitize_settings_and_apply_defaults)
235 context = loadcontext(APP, 'config:' + pylons_config)
235 context = loadcontext(APP, 'config:' + pylons_config)
236 settings = sanitize_settings_and_apply_defaults(context.config())
236 settings = sanitize_settings_and_apply_defaults(context.config())
237 return settings
237 return settings
238
238
239
239
240 @pytest.fixture(scope='session')
240 @pytest.fixture(scope='session')
241 def db(app_settings):
241 def db(app_settings):
242 """
242 """
243 Initializes the database connection.
243 Initializes the database connection.
244
244
245 It uses the same settings which are used to create the ``pylonsapp`` or
245 It uses the same settings which are used to create the ``pylonsapp`` or
246 ``app`` fixtures.
246 ``app`` fixtures.
247 """
247 """
248 from rhodecode.config.utils import initialize_database
248 from rhodecode.config.utils import initialize_database
249 initialize_database(app_settings)
249 initialize_database(app_settings)
250
250
251
251
252 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
252 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
253
253
254
254
255 def _autologin_user(app, *args):
255 def _autologin_user(app, *args):
256 session = login_user_session(app, *args)
256 session = login_user_session(app, *args)
257 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
257 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
258 return LoginData(csrf_token, session['rhodecode_user'])
258 return LoginData(csrf_token, session['rhodecode_user'])
259
259
260
260
261 @pytest.fixture
261 @pytest.fixture
262 def autologin_user(app):
262 def autologin_user(app):
263 """
263 """
264 Utility fixture which makes sure that the admin user is logged in
264 Utility fixture which makes sure that the admin user is logged in
265 """
265 """
266 return _autologin_user(app)
266 return _autologin_user(app)
267
267
268
268
269 @pytest.fixture
269 @pytest.fixture
270 def autologin_regular_user(app):
270 def autologin_regular_user(app):
271 """
271 """
272 Utility fixture which makes sure that the regular user is logged in
272 Utility fixture which makes sure that the regular user is logged in
273 """
273 """
274 return _autologin_user(
274 return _autologin_user(
275 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
275 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
276
276
277
277
278 @pytest.fixture(scope='function')
278 @pytest.fixture(scope='function')
279 def csrf_token(request, autologin_user):
279 def csrf_token(request, autologin_user):
280 return autologin_user.csrf_token
280 return autologin_user.csrf_token
281
281
282
282
283 @pytest.fixture(scope='function')
283 @pytest.fixture(scope='function')
284 def xhr_header(request):
284 def xhr_header(request):
285 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
285 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
286
286
287
287
288 @pytest.fixture
288 @pytest.fixture
289 def real_crypto_backend(monkeypatch):
289 def real_crypto_backend(monkeypatch):
290 """
290 """
291 Switch the production crypto backend on for this test.
291 Switch the production crypto backend on for this test.
292
292
293 During the test run the crypto backend is replaced with a faster
293 During the test run the crypto backend is replaced with a faster
294 implementation based on the MD5 algorithm.
294 implementation based on the MD5 algorithm.
295 """
295 """
296 monkeypatch.setattr(rhodecode, 'is_test', False)
296 monkeypatch.setattr(rhodecode, 'is_test', False)
297
297
298
298
299 @pytest.fixture(scope='class')
299 @pytest.fixture(scope='class')
300 def index_location(request, pylonsapp):
300 def index_location(request, pylonsapp):
301 index_location = pylonsapp.config['app_conf']['search.location']
301 index_location = pylonsapp.config['app_conf']['search.location']
302 if request.cls:
302 if request.cls:
303 request.cls.index_location = index_location
303 request.cls.index_location = index_location
304 return index_location
304 return index_location
305
305
306
306
307 @pytest.fixture(scope='session', autouse=True)
307 @pytest.fixture(scope='session', autouse=True)
308 def tests_tmp_path(request):
308 def tests_tmp_path(request):
309 """
309 """
310 Create temporary directory to be used during the test session.
310 Create temporary directory to be used during the test session.
311 """
311 """
312 if not os.path.exists(TESTS_TMP_PATH):
312 if not os.path.exists(TESTS_TMP_PATH):
313 os.makedirs(TESTS_TMP_PATH)
313 os.makedirs(TESTS_TMP_PATH)
314
314
315 if not request.config.getoption('--keep-tmp-path'):
315 if not request.config.getoption('--keep-tmp-path'):
316 @request.addfinalizer
316 @request.addfinalizer
317 def remove_tmp_path():
317 def remove_tmp_path():
318 shutil.rmtree(TESTS_TMP_PATH)
318 shutil.rmtree(TESTS_TMP_PATH)
319
319
320 return TESTS_TMP_PATH
320 return TESTS_TMP_PATH
321
321
322
322
323 @pytest.fixture(scope='session', autouse=True)
323 @pytest.fixture(scope='session', autouse=True)
324 def patch_pyro_request_scope_proxy_factory(request):
324 def patch_pyro_request_scope_proxy_factory(request):
325 """
325 """
326 Patch the pyro proxy factory to always use the same dummy request object
326 Patch the pyro proxy factory to always use the same dummy request object
327 when under test. This will return the same pyro proxy on every call.
327 when under test. This will return the same pyro proxy on every call.
328 """
328 """
329 dummy_request = pyramid.testing.DummyRequest()
329 dummy_request = pyramid.testing.DummyRequest()
330
330
331 def mocked_call(self, request=None):
331 def mocked_call(self, request=None):
332 return self.getProxy(request=dummy_request)
332 return self.getProxy(request=dummy_request)
333
333
334 patcher = mock.patch(
334 patcher = mock.patch(
335 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
335 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
336 new=mocked_call)
336 new=mocked_call)
337 patcher.start()
337 patcher.start()
338
338
339 @request.addfinalizer
339 @request.addfinalizer
340 def undo_patching():
340 def undo_patching():
341 patcher.stop()
341 patcher.stop()
342
342
343
343
344 @pytest.fixture
344 @pytest.fixture
345 def test_repo_group(request):
345 def test_repo_group(request):
346 """
346 """
347 Create a temporary repository group, and destroy it after
347 Create a temporary repository group, and destroy it after
348 usage automatically
348 usage automatically
349 """
349 """
350 fixture = Fixture()
350 fixture = Fixture()
351 repogroupid = 'test_repo_group_%s' % int(time.time())
351 repogroupid = 'test_repo_group_%s' % int(time.time())
352 repo_group = fixture.create_repo_group(repogroupid)
352 repo_group = fixture.create_repo_group(repogroupid)
353
353
354 def _cleanup():
354 def _cleanup():
355 fixture.destroy_repo_group(repogroupid)
355 fixture.destroy_repo_group(repogroupid)
356
356
357 request.addfinalizer(_cleanup)
357 request.addfinalizer(_cleanup)
358 return repo_group
358 return repo_group
359
359
360
360
361 @pytest.fixture
361 @pytest.fixture
362 def test_user_group(request):
362 def test_user_group(request):
363 """
363 """
364 Create a temporary user group, and destroy it after
364 Create a temporary user group, and destroy it after
365 usage automatically
365 usage automatically
366 """
366 """
367 fixture = Fixture()
367 fixture = Fixture()
368 usergroupid = 'test_user_group_%s' % int(time.time())
368 usergroupid = 'test_user_group_%s' % int(time.time())
369 user_group = fixture.create_user_group(usergroupid)
369 user_group = fixture.create_user_group(usergroupid)
370
370
371 def _cleanup():
371 def _cleanup():
372 fixture.destroy_user_group(user_group)
372 fixture.destroy_user_group(user_group)
373
373
374 request.addfinalizer(_cleanup)
374 request.addfinalizer(_cleanup)
375 return user_group
375 return user_group
376
376
377
377
378 @pytest.fixture(scope='session')
378 @pytest.fixture(scope='session')
379 def test_repo(request):
379 def test_repo(request):
380 container = TestRepoContainer()
380 container = TestRepoContainer()
381 request.addfinalizer(container._cleanup)
381 request.addfinalizer(container._cleanup)
382 return container
382 return container
383
383
384
384
385 class TestRepoContainer(object):
385 class TestRepoContainer(object):
386 """
386 """
387 Container for test repositories which are used read only.
387 Container for test repositories which are used read only.
388
388
389 Repositories will be created on demand and re-used during the lifetime
389 Repositories will be created on demand and re-used during the lifetime
390 of this object.
390 of this object.
391
391
392 Usage to get the svn test repository "minimal"::
392 Usage to get the svn test repository "minimal"::
393
393
394 test_repo = TestContainer()
394 test_repo = TestContainer()
395 repo = test_repo('minimal', 'svn')
395 repo = test_repo('minimal', 'svn')
396
396
397 """
397 """
398
398
399 dump_extractors = {
399 dump_extractors = {
400 'git': utils.extract_git_repo_from_dump,
400 'git': utils.extract_git_repo_from_dump,
401 'hg': utils.extract_hg_repo_from_dump,
401 'hg': utils.extract_hg_repo_from_dump,
402 'svn': utils.extract_svn_repo_from_dump,
402 'svn': utils.extract_svn_repo_from_dump,
403 }
403 }
404
404
405 def __init__(self):
405 def __init__(self):
406 self._cleanup_repos = []
406 self._cleanup_repos = []
407 self._fixture = Fixture()
407 self._fixture = Fixture()
408 self._repos = {}
408 self._repos = {}
409
409
410 def __call__(self, dump_name, backend_alias):
410 def __call__(self, dump_name, backend_alias):
411 key = (dump_name, backend_alias)
411 key = (dump_name, backend_alias)
412 if key not in self._repos:
412 if key not in self._repos:
413 repo = self._create_repo(dump_name, backend_alias)
413 repo = self._create_repo(dump_name, backend_alias)
414 self._repos[key] = repo.repo_id
414 self._repos[key] = repo.repo_id
415 return Repository.get(self._repos[key])
415 return Repository.get(self._repos[key])
416
416
417 def _create_repo(self, dump_name, backend_alias):
417 def _create_repo(self, dump_name, backend_alias):
418 repo_name = '%s-%s' % (backend_alias, dump_name)
418 repo_name = '%s-%s' % (backend_alias, dump_name)
419 backend_class = get_backend(backend_alias)
419 backend_class = get_backend(backend_alias)
420 dump_extractor = self.dump_extractors[backend_alias]
420 dump_extractor = self.dump_extractors[backend_alias]
421 repo_path = dump_extractor(dump_name, repo_name)
421 repo_path = dump_extractor(dump_name, repo_name)
422 vcs_repo = backend_class(repo_path)
422 vcs_repo = backend_class(repo_path)
423 repo2db_mapper({repo_name: vcs_repo})
423 repo2db_mapper({repo_name: vcs_repo})
424 repo = RepoModel().get_by_repo_name(repo_name)
424 repo = RepoModel().get_by_repo_name(repo_name)
425 self._cleanup_repos.append(repo_name)
425 self._cleanup_repos.append(repo_name)
426 return repo
426 return repo
427
427
428 def _cleanup(self):
428 def _cleanup(self):
429 for repo_name in reversed(self._cleanup_repos):
429 for repo_name in reversed(self._cleanup_repos):
430 self._fixture.destroy_repo(repo_name)
430 self._fixture.destroy_repo(repo_name)
431
431
432
432
433 @pytest.fixture
433 @pytest.fixture
434 def backend(request, backend_alias, pylonsapp, test_repo):
434 def backend(request, backend_alias, pylonsapp, test_repo):
435 """
435 """
436 Parametrized fixture which represents a single backend implementation.
436 Parametrized fixture which represents a single backend implementation.
437
437
438 It respects the option `--backends` to focus the test run on specific
438 It respects the option `--backends` to focus the test run on specific
439 backend implementations.
439 backend implementations.
440
440
441 It also supports `pytest.mark.xfail_backends` to mark tests as failing
441 It also supports `pytest.mark.xfail_backends` to mark tests as failing
442 for specific backends. This is intended as a utility for incremental
442 for specific backends. This is intended as a utility for incremental
443 development of a new backend implementation.
443 development of a new backend implementation.
444 """
444 """
445 if backend_alias not in request.config.getoption('--backends'):
445 if backend_alias not in request.config.getoption('--backends'):
446 pytest.skip("Backend %s not selected." % (backend_alias, ))
446 pytest.skip("Backend %s not selected." % (backend_alias, ))
447
447
448 utils.check_xfail_backends(request.node, backend_alias)
448 utils.check_xfail_backends(request.node, backend_alias)
449 utils.check_skip_backends(request.node, backend_alias)
449 utils.check_skip_backends(request.node, backend_alias)
450
450
451 repo_name = 'vcs_test_%s' % (backend_alias, )
451 repo_name = 'vcs_test_%s' % (backend_alias, )
452 backend = Backend(
452 backend = Backend(
453 alias=backend_alias,
453 alias=backend_alias,
454 repo_name=repo_name,
454 repo_name=repo_name,
455 test_name=request.node.name,
455 test_name=request.node.name,
456 test_repo_container=test_repo)
456 test_repo_container=test_repo)
457 request.addfinalizer(backend.cleanup)
457 request.addfinalizer(backend.cleanup)
458 return backend
458 return backend
459
459
460
460
461 @pytest.fixture
461 @pytest.fixture
462 def backend_git(request, pylonsapp, test_repo):
462 def backend_git(request, pylonsapp, test_repo):
463 return backend(request, 'git', pylonsapp, test_repo)
463 return backend(request, 'git', pylonsapp, test_repo)
464
464
465
465
466 @pytest.fixture
466 @pytest.fixture
467 def backend_hg(request, pylonsapp, test_repo):
467 def backend_hg(request, pylonsapp, test_repo):
468 return backend(request, 'hg', pylonsapp, test_repo)
468 return backend(request, 'hg', pylonsapp, test_repo)
469
469
470
470
471 @pytest.fixture
471 @pytest.fixture
472 def backend_svn(request, pylonsapp, test_repo):
472 def backend_svn(request, pylonsapp, test_repo):
473 return backend(request, 'svn', pylonsapp, test_repo)
473 return backend(request, 'svn', pylonsapp, test_repo)
474
474
475
475
476 @pytest.fixture
476 @pytest.fixture
477 def backend_random(backend_git):
477 def backend_random(backend_git):
478 """
478 """
479 Use this to express that your tests need "a backend.
479 Use this to express that your tests need "a backend.
480
480
481 A few of our tests need a backend, so that we can run the code. This
481 A few of our tests need a backend, so that we can run the code. This
482 fixture is intended to be used for such cases. It will pick one of the
482 fixture is intended to be used for such cases. It will pick one of the
483 backends and run the tests.
483 backends and run the tests.
484
484
485 The fixture `backend` would run the test multiple times for each
485 The fixture `backend` would run the test multiple times for each
486 available backend which is a pure waste of time if the test is
486 available backend which is a pure waste of time if the test is
487 independent of the backend type.
487 independent of the backend type.
488 """
488 """
489 # TODO: johbo: Change this to pick a random backend
489 # TODO: johbo: Change this to pick a random backend
490 return backend_git
490 return backend_git
491
491
492
492
493 @pytest.fixture
493 @pytest.fixture
494 def backend_stub(backend_git):
494 def backend_stub(backend_git):
495 """
495 """
496 Use this to express that your tests need a backend stub
496 Use this to express that your tests need a backend stub
497
497
498 TODO: mikhail: Implement a real stub logic instead of returning
498 TODO: mikhail: Implement a real stub logic instead of returning
499 a git backend
499 a git backend
500 """
500 """
501 return backend_git
501 return backend_git
502
502
503
503
504 @pytest.fixture
504 @pytest.fixture
505 def repo_stub(backend_stub):
505 def repo_stub(backend_stub):
506 """
506 """
507 Use this to express that your tests need a repository stub
507 Use this to express that your tests need a repository stub
508 """
508 """
509 return backend_stub.create_repo()
509 return backend_stub.create_repo()
510
510
511
511
512 class Backend(object):
512 class Backend(object):
513 """
513 """
514 Represents the test configuration for one supported backend
514 Represents the test configuration for one supported backend
515
515
516 Provides easy access to different test repositories based on
516 Provides easy access to different test repositories based on
517 `__getitem__`. Such repositories will only be created once per test
517 `__getitem__`. Such repositories will only be created once per test
518 session.
518 session.
519 """
519 """
520
520
521 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
521 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
522 _master_repo = None
522 _master_repo = None
523 _commit_ids = {}
523 _commit_ids = {}
524
524
525 def __init__(self, alias, repo_name, test_name, test_repo_container):
525 def __init__(self, alias, repo_name, test_name, test_repo_container):
526 self.alias = alias
526 self.alias = alias
527 self.repo_name = repo_name
527 self.repo_name = repo_name
528 self._cleanup_repos = []
528 self._cleanup_repos = []
529 self._test_name = test_name
529 self._test_name = test_name
530 self._test_repo_container = test_repo_container
530 self._test_repo_container = test_repo_container
531 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
531 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
532 # Fixture will survive in the end.
532 # Fixture will survive in the end.
533 self._fixture = Fixture()
533 self._fixture = Fixture()
534
534
535 def __getitem__(self, key):
535 def __getitem__(self, key):
536 return self._test_repo_container(key, self.alias)
536 return self._test_repo_container(key, self.alias)
537
537
538 @property
538 @property
539 def repo(self):
539 def repo(self):
540 """
540 """
541 Returns the "current" repository. This is the vcs_test repo or the
541 Returns the "current" repository. This is the vcs_test repo or the
542 last repo which has been created with `create_repo`.
542 last repo which has been created with `create_repo`.
543 """
543 """
544 from rhodecode.model.db import Repository
544 from rhodecode.model.db import Repository
545 return Repository.get_by_repo_name(self.repo_name)
545 return Repository.get_by_repo_name(self.repo_name)
546
546
547 @property
547 @property
548 def default_branch_name(self):
548 def default_branch_name(self):
549 VcsRepository = get_backend(self.alias)
549 VcsRepository = get_backend(self.alias)
550 return VcsRepository.DEFAULT_BRANCH_NAME
550 return VcsRepository.DEFAULT_BRANCH_NAME
551
551
552 @property
552 @property
553 def default_head_id(self):
553 def default_head_id(self):
554 """
554 """
555 Returns the default head id of the underlying backend.
555 Returns the default head id of the underlying backend.
556
556
557 This will be the default branch name in case the backend does have a
557 This will be the default branch name in case the backend does have a
558 default branch. In the other cases it will point to a valid head
558 default branch. In the other cases it will point to a valid head
559 which can serve as the base to create a new commit on top of it.
559 which can serve as the base to create a new commit on top of it.
560 """
560 """
561 vcsrepo = self.repo.scm_instance()
561 vcsrepo = self.repo.scm_instance()
562 head_id = (
562 head_id = (
563 vcsrepo.DEFAULT_BRANCH_NAME or
563 vcsrepo.DEFAULT_BRANCH_NAME or
564 vcsrepo.commit_ids[-1])
564 vcsrepo.commit_ids[-1])
565 return head_id
565 return head_id
566
566
567 @property
567 @property
568 def commit_ids(self):
568 def commit_ids(self):
569 """
569 """
570 Returns the list of commits for the last created repository
570 Returns the list of commits for the last created repository
571 """
571 """
572 return self._commit_ids
572 return self._commit_ids
573
573
574 def create_master_repo(self, commits):
574 def create_master_repo(self, commits):
575 """
575 """
576 Create a repository and remember it as a template.
576 Create a repository and remember it as a template.
577
577
578 This allows to easily create derived repositories to construct
578 This allows to easily create derived repositories to construct
579 more complex scenarios for diff, compare and pull requests.
579 more complex scenarios for diff, compare and pull requests.
580
580
581 Returns a commit map which maps from commit message to raw_id.
581 Returns a commit map which maps from commit message to raw_id.
582 """
582 """
583 self._master_repo = self.create_repo(commits=commits)
583 self._master_repo = self.create_repo(commits=commits)
584 return self._commit_ids
584 return self._commit_ids
585
585
586 def create_repo(
586 def create_repo(
587 self, commits=None, number_of_commits=0, heads=None,
587 self, commits=None, number_of_commits=0, heads=None,
588 name_suffix=u'', **kwargs):
588 name_suffix=u'', **kwargs):
589 """
589 """
590 Create a repository and record it for later cleanup.
590 Create a repository and record it for later cleanup.
591
591
592 :param commits: Optional. A sequence of dict instances.
592 :param commits: Optional. A sequence of dict instances.
593 Will add a commit per entry to the new repository.
593 Will add a commit per entry to the new repository.
594 :param number_of_commits: Optional. If set to a number, this number of
594 :param number_of_commits: Optional. If set to a number, this number of
595 commits will be added to the new repository.
595 commits will be added to the new repository.
596 :param heads: Optional. Can be set to a sequence of of commit
596 :param heads: Optional. Can be set to a sequence of of commit
597 names which shall be pulled in from the master repository.
597 names which shall be pulled in from the master repository.
598
598
599 """
599 """
600 self.repo_name = self._next_repo_name() + name_suffix
600 self.repo_name = self._next_repo_name() + name_suffix
601 repo = self._fixture.create_repo(
601 repo = self._fixture.create_repo(
602 self.repo_name, repo_type=self.alias, **kwargs)
602 self.repo_name, repo_type=self.alias, **kwargs)
603 self._cleanup_repos.append(repo.repo_name)
603 self._cleanup_repos.append(repo.repo_name)
604
604
605 commits = commits or [
605 commits = commits or [
606 {'message': 'Commit %s of %s' % (x, self.repo_name)}
606 {'message': 'Commit %s of %s' % (x, self.repo_name)}
607 for x in xrange(number_of_commits)]
607 for x in xrange(number_of_commits)]
608 self._add_commits_to_repo(repo.scm_instance(), commits)
608 self._add_commits_to_repo(repo.scm_instance(), commits)
609 if heads:
609 if heads:
610 self.pull_heads(repo, heads)
610 self.pull_heads(repo, heads)
611
611
612 return repo
612 return repo
613
613
614 def pull_heads(self, repo, heads):
614 def pull_heads(self, repo, heads):
615 """
615 """
616 Make sure that repo contains all commits mentioned in `heads`
616 Make sure that repo contains all commits mentioned in `heads`
617 """
617 """
618 vcsmaster = self._master_repo.scm_instance()
618 vcsmaster = self._master_repo.scm_instance()
619 vcsrepo = repo.scm_instance()
619 vcsrepo = repo.scm_instance()
620 vcsrepo.config.clear_section('hooks')
620 vcsrepo.config.clear_section('hooks')
621 commit_ids = [self._commit_ids[h] for h in heads]
621 commit_ids = [self._commit_ids[h] for h in heads]
622 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
622 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
623
623
624 def create_fork(self):
624 def create_fork(self):
625 repo_to_fork = self.repo_name
625 repo_to_fork = self.repo_name
626 self.repo_name = self._next_repo_name()
626 self.repo_name = self._next_repo_name()
627 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
627 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
628 self._cleanup_repos.append(self.repo_name)
628 self._cleanup_repos.append(self.repo_name)
629 return repo
629 return repo
630
630
631 def new_repo_name(self, suffix=u''):
631 def new_repo_name(self, suffix=u''):
632 self.repo_name = self._next_repo_name() + suffix
632 self.repo_name = self._next_repo_name() + suffix
633 self._cleanup_repos.append(self.repo_name)
633 self._cleanup_repos.append(self.repo_name)
634 return self.repo_name
634 return self.repo_name
635
635
636 def _next_repo_name(self):
636 def _next_repo_name(self):
637 return u"%s_%s" % (
637 return u"%s_%s" % (
638 self.invalid_repo_name.sub(u'_', self._test_name),
638 self.invalid_repo_name.sub(u'_', self._test_name),
639 len(self._cleanup_repos))
639 len(self._cleanup_repos))
640
640
641 def ensure_file(self, filename, content='Test content\n'):
641 def ensure_file(self, filename, content='Test content\n'):
642 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
642 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
643 commits = [
643 commits = [
644 {'added': [
644 {'added': [
645 FileNode(filename, content=content),
645 FileNode(filename, content=content),
646 ]},
646 ]},
647 ]
647 ]
648 self._add_commits_to_repo(self.repo.scm_instance(), commits)
648 self._add_commits_to_repo(self.repo.scm_instance(), commits)
649
649
650 def enable_downloads(self):
650 def enable_downloads(self):
651 repo = self.repo
651 repo = self.repo
652 repo.enable_downloads = True
652 repo.enable_downloads = True
653 Session().add(repo)
653 Session().add(repo)
654 Session().commit()
654 Session().commit()
655
655
656 def cleanup(self):
656 def cleanup(self):
657 for repo_name in reversed(self._cleanup_repos):
657 for repo_name in reversed(self._cleanup_repos):
658 self._fixture.destroy_repo(repo_name)
658 self._fixture.destroy_repo(repo_name)
659
659
660 def _add_commits_to_repo(self, repo, commits):
660 def _add_commits_to_repo(self, repo, commits):
661 commit_ids = _add_commits_to_repo(repo, commits)
661 commit_ids = _add_commits_to_repo(repo, commits)
662 if not commit_ids:
662 if not commit_ids:
663 return
663 return
664 self._commit_ids = commit_ids
664 self._commit_ids = commit_ids
665
665
666 # Creating refs for Git to allow fetching them from remote repository
666 # Creating refs for Git to allow fetching them from remote repository
667 if self.alias == 'git':
667 if self.alias == 'git':
668 refs = {}
668 refs = {}
669 for message in self._commit_ids:
669 for message in self._commit_ids:
670 # TODO: mikhail: do more special chars replacements
670 # TODO: mikhail: do more special chars replacements
671 ref_name = 'refs/test-refs/{}'.format(
671 ref_name = 'refs/test-refs/{}'.format(
672 message.replace(' ', ''))
672 message.replace(' ', ''))
673 refs[ref_name] = self._commit_ids[message]
673 refs[ref_name] = self._commit_ids[message]
674 self._create_refs(repo, refs)
674 self._create_refs(repo, refs)
675
675
676 def _create_refs(self, repo, refs):
676 def _create_refs(self, repo, refs):
677 for ref_name in refs:
677 for ref_name in refs:
678 repo.set_refs(ref_name, refs[ref_name])
678 repo.set_refs(ref_name, refs[ref_name])
679
679
680
680
681 @pytest.fixture
681 @pytest.fixture
682 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
682 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
683 """
683 """
684 Parametrized fixture which represents a single vcs backend implementation.
684 Parametrized fixture which represents a single vcs backend implementation.
685
685
686 See the fixture `backend` for more details. This one implements the same
686 See the fixture `backend` for more details. This one implements the same
687 concept, but on vcs level. So it does not provide model instances etc.
687 concept, but on vcs level. So it does not provide model instances etc.
688
688
689 Parameters are generated dynamically, see :func:`pytest_generate_tests`
689 Parameters are generated dynamically, see :func:`pytest_generate_tests`
690 for how this works.
690 for how this works.
691 """
691 """
692 if backend_alias not in request.config.getoption('--backends'):
692 if backend_alias not in request.config.getoption('--backends'):
693 pytest.skip("Backend %s not selected." % (backend_alias, ))
693 pytest.skip("Backend %s not selected." % (backend_alias, ))
694
694
695 utils.check_xfail_backends(request.node, backend_alias)
695 utils.check_xfail_backends(request.node, backend_alias)
696 utils.check_skip_backends(request.node, backend_alias)
696 utils.check_skip_backends(request.node, backend_alias)
697
697
698 repo_name = 'vcs_test_%s' % (backend_alias, )
698 repo_name = 'vcs_test_%s' % (backend_alias, )
699 repo_path = os.path.join(tests_tmp_path, repo_name)
699 repo_path = os.path.join(tests_tmp_path, repo_name)
700 backend = VcsBackend(
700 backend = VcsBackend(
701 alias=backend_alias,
701 alias=backend_alias,
702 repo_path=repo_path,
702 repo_path=repo_path,
703 test_name=request.node.name,
703 test_name=request.node.name,
704 test_repo_container=test_repo)
704 test_repo_container=test_repo)
705 request.addfinalizer(backend.cleanup)
705 request.addfinalizer(backend.cleanup)
706 return backend
706 return backend
707
707
708
708
709 @pytest.fixture
709 @pytest.fixture
710 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
710 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
711 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
711 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
712
712
713
713
714 @pytest.fixture
714 @pytest.fixture
715 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
715 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
716 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
716 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
717
717
718
718
719 @pytest.fixture
719 @pytest.fixture
720 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
720 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
721 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
721 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
722
722
723
723
724 @pytest.fixture
724 @pytest.fixture
725 def vcsbackend_random(vcsbackend_git):
725 def vcsbackend_random(vcsbackend_git):
726 """
726 """
727 Use this to express that your tests need "a vcsbackend".
727 Use this to express that your tests need "a vcsbackend".
728
728
729 The fixture `vcsbackend` would run the test multiple times for each
729 The fixture `vcsbackend` would run the test multiple times for each
730 available vcs backend which is a pure waste of time if the test is
730 available vcs backend which is a pure waste of time if the test is
731 independent of the vcs backend type.
731 independent of the vcs backend type.
732 """
732 """
733 # TODO: johbo: Change this to pick a random backend
733 # TODO: johbo: Change this to pick a random backend
734 return vcsbackend_git
734 return vcsbackend_git
735
735
736
736
737 @pytest.fixture
737 @pytest.fixture
738 def vcsbackend_stub(vcsbackend_git):
738 def vcsbackend_stub(vcsbackend_git):
739 """
739 """
740 Use this to express that your test just needs a stub of a vcsbackend.
740 Use this to express that your test just needs a stub of a vcsbackend.
741
741
742 Plan is to eventually implement an in-memory stub to speed tests up.
742 Plan is to eventually implement an in-memory stub to speed tests up.
743 """
743 """
744 return vcsbackend_git
744 return vcsbackend_git
745
745
746
746
747 class VcsBackend(object):
747 class VcsBackend(object):
748 """
748 """
749 Represents the test configuration for one supported vcs backend.
749 Represents the test configuration for one supported vcs backend.
750 """
750 """
751
751
752 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
752 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
753
753
754 def __init__(self, alias, repo_path, test_name, test_repo_container):
754 def __init__(self, alias, repo_path, test_name, test_repo_container):
755 self.alias = alias
755 self.alias = alias
756 self._repo_path = repo_path
756 self._repo_path = repo_path
757 self._cleanup_repos = []
757 self._cleanup_repos = []
758 self._test_name = test_name
758 self._test_name = test_name
759 self._test_repo_container = test_repo_container
759 self._test_repo_container = test_repo_container
760
760
761 def __getitem__(self, key):
761 def __getitem__(self, key):
762 return self._test_repo_container(key, self.alias).scm_instance()
762 return self._test_repo_container(key, self.alias).scm_instance()
763
763
764 @property
764 @property
765 def repo(self):
765 def repo(self):
766 """
766 """
767 Returns the "current" repository. This is the vcs_test repo of the last
767 Returns the "current" repository. This is the vcs_test repo of the last
768 repo which has been created.
768 repo which has been created.
769 """
769 """
770 Repository = get_backend(self.alias)
770 Repository = get_backend(self.alias)
771 return Repository(self._repo_path)
771 return Repository(self._repo_path)
772
772
773 @property
773 @property
774 def backend(self):
774 def backend(self):
775 """
775 """
776 Returns the backend implementation class.
776 Returns the backend implementation class.
777 """
777 """
778 return get_backend(self.alias)
778 return get_backend(self.alias)
779
779
780 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
780 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
781 repo_name = self._next_repo_name()
781 repo_name = self._next_repo_name()
782 self._repo_path = get_new_dir(repo_name)
782 self._repo_path = get_new_dir(repo_name)
783 repo_class = get_backend(self.alias)
783 repo_class = get_backend(self.alias)
784 src_url = None
784 src_url = None
785 if _clone_repo:
785 if _clone_repo:
786 src_url = _clone_repo.path
786 src_url = _clone_repo.path
787 repo = repo_class(self._repo_path, create=True, src_url=src_url)
787 repo = repo_class(self._repo_path, create=True, src_url=src_url)
788 self._cleanup_repos.append(repo)
788 self._cleanup_repos.append(repo)
789
789
790 commits = commits or [
790 commits = commits or [
791 {'message': 'Commit %s of %s' % (x, repo_name)}
791 {'message': 'Commit %s of %s' % (x, repo_name)}
792 for x in xrange(number_of_commits)]
792 for x in xrange(number_of_commits)]
793 _add_commits_to_repo(repo, commits)
793 _add_commits_to_repo(repo, commits)
794 return repo
794 return repo
795
795
796 def clone_repo(self, repo):
796 def clone_repo(self, repo):
797 return self.create_repo(_clone_repo=repo)
797 return self.create_repo(_clone_repo=repo)
798
798
799 def cleanup(self):
799 def cleanup(self):
800 for repo in self._cleanup_repos:
800 for repo in self._cleanup_repos:
801 shutil.rmtree(repo.path)
801 shutil.rmtree(repo.path)
802
802
803 def new_repo_path(self):
803 def new_repo_path(self):
804 repo_name = self._next_repo_name()
804 repo_name = self._next_repo_name()
805 self._repo_path = get_new_dir(repo_name)
805 self._repo_path = get_new_dir(repo_name)
806 return self._repo_path
806 return self._repo_path
807
807
808 def _next_repo_name(self):
808 def _next_repo_name(self):
809 return "%s_%s" % (
809 return "%s_%s" % (
810 self.invalid_repo_name.sub('_', self._test_name),
810 self.invalid_repo_name.sub('_', self._test_name),
811 len(self._cleanup_repos))
811 len(self._cleanup_repos))
812
812
813 def add_file(self, repo, filename, content='Test content\n'):
813 def add_file(self, repo, filename, content='Test content\n'):
814 imc = repo.in_memory_commit
814 imc = repo.in_memory_commit
815 imc.add(FileNode(filename, content=content))
815 imc.add(FileNode(filename, content=content))
816 imc.commit(
816 imc.commit(
817 message=u'Automatic commit from vcsbackend fixture',
817 message=u'Automatic commit from vcsbackend fixture',
818 author=u'Automatic')
818 author=u'Automatic')
819
819
820 def ensure_file(self, filename, content='Test content\n'):
820 def ensure_file(self, filename, content='Test content\n'):
821 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
821 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
822 self.add_file(self.repo, filename, content)
822 self.add_file(self.repo, filename, content)
823
823
824
824
825 def _add_commits_to_repo(vcs_repo, commits):
825 def _add_commits_to_repo(vcs_repo, commits):
826 commit_ids = {}
826 commit_ids = {}
827 if not commits:
827 if not commits:
828 return commit_ids
828 return commit_ids
829
829
830 imc = vcs_repo.in_memory_commit
830 imc = vcs_repo.in_memory_commit
831 commit = None
831 commit = None
832
832
833 for idx, commit in enumerate(commits):
833 for idx, commit in enumerate(commits):
834 message = unicode(commit.get('message', 'Commit %s' % idx))
834 message = unicode(commit.get('message', 'Commit %s' % idx))
835
835
836 for node in commit.get('added', []):
836 for node in commit.get('added', []):
837 imc.add(FileNode(node.path, content=node.content))
837 imc.add(FileNode(node.path, content=node.content))
838 for node in commit.get('changed', []):
838 for node in commit.get('changed', []):
839 imc.change(FileNode(node.path, content=node.content))
839 imc.change(FileNode(node.path, content=node.content))
840 for node in commit.get('removed', []):
840 for node in commit.get('removed', []):
841 imc.remove(FileNode(node.path))
841 imc.remove(FileNode(node.path))
842
842
843 parents = [
843 parents = [
844 vcs_repo.get_commit(commit_id=commit_ids[p])
844 vcs_repo.get_commit(commit_id=commit_ids[p])
845 for p in commit.get('parents', [])]
845 for p in commit.get('parents', [])]
846
846
847 operations = ('added', 'changed', 'removed')
847 operations = ('added', 'changed', 'removed')
848 if not any((commit.get(o) for o in operations)):
848 if not any((commit.get(o) for o in operations)):
849 imc.add(FileNode('file_%s' % idx, content=message))
849 imc.add(FileNode('file_%s' % idx, content=message))
850
850
851 commit = imc.commit(
851 commit = imc.commit(
852 message=message,
852 message=message,
853 author=unicode(commit.get('author', 'Automatic')),
853 author=unicode(commit.get('author', 'Automatic')),
854 date=commit.get('date'),
854 date=commit.get('date'),
855 branch=commit.get('branch'),
855 branch=commit.get('branch'),
856 parents=parents)
856 parents=parents)
857
857
858 commit_ids[commit.message] = commit.raw_id
858 commit_ids[commit.message] = commit.raw_id
859
859
860 return commit_ids
860 return commit_ids
861
861
862
862
863 @pytest.fixture
863 @pytest.fixture
864 def reposerver(request):
864 def reposerver(request):
865 """
865 """
866 Allows to serve a backend repository
866 Allows to serve a backend repository
867 """
867 """
868
868
869 repo_server = RepoServer()
869 repo_server = RepoServer()
870 request.addfinalizer(repo_server.cleanup)
870 request.addfinalizer(repo_server.cleanup)
871 return repo_server
871 return repo_server
872
872
873
873
874 class RepoServer(object):
874 class RepoServer(object):
875 """
875 """
876 Utility to serve a local repository for the duration of a test case.
876 Utility to serve a local repository for the duration of a test case.
877
877
878 Supports only Subversion so far.
878 Supports only Subversion so far.
879 """
879 """
880
880
881 url = None
881 url = None
882
882
883 def __init__(self):
883 def __init__(self):
884 self._cleanup_servers = []
884 self._cleanup_servers = []
885
885
886 def serve(self, vcsrepo):
886 def serve(self, vcsrepo):
887 if vcsrepo.alias != 'svn':
887 if vcsrepo.alias != 'svn':
888 raise TypeError("Backend %s not supported" % vcsrepo.alias)
888 raise TypeError("Backend %s not supported" % vcsrepo.alias)
889
889
890 proc = subprocess32.Popen(
890 proc = subprocess32.Popen(
891 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
891 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
892 '--root', vcsrepo.path])
892 '--root', vcsrepo.path])
893 self._cleanup_servers.append(proc)
893 self._cleanup_servers.append(proc)
894 self.url = 'svn://localhost'
894 self.url = 'svn://localhost'
895
895
896 def cleanup(self):
896 def cleanup(self):
897 for proc in self._cleanup_servers:
897 for proc in self._cleanup_servers:
898 proc.terminate()
898 proc.terminate()
899
899
900
900
901 @pytest.fixture
901 @pytest.fixture
902 def pr_util(backend, request):
902 def pr_util(backend, request):
903 """
903 """
904 Utility for tests of models and for functional tests around pull requests.
904 Utility for tests of models and for functional tests around pull requests.
905
905
906 It gives an instance of :class:`PRTestUtility` which provides various
906 It gives an instance of :class:`PRTestUtility` which provides various
907 utility methods around one pull request.
907 utility methods around one pull request.
908
908
909 This fixture uses `backend` and inherits its parameterization.
909 This fixture uses `backend` and inherits its parameterization.
910 """
910 """
911
911
912 util = PRTestUtility(backend)
912 util = PRTestUtility(backend)
913
913
914 @request.addfinalizer
914 @request.addfinalizer
915 def cleanup():
915 def cleanup():
916 util.cleanup()
916 util.cleanup()
917
917
918 return util
918 return util
919
919
920
920
921 class PRTestUtility(object):
921 class PRTestUtility(object):
922
922
923 pull_request = None
923 pull_request = None
924 pull_request_id = None
924 pull_request_id = None
925 mergeable_patcher = None
925 mergeable_patcher = None
926 mergeable_mock = None
926 mergeable_mock = None
927 notification_patcher = None
927 notification_patcher = None
928
928
929 def __init__(self, backend):
929 def __init__(self, backend):
930 self.backend = backend
930 self.backend = backend
931
931
932 def create_pull_request(
932 def create_pull_request(
933 self, commits=None, target_head=None, source_head=None,
933 self, commits=None, target_head=None, source_head=None,
934 revisions=None, approved=False, author=None, mergeable=False,
934 revisions=None, approved=False, author=None, mergeable=False,
935 enable_notifications=True, name_suffix=u'', reviewers=None,
935 enable_notifications=True, name_suffix=u'', reviewers=None,
936 title=u"Test", description=u"Description"):
936 title=u"Test", description=u"Description"):
937 self.set_mergeable(mergeable)
937 self.set_mergeable(mergeable)
938 if not enable_notifications:
938 if not enable_notifications:
939 # mock notification side effect
939 # mock notification side effect
940 self.notification_patcher = mock.patch(
940 self.notification_patcher = mock.patch(
941 'rhodecode.model.notification.NotificationModel.create')
941 'rhodecode.model.notification.NotificationModel.create')
942 self.notification_patcher.start()
942 self.notification_patcher.start()
943
943
944 if not self.pull_request:
944 if not self.pull_request:
945 if not commits:
945 if not commits:
946 commits = [
946 commits = [
947 {'message': 'c1'},
947 {'message': 'c1'},
948 {'message': 'c2'},
948 {'message': 'c2'},
949 {'message': 'c3'},
949 {'message': 'c3'},
950 ]
950 ]
951 target_head = 'c1'
951 target_head = 'c1'
952 source_head = 'c2'
952 source_head = 'c2'
953 revisions = ['c2']
953 revisions = ['c2']
954
954
955 self.commit_ids = self.backend.create_master_repo(commits)
955 self.commit_ids = self.backend.create_master_repo(commits)
956 self.target_repository = self.backend.create_repo(
956 self.target_repository = self.backend.create_repo(
957 heads=[target_head], name_suffix=name_suffix)
957 heads=[target_head], name_suffix=name_suffix)
958 self.source_repository = self.backend.create_repo(
958 self.source_repository = self.backend.create_repo(
959 heads=[source_head], name_suffix=name_suffix)
959 heads=[source_head], name_suffix=name_suffix)
960 self.author = author or UserModel().get_by_username(
960 self.author = author or UserModel().get_by_username(
961 TEST_USER_ADMIN_LOGIN)
961 TEST_USER_ADMIN_LOGIN)
962
962
963 model = PullRequestModel()
963 model = PullRequestModel()
964 self.create_parameters = {
964 self.create_parameters = {
965 'created_by': self.author,
965 'created_by': self.author,
966 'source_repo': self.source_repository.repo_name,
966 'source_repo': self.source_repository.repo_name,
967 'source_ref': self._default_branch_reference(source_head),
967 'source_ref': self._default_branch_reference(source_head),
968 'target_repo': self.target_repository.repo_name,
968 'target_repo': self.target_repository.repo_name,
969 'target_ref': self._default_branch_reference(target_head),
969 'target_ref': self._default_branch_reference(target_head),
970 'revisions': [self.commit_ids[r] for r in revisions],
970 'revisions': [self.commit_ids[r] for r in revisions],
971 'reviewers': reviewers or self._get_reviewers(),
971 'reviewers': reviewers or self._get_reviewers(),
972 'title': title,
972 'title': title,
973 'description': description,
973 'description': description,
974 }
974 }
975 self.pull_request = model.create(**self.create_parameters)
975 self.pull_request = model.create(**self.create_parameters)
976 assert model.get_versions(self.pull_request) == []
976 assert model.get_versions(self.pull_request) == []
977
977
978 self.pull_request_id = self.pull_request.pull_request_id
978 self.pull_request_id = self.pull_request.pull_request_id
979
979
980 if approved:
980 if approved:
981 self.approve()
981 self.approve()
982
982
983 Session().add(self.pull_request)
983 Session().add(self.pull_request)
984 Session().commit()
984 Session().commit()
985
985
986 return self.pull_request
986 return self.pull_request
987
987
988 def approve(self):
988 def approve(self):
989 self.create_status_votes(
989 self.create_status_votes(
990 ChangesetStatus.STATUS_APPROVED,
990 ChangesetStatus.STATUS_APPROVED,
991 *self.pull_request.reviewers)
991 *self.pull_request.reviewers)
992
992
993 def close(self):
993 def close(self):
994 PullRequestModel().close_pull_request(self.pull_request, self.author)
994 PullRequestModel().close_pull_request(self.pull_request, self.author)
995
995
996 def _default_branch_reference(self, commit_message):
996 def _default_branch_reference(self, commit_message):
997 reference = '%s:%s:%s' % (
997 reference = '%s:%s:%s' % (
998 'branch',
998 'branch',
999 self.backend.default_branch_name,
999 self.backend.default_branch_name,
1000 self.commit_ids[commit_message])
1000 self.commit_ids[commit_message])
1001 return reference
1001 return reference
1002
1002
1003 def _get_reviewers(self):
1003 def _get_reviewers(self):
1004 model = UserModel()
1004 model = UserModel()
1005 return [
1005 return [
1006 model.get_by_username(TEST_USER_REGULAR_LOGIN),
1006 model.get_by_username(TEST_USER_REGULAR_LOGIN),
1007 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
1007 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
1008 ]
1008 ]
1009
1009
1010 def update_source_repository(self, head=None):
1010 def update_source_repository(self, head=None):
1011 heads = [head or 'c3']
1011 heads = [head or 'c3']
1012 self.backend.pull_heads(self.source_repository, heads=heads)
1012 self.backend.pull_heads(self.source_repository, heads=heads)
1013
1013
1014 def add_one_commit(self, head=None):
1014 def add_one_commit(self, head=None):
1015 self.update_source_repository(head=head)
1015 self.update_source_repository(head=head)
1016 old_commit_ids = set(self.pull_request.revisions)
1016 old_commit_ids = set(self.pull_request.revisions)
1017 PullRequestModel().update_commits(self.pull_request)
1017 PullRequestModel().update_commits(self.pull_request)
1018 commit_ids = set(self.pull_request.revisions)
1018 commit_ids = set(self.pull_request.revisions)
1019 new_commit_ids = commit_ids - old_commit_ids
1019 new_commit_ids = commit_ids - old_commit_ids
1020 assert len(new_commit_ids) == 1
1020 assert len(new_commit_ids) == 1
1021 return new_commit_ids.pop()
1021 return new_commit_ids.pop()
1022
1022
1023 def remove_one_commit(self):
1023 def remove_one_commit(self):
1024 assert len(self.pull_request.revisions) == 2
1024 assert len(self.pull_request.revisions) == 2
1025 source_vcs = self.source_repository.scm_instance()
1025 source_vcs = self.source_repository.scm_instance()
1026 removed_commit_id = source_vcs.commit_ids[-1]
1026 removed_commit_id = source_vcs.commit_ids[-1]
1027
1027
1028 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1028 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1029 # remove the if once that's sorted out.
1029 # remove the if once that's sorted out.
1030 if self.backend.alias == "git":
1030 if self.backend.alias == "git":
1031 kwargs = {'branch_name': self.backend.default_branch_name}
1031 kwargs = {'branch_name': self.backend.default_branch_name}
1032 else:
1032 else:
1033 kwargs = {}
1033 kwargs = {}
1034 source_vcs.strip(removed_commit_id, **kwargs)
1034 source_vcs.strip(removed_commit_id, **kwargs)
1035
1035
1036 PullRequestModel().update_commits(self.pull_request)
1036 PullRequestModel().update_commits(self.pull_request)
1037 assert len(self.pull_request.revisions) == 1
1037 assert len(self.pull_request.revisions) == 1
1038 return removed_commit_id
1038 return removed_commit_id
1039
1039
1040 def create_comment(self, linked_to=None):
1040 def create_comment(self, linked_to=None):
1041 comment = ChangesetCommentsModel().create(
1041 comment = CommentsModel().create(
1042 text=u"Test comment",
1042 text=u"Test comment",
1043 repo=self.target_repository.repo_name,
1043 repo=self.target_repository.repo_name,
1044 user=self.author,
1044 user=self.author,
1045 pull_request=self.pull_request)
1045 pull_request=self.pull_request)
1046 assert comment.pull_request_version_id is None
1046 assert comment.pull_request_version_id is None
1047
1047
1048 if linked_to:
1048 if linked_to:
1049 PullRequestModel()._link_comments_to_version(linked_to)
1049 PullRequestModel()._link_comments_to_version(linked_to)
1050
1050
1051 return comment
1051 return comment
1052
1052
1053 def create_inline_comment(
1053 def create_inline_comment(
1054 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1054 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1055 comment = ChangesetCommentsModel().create(
1055 comment = CommentsModel().create(
1056 text=u"Test comment",
1056 text=u"Test comment",
1057 repo=self.target_repository.repo_name,
1057 repo=self.target_repository.repo_name,
1058 user=self.author,
1058 user=self.author,
1059 line_no=line_no,
1059 line_no=line_no,
1060 f_path=file_path,
1060 f_path=file_path,
1061 pull_request=self.pull_request)
1061 pull_request=self.pull_request)
1062 assert comment.pull_request_version_id is None
1062 assert comment.pull_request_version_id is None
1063
1063
1064 if linked_to:
1064 if linked_to:
1065 PullRequestModel()._link_comments_to_version(linked_to)
1065 PullRequestModel()._link_comments_to_version(linked_to)
1066
1066
1067 return comment
1067 return comment
1068
1068
1069 def create_version_of_pull_request(self):
1069 def create_version_of_pull_request(self):
1070 pull_request = self.create_pull_request()
1070 pull_request = self.create_pull_request()
1071 version = PullRequestModel()._create_version_from_snapshot(
1071 version = PullRequestModel()._create_version_from_snapshot(
1072 pull_request)
1072 pull_request)
1073 return version
1073 return version
1074
1074
1075 def create_status_votes(self, status, *reviewers):
1075 def create_status_votes(self, status, *reviewers):
1076 for reviewer in reviewers:
1076 for reviewer in reviewers:
1077 ChangesetStatusModel().set_status(
1077 ChangesetStatusModel().set_status(
1078 repo=self.pull_request.target_repo,
1078 repo=self.pull_request.target_repo,
1079 status=status,
1079 status=status,
1080 user=reviewer.user_id,
1080 user=reviewer.user_id,
1081 pull_request=self.pull_request)
1081 pull_request=self.pull_request)
1082
1082
1083 def set_mergeable(self, value):
1083 def set_mergeable(self, value):
1084 if not self.mergeable_patcher:
1084 if not self.mergeable_patcher:
1085 self.mergeable_patcher = mock.patch.object(
1085 self.mergeable_patcher = mock.patch.object(
1086 VcsSettingsModel, 'get_general_settings')
1086 VcsSettingsModel, 'get_general_settings')
1087 self.mergeable_mock = self.mergeable_patcher.start()
1087 self.mergeable_mock = self.mergeable_patcher.start()
1088 self.mergeable_mock.return_value = {
1088 self.mergeable_mock.return_value = {
1089 'rhodecode_pr_merge_enabled': value}
1089 'rhodecode_pr_merge_enabled': value}
1090
1090
1091 def cleanup(self):
1091 def cleanup(self):
1092 # In case the source repository is already cleaned up, the pull
1092 # In case the source repository is already cleaned up, the pull
1093 # request will already be deleted.
1093 # request will already be deleted.
1094 pull_request = PullRequest().get(self.pull_request_id)
1094 pull_request = PullRequest().get(self.pull_request_id)
1095 if pull_request:
1095 if pull_request:
1096 PullRequestModel().delete(pull_request)
1096 PullRequestModel().delete(pull_request)
1097 Session().commit()
1097 Session().commit()
1098
1098
1099 if self.notification_patcher:
1099 if self.notification_patcher:
1100 self.notification_patcher.stop()
1100 self.notification_patcher.stop()
1101
1101
1102 if self.mergeable_patcher:
1102 if self.mergeable_patcher:
1103 self.mergeable_patcher.stop()
1103 self.mergeable_patcher.stop()
1104
1104
1105
1105
1106 @pytest.fixture
1106 @pytest.fixture
1107 def user_admin(pylonsapp):
1107 def user_admin(pylonsapp):
1108 """
1108 """
1109 Provides the default admin test user as an instance of `db.User`.
1109 Provides the default admin test user as an instance of `db.User`.
1110 """
1110 """
1111 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1111 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1112 return user
1112 return user
1113
1113
1114
1114
1115 @pytest.fixture
1115 @pytest.fixture
1116 def user_regular(pylonsapp):
1116 def user_regular(pylonsapp):
1117 """
1117 """
1118 Provides the default regular test user as an instance of `db.User`.
1118 Provides the default regular test user as an instance of `db.User`.
1119 """
1119 """
1120 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1120 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1121 return user
1121 return user
1122
1122
1123
1123
1124 @pytest.fixture
1124 @pytest.fixture
1125 def user_util(request, pylonsapp):
1125 def user_util(request, pylonsapp):
1126 """
1126 """
1127 Provides a wired instance of `UserUtility` with integrated cleanup.
1127 Provides a wired instance of `UserUtility` with integrated cleanup.
1128 """
1128 """
1129 utility = UserUtility(test_name=request.node.name)
1129 utility = UserUtility(test_name=request.node.name)
1130 request.addfinalizer(utility.cleanup)
1130 request.addfinalizer(utility.cleanup)
1131 return utility
1131 return utility
1132
1132
1133
1133
1134 # TODO: johbo: Split this up into utilities per domain or something similar
1134 # TODO: johbo: Split this up into utilities per domain or something similar
1135 class UserUtility(object):
1135 class UserUtility(object):
1136
1136
1137 def __init__(self, test_name="test"):
1137 def __init__(self, test_name="test"):
1138 self._test_name = self._sanitize_name(test_name)
1138 self._test_name = self._sanitize_name(test_name)
1139 self.fixture = Fixture()
1139 self.fixture = Fixture()
1140 self.repo_group_ids = []
1140 self.repo_group_ids = []
1141 self.repos_ids = []
1141 self.repos_ids = []
1142 self.user_ids = []
1142 self.user_ids = []
1143 self.user_group_ids = []
1143 self.user_group_ids = []
1144 self.user_repo_permission_ids = []
1144 self.user_repo_permission_ids = []
1145 self.user_group_repo_permission_ids = []
1145 self.user_group_repo_permission_ids = []
1146 self.user_repo_group_permission_ids = []
1146 self.user_repo_group_permission_ids = []
1147 self.user_group_repo_group_permission_ids = []
1147 self.user_group_repo_group_permission_ids = []
1148 self.user_user_group_permission_ids = []
1148 self.user_user_group_permission_ids = []
1149 self.user_group_user_group_permission_ids = []
1149 self.user_group_user_group_permission_ids = []
1150 self.user_permissions = []
1150 self.user_permissions = []
1151
1151
1152 def _sanitize_name(self, name):
1152 def _sanitize_name(self, name):
1153 for char in ['[', ']']:
1153 for char in ['[', ']']:
1154 name = name.replace(char, '_')
1154 name = name.replace(char, '_')
1155 return name
1155 return name
1156
1156
1157 def create_repo_group(
1157 def create_repo_group(
1158 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1158 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1159 group_name = "{prefix}_repogroup_{count}".format(
1159 group_name = "{prefix}_repogroup_{count}".format(
1160 prefix=self._test_name,
1160 prefix=self._test_name,
1161 count=len(self.repo_group_ids))
1161 count=len(self.repo_group_ids))
1162 repo_group = self.fixture.create_repo_group(
1162 repo_group = self.fixture.create_repo_group(
1163 group_name, cur_user=owner)
1163 group_name, cur_user=owner)
1164 if auto_cleanup:
1164 if auto_cleanup:
1165 self.repo_group_ids.append(repo_group.group_id)
1165 self.repo_group_ids.append(repo_group.group_id)
1166 return repo_group
1166 return repo_group
1167
1167
1168 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None, auto_cleanup=True):
1168 def create_repo(self, owner=TEST_USER_ADMIN_LOGIN, parent=None, auto_cleanup=True):
1169 repo_name = "{prefix}_repository_{count}".format(
1169 repo_name = "{prefix}_repository_{count}".format(
1170 prefix=self._test_name,
1170 prefix=self._test_name,
1171 count=len(self.repos_ids))
1171 count=len(self.repos_ids))
1172
1172
1173 repository = self.fixture.create_repo(
1173 repository = self.fixture.create_repo(
1174 repo_name, cur_user=owner, repo_group=parent)
1174 repo_name, cur_user=owner, repo_group=parent)
1175 if auto_cleanup:
1175 if auto_cleanup:
1176 self.repos_ids.append(repository.repo_id)
1176 self.repos_ids.append(repository.repo_id)
1177 return repository
1177 return repository
1178
1178
1179 def create_user(self, auto_cleanup=True, **kwargs):
1179 def create_user(self, auto_cleanup=True, **kwargs):
1180 user_name = "{prefix}_user_{count}".format(
1180 user_name = "{prefix}_user_{count}".format(
1181 prefix=self._test_name,
1181 prefix=self._test_name,
1182 count=len(self.user_ids))
1182 count=len(self.user_ids))
1183 user = self.fixture.create_user(user_name, **kwargs)
1183 user = self.fixture.create_user(user_name, **kwargs)
1184 if auto_cleanup:
1184 if auto_cleanup:
1185 self.user_ids.append(user.user_id)
1185 self.user_ids.append(user.user_id)
1186 return user
1186 return user
1187
1187
1188 def create_user_with_group(self):
1188 def create_user_with_group(self):
1189 user = self.create_user()
1189 user = self.create_user()
1190 user_group = self.create_user_group(members=[user])
1190 user_group = self.create_user_group(members=[user])
1191 return user, user_group
1191 return user, user_group
1192
1192
1193 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1193 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1194 group_name = "{prefix}_usergroup_{count}".format(
1194 group_name = "{prefix}_usergroup_{count}".format(
1195 prefix=self._test_name,
1195 prefix=self._test_name,
1196 count=len(self.user_group_ids))
1196 count=len(self.user_group_ids))
1197 user_group = self.fixture.create_user_group(group_name, **kwargs)
1197 user_group = self.fixture.create_user_group(group_name, **kwargs)
1198 if auto_cleanup:
1198 if auto_cleanup:
1199 self.user_group_ids.append(user_group.users_group_id)
1199 self.user_group_ids.append(user_group.users_group_id)
1200 if members:
1200 if members:
1201 for user in members:
1201 for user in members:
1202 UserGroupModel().add_user_to_group(user_group, user)
1202 UserGroupModel().add_user_to_group(user_group, user)
1203 return user_group
1203 return user_group
1204
1204
1205 def grant_user_permission(self, user_name, permission_name):
1205 def grant_user_permission(self, user_name, permission_name):
1206 self._inherit_default_user_permissions(user_name, False)
1206 self._inherit_default_user_permissions(user_name, False)
1207 self.user_permissions.append((user_name, permission_name))
1207 self.user_permissions.append((user_name, permission_name))
1208
1208
1209 def grant_user_permission_to_repo_group(
1209 def grant_user_permission_to_repo_group(
1210 self, repo_group, user, permission_name):
1210 self, repo_group, user, permission_name):
1211 permission = RepoGroupModel().grant_user_permission(
1211 permission = RepoGroupModel().grant_user_permission(
1212 repo_group, user, permission_name)
1212 repo_group, user, permission_name)
1213 self.user_repo_group_permission_ids.append(
1213 self.user_repo_group_permission_ids.append(
1214 (repo_group.group_id, user.user_id))
1214 (repo_group.group_id, user.user_id))
1215 return permission
1215 return permission
1216
1216
1217 def grant_user_group_permission_to_repo_group(
1217 def grant_user_group_permission_to_repo_group(
1218 self, repo_group, user_group, permission_name):
1218 self, repo_group, user_group, permission_name):
1219 permission = RepoGroupModel().grant_user_group_permission(
1219 permission = RepoGroupModel().grant_user_group_permission(
1220 repo_group, user_group, permission_name)
1220 repo_group, user_group, permission_name)
1221 self.user_group_repo_group_permission_ids.append(
1221 self.user_group_repo_group_permission_ids.append(
1222 (repo_group.group_id, user_group.users_group_id))
1222 (repo_group.group_id, user_group.users_group_id))
1223 return permission
1223 return permission
1224
1224
1225 def grant_user_permission_to_repo(
1225 def grant_user_permission_to_repo(
1226 self, repo, user, permission_name):
1226 self, repo, user, permission_name):
1227 permission = RepoModel().grant_user_permission(
1227 permission = RepoModel().grant_user_permission(
1228 repo, user, permission_name)
1228 repo, user, permission_name)
1229 self.user_repo_permission_ids.append(
1229 self.user_repo_permission_ids.append(
1230 (repo.repo_id, user.user_id))
1230 (repo.repo_id, user.user_id))
1231 return permission
1231 return permission
1232
1232
1233 def grant_user_group_permission_to_repo(
1233 def grant_user_group_permission_to_repo(
1234 self, repo, user_group, permission_name):
1234 self, repo, user_group, permission_name):
1235 permission = RepoModel().grant_user_group_permission(
1235 permission = RepoModel().grant_user_group_permission(
1236 repo, user_group, permission_name)
1236 repo, user_group, permission_name)
1237 self.user_group_repo_permission_ids.append(
1237 self.user_group_repo_permission_ids.append(
1238 (repo.repo_id, user_group.users_group_id))
1238 (repo.repo_id, user_group.users_group_id))
1239 return permission
1239 return permission
1240
1240
1241 def grant_user_permission_to_user_group(
1241 def grant_user_permission_to_user_group(
1242 self, target_user_group, user, permission_name):
1242 self, target_user_group, user, permission_name):
1243 permission = UserGroupModel().grant_user_permission(
1243 permission = UserGroupModel().grant_user_permission(
1244 target_user_group, user, permission_name)
1244 target_user_group, user, permission_name)
1245 self.user_user_group_permission_ids.append(
1245 self.user_user_group_permission_ids.append(
1246 (target_user_group.users_group_id, user.user_id))
1246 (target_user_group.users_group_id, user.user_id))
1247 return permission
1247 return permission
1248
1248
1249 def grant_user_group_permission_to_user_group(
1249 def grant_user_group_permission_to_user_group(
1250 self, target_user_group, user_group, permission_name):
1250 self, target_user_group, user_group, permission_name):
1251 permission = UserGroupModel().grant_user_group_permission(
1251 permission = UserGroupModel().grant_user_group_permission(
1252 target_user_group, user_group, permission_name)
1252 target_user_group, user_group, permission_name)
1253 self.user_group_user_group_permission_ids.append(
1253 self.user_group_user_group_permission_ids.append(
1254 (target_user_group.users_group_id, user_group.users_group_id))
1254 (target_user_group.users_group_id, user_group.users_group_id))
1255 return permission
1255 return permission
1256
1256
1257 def revoke_user_permission(self, user_name, permission_name):
1257 def revoke_user_permission(self, user_name, permission_name):
1258 self._inherit_default_user_permissions(user_name, True)
1258 self._inherit_default_user_permissions(user_name, True)
1259 UserModel().revoke_perm(user_name, permission_name)
1259 UserModel().revoke_perm(user_name, permission_name)
1260
1260
1261 def _inherit_default_user_permissions(self, user_name, value):
1261 def _inherit_default_user_permissions(self, user_name, value):
1262 user = UserModel().get_by_username(user_name)
1262 user = UserModel().get_by_username(user_name)
1263 user.inherit_default_permissions = value
1263 user.inherit_default_permissions = value
1264 Session().add(user)
1264 Session().add(user)
1265 Session().commit()
1265 Session().commit()
1266
1266
1267 def cleanup(self):
1267 def cleanup(self):
1268 self._cleanup_permissions()
1268 self._cleanup_permissions()
1269 self._cleanup_repos()
1269 self._cleanup_repos()
1270 self._cleanup_repo_groups()
1270 self._cleanup_repo_groups()
1271 self._cleanup_user_groups()
1271 self._cleanup_user_groups()
1272 self._cleanup_users()
1272 self._cleanup_users()
1273
1273
1274 def _cleanup_permissions(self):
1274 def _cleanup_permissions(self):
1275 if self.user_permissions:
1275 if self.user_permissions:
1276 for user_name, permission_name in self.user_permissions:
1276 for user_name, permission_name in self.user_permissions:
1277 self.revoke_user_permission(user_name, permission_name)
1277 self.revoke_user_permission(user_name, permission_name)
1278
1278
1279 for permission in self.user_repo_permission_ids:
1279 for permission in self.user_repo_permission_ids:
1280 RepoModel().revoke_user_permission(*permission)
1280 RepoModel().revoke_user_permission(*permission)
1281
1281
1282 for permission in self.user_group_repo_permission_ids:
1282 for permission in self.user_group_repo_permission_ids:
1283 RepoModel().revoke_user_group_permission(*permission)
1283 RepoModel().revoke_user_group_permission(*permission)
1284
1284
1285 for permission in self.user_repo_group_permission_ids:
1285 for permission in self.user_repo_group_permission_ids:
1286 RepoGroupModel().revoke_user_permission(*permission)
1286 RepoGroupModel().revoke_user_permission(*permission)
1287
1287
1288 for permission in self.user_group_repo_group_permission_ids:
1288 for permission in self.user_group_repo_group_permission_ids:
1289 RepoGroupModel().revoke_user_group_permission(*permission)
1289 RepoGroupModel().revoke_user_group_permission(*permission)
1290
1290
1291 for permission in self.user_user_group_permission_ids:
1291 for permission in self.user_user_group_permission_ids:
1292 UserGroupModel().revoke_user_permission(*permission)
1292 UserGroupModel().revoke_user_permission(*permission)
1293
1293
1294 for permission in self.user_group_user_group_permission_ids:
1294 for permission in self.user_group_user_group_permission_ids:
1295 UserGroupModel().revoke_user_group_permission(*permission)
1295 UserGroupModel().revoke_user_group_permission(*permission)
1296
1296
1297 def _cleanup_repo_groups(self):
1297 def _cleanup_repo_groups(self):
1298 def _repo_group_compare(first_group_id, second_group_id):
1298 def _repo_group_compare(first_group_id, second_group_id):
1299 """
1299 """
1300 Gives higher priority to the groups with the most complex paths
1300 Gives higher priority to the groups with the most complex paths
1301 """
1301 """
1302 first_group = RepoGroup.get(first_group_id)
1302 first_group = RepoGroup.get(first_group_id)
1303 second_group = RepoGroup.get(second_group_id)
1303 second_group = RepoGroup.get(second_group_id)
1304 first_group_parts = (
1304 first_group_parts = (
1305 len(first_group.group_name.split('/')) if first_group else 0)
1305 len(first_group.group_name.split('/')) if first_group else 0)
1306 second_group_parts = (
1306 second_group_parts = (
1307 len(second_group.group_name.split('/')) if second_group else 0)
1307 len(second_group.group_name.split('/')) if second_group else 0)
1308 return cmp(second_group_parts, first_group_parts)
1308 return cmp(second_group_parts, first_group_parts)
1309
1309
1310 sorted_repo_group_ids = sorted(
1310 sorted_repo_group_ids = sorted(
1311 self.repo_group_ids, cmp=_repo_group_compare)
1311 self.repo_group_ids, cmp=_repo_group_compare)
1312 for repo_group_id in sorted_repo_group_ids:
1312 for repo_group_id in sorted_repo_group_ids:
1313 self.fixture.destroy_repo_group(repo_group_id)
1313 self.fixture.destroy_repo_group(repo_group_id)
1314
1314
1315 def _cleanup_repos(self):
1315 def _cleanup_repos(self):
1316 sorted_repos_ids = sorted(self.repos_ids)
1316 sorted_repos_ids = sorted(self.repos_ids)
1317 for repo_id in sorted_repos_ids:
1317 for repo_id in sorted_repos_ids:
1318 self.fixture.destroy_repo(repo_id)
1318 self.fixture.destroy_repo(repo_id)
1319
1319
1320 def _cleanup_user_groups(self):
1320 def _cleanup_user_groups(self):
1321 def _user_group_compare(first_group_id, second_group_id):
1321 def _user_group_compare(first_group_id, second_group_id):
1322 """
1322 """
1323 Gives higher priority to the groups with the most complex paths
1323 Gives higher priority to the groups with the most complex paths
1324 """
1324 """
1325 first_group = UserGroup.get(first_group_id)
1325 first_group = UserGroup.get(first_group_id)
1326 second_group = UserGroup.get(second_group_id)
1326 second_group = UserGroup.get(second_group_id)
1327 first_group_parts = (
1327 first_group_parts = (
1328 len(first_group.users_group_name.split('/'))
1328 len(first_group.users_group_name.split('/'))
1329 if first_group else 0)
1329 if first_group else 0)
1330 second_group_parts = (
1330 second_group_parts = (
1331 len(second_group.users_group_name.split('/'))
1331 len(second_group.users_group_name.split('/'))
1332 if second_group else 0)
1332 if second_group else 0)
1333 return cmp(second_group_parts, first_group_parts)
1333 return cmp(second_group_parts, first_group_parts)
1334
1334
1335 sorted_user_group_ids = sorted(
1335 sorted_user_group_ids = sorted(
1336 self.user_group_ids, cmp=_user_group_compare)
1336 self.user_group_ids, cmp=_user_group_compare)
1337 for user_group_id in sorted_user_group_ids:
1337 for user_group_id in sorted_user_group_ids:
1338 self.fixture.destroy_user_group(user_group_id)
1338 self.fixture.destroy_user_group(user_group_id)
1339
1339
1340 def _cleanup_users(self):
1340 def _cleanup_users(self):
1341 for user_id in self.user_ids:
1341 for user_id in self.user_ids:
1342 self.fixture.destroy_user(user_id)
1342 self.fixture.destroy_user(user_id)
1343
1343
1344
1344
1345 # TODO: Think about moving this into a pytest-pyro package and make it a
1345 # TODO: Think about moving this into a pytest-pyro package and make it a
1346 # pytest plugin
1346 # pytest plugin
1347 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1347 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1348 def pytest_runtest_makereport(item, call):
1348 def pytest_runtest_makereport(item, call):
1349 """
1349 """
1350 Adding the remote traceback if the exception has this information.
1350 Adding the remote traceback if the exception has this information.
1351
1351
1352 Pyro4 attaches this information as the attribute `_vcs_server_traceback`
1352 Pyro4 attaches this information as the attribute `_vcs_server_traceback`
1353 to the exception instance.
1353 to the exception instance.
1354 """
1354 """
1355 outcome = yield
1355 outcome = yield
1356 report = outcome.get_result()
1356 report = outcome.get_result()
1357 if call.excinfo:
1357 if call.excinfo:
1358 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1358 _add_vcsserver_remote_traceback(report, call.excinfo.value)
1359
1359
1360
1360
1361 def _add_vcsserver_remote_traceback(report, exc):
1361 def _add_vcsserver_remote_traceback(report, exc):
1362 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1362 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
1363
1363
1364 if vcsserver_traceback:
1364 if vcsserver_traceback:
1365 section = 'VCSServer remote traceback ' + report.when
1365 section = 'VCSServer remote traceback ' + report.when
1366 report.sections.append((section, vcsserver_traceback))
1366 report.sections.append((section, vcsserver_traceback))
1367
1367
1368
1368
1369 @pytest.fixture(scope='session')
1369 @pytest.fixture(scope='session')
1370 def testrun():
1370 def testrun():
1371 return {
1371 return {
1372 'uuid': uuid.uuid4(),
1372 'uuid': uuid.uuid4(),
1373 'start': datetime.datetime.utcnow().isoformat(),
1373 'start': datetime.datetime.utcnow().isoformat(),
1374 'timestamp': int(time.time()),
1374 'timestamp': int(time.time()),
1375 }
1375 }
1376
1376
1377
1377
1378 @pytest.fixture(autouse=True)
1378 @pytest.fixture(autouse=True)
1379 def collect_appenlight_stats(request, testrun):
1379 def collect_appenlight_stats(request, testrun):
1380 """
1380 """
1381 This fixture reports memory consumtion of single tests.
1381 This fixture reports memory consumtion of single tests.
1382
1382
1383 It gathers data based on `psutil` and sends them to Appenlight. The option
1383 It gathers data based on `psutil` and sends them to Appenlight. The option
1384 ``--ae`` has te be used to enable this fixture and the API key for your
1384 ``--ae`` has te be used to enable this fixture and the API key for your
1385 application has to be provided in ``--ae-key``.
1385 application has to be provided in ``--ae-key``.
1386 """
1386 """
1387 try:
1387 try:
1388 # cygwin cannot have yet psutil support.
1388 # cygwin cannot have yet psutil support.
1389 import psutil
1389 import psutil
1390 except ImportError:
1390 except ImportError:
1391 return
1391 return
1392
1392
1393 if not request.config.getoption('--appenlight'):
1393 if not request.config.getoption('--appenlight'):
1394 return
1394 return
1395 else:
1395 else:
1396 # Only request the pylonsapp fixture if appenlight tracking is
1396 # Only request the pylonsapp fixture if appenlight tracking is
1397 # enabled. This will speed up a test run of unit tests by 2 to 3
1397 # enabled. This will speed up a test run of unit tests by 2 to 3
1398 # seconds if appenlight is not enabled.
1398 # seconds if appenlight is not enabled.
1399 pylonsapp = request.getfuncargvalue("pylonsapp")
1399 pylonsapp = request.getfuncargvalue("pylonsapp")
1400 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1400 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1401 client = AppenlightClient(
1401 client = AppenlightClient(
1402 url=url,
1402 url=url,
1403 api_key=request.config.getoption('--appenlight-api-key'),
1403 api_key=request.config.getoption('--appenlight-api-key'),
1404 namespace=request.node.nodeid,
1404 namespace=request.node.nodeid,
1405 request=str(testrun['uuid']),
1405 request=str(testrun['uuid']),
1406 testrun=testrun)
1406 testrun=testrun)
1407
1407
1408 client.collect({
1408 client.collect({
1409 'message': "Starting",
1409 'message': "Starting",
1410 })
1410 })
1411
1411
1412 server_and_port = pylonsapp.config['vcs.server']
1412 server_and_port = pylonsapp.config['vcs.server']
1413 server = create_vcsserver_proxy(server_and_port)
1413 server = create_vcsserver_proxy(server_and_port)
1414 with server:
1414 with server:
1415 vcs_pid = server.get_pid()
1415 vcs_pid = server.get_pid()
1416 server.run_gc()
1416 server.run_gc()
1417 vcs_process = psutil.Process(vcs_pid)
1417 vcs_process = psutil.Process(vcs_pid)
1418 mem = vcs_process.memory_info()
1418 mem = vcs_process.memory_info()
1419 client.tag_before('vcsserver.rss', mem.rss)
1419 client.tag_before('vcsserver.rss', mem.rss)
1420 client.tag_before('vcsserver.vms', mem.vms)
1420 client.tag_before('vcsserver.vms', mem.vms)
1421
1421
1422 test_process = psutil.Process()
1422 test_process = psutil.Process()
1423 mem = test_process.memory_info()
1423 mem = test_process.memory_info()
1424 client.tag_before('test.rss', mem.rss)
1424 client.tag_before('test.rss', mem.rss)
1425 client.tag_before('test.vms', mem.vms)
1425 client.tag_before('test.vms', mem.vms)
1426
1426
1427 client.tag_before('time', time.time())
1427 client.tag_before('time', time.time())
1428
1428
1429 @request.addfinalizer
1429 @request.addfinalizer
1430 def send_stats():
1430 def send_stats():
1431 client.tag_after('time', time.time())
1431 client.tag_after('time', time.time())
1432 with server:
1432 with server:
1433 gc_stats = server.run_gc()
1433 gc_stats = server.run_gc()
1434 for tag, value in gc_stats.items():
1434 for tag, value in gc_stats.items():
1435 client.tag_after(tag, value)
1435 client.tag_after(tag, value)
1436 mem = vcs_process.memory_info()
1436 mem = vcs_process.memory_info()
1437 client.tag_after('vcsserver.rss', mem.rss)
1437 client.tag_after('vcsserver.rss', mem.rss)
1438 client.tag_after('vcsserver.vms', mem.vms)
1438 client.tag_after('vcsserver.vms', mem.vms)
1439
1439
1440 mem = test_process.memory_info()
1440 mem = test_process.memory_info()
1441 client.tag_after('test.rss', mem.rss)
1441 client.tag_after('test.rss', mem.rss)
1442 client.tag_after('test.vms', mem.vms)
1442 client.tag_after('test.vms', mem.vms)
1443
1443
1444 client.collect({
1444 client.collect({
1445 'message': "Finished",
1445 'message': "Finished",
1446 })
1446 })
1447 client.send_stats()
1447 client.send_stats()
1448
1448
1449 return client
1449 return client
1450
1450
1451
1451
1452 class AppenlightClient():
1452 class AppenlightClient():
1453
1453
1454 url_template = '{url}?protocol_version=0.5'
1454 url_template = '{url}?protocol_version=0.5'
1455
1455
1456 def __init__(
1456 def __init__(
1457 self, url, api_key, add_server=True, add_timestamp=True,
1457 self, url, api_key, add_server=True, add_timestamp=True,
1458 namespace=None, request=None, testrun=None):
1458 namespace=None, request=None, testrun=None):
1459 self.url = self.url_template.format(url=url)
1459 self.url = self.url_template.format(url=url)
1460 self.api_key = api_key
1460 self.api_key = api_key
1461 self.add_server = add_server
1461 self.add_server = add_server
1462 self.add_timestamp = add_timestamp
1462 self.add_timestamp = add_timestamp
1463 self.namespace = namespace
1463 self.namespace = namespace
1464 self.request = request
1464 self.request = request
1465 self.server = socket.getfqdn(socket.gethostname())
1465 self.server = socket.getfqdn(socket.gethostname())
1466 self.tags_before = {}
1466 self.tags_before = {}
1467 self.tags_after = {}
1467 self.tags_after = {}
1468 self.stats = []
1468 self.stats = []
1469 self.testrun = testrun or {}
1469 self.testrun = testrun or {}
1470
1470
1471 def tag_before(self, tag, value):
1471 def tag_before(self, tag, value):
1472 self.tags_before[tag] = value
1472 self.tags_before[tag] = value
1473
1473
1474 def tag_after(self, tag, value):
1474 def tag_after(self, tag, value):
1475 self.tags_after[tag] = value
1475 self.tags_after[tag] = value
1476
1476
1477 def collect(self, data):
1477 def collect(self, data):
1478 if self.add_server:
1478 if self.add_server:
1479 data.setdefault('server', self.server)
1479 data.setdefault('server', self.server)
1480 if self.add_timestamp:
1480 if self.add_timestamp:
1481 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1481 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1482 if self.namespace:
1482 if self.namespace:
1483 data.setdefault('namespace', self.namespace)
1483 data.setdefault('namespace', self.namespace)
1484 if self.request:
1484 if self.request:
1485 data.setdefault('request', self.request)
1485 data.setdefault('request', self.request)
1486 self.stats.append(data)
1486 self.stats.append(data)
1487
1487
1488 def send_stats(self):
1488 def send_stats(self):
1489 tags = [
1489 tags = [
1490 ('testrun', self.request),
1490 ('testrun', self.request),
1491 ('testrun.start', self.testrun['start']),
1491 ('testrun.start', self.testrun['start']),
1492 ('testrun.timestamp', self.testrun['timestamp']),
1492 ('testrun.timestamp', self.testrun['timestamp']),
1493 ('test', self.namespace),
1493 ('test', self.namespace),
1494 ]
1494 ]
1495 for key, value in self.tags_before.items():
1495 for key, value in self.tags_before.items():
1496 tags.append((key + '.before', value))
1496 tags.append((key + '.before', value))
1497 try:
1497 try:
1498 delta = self.tags_after[key] - value
1498 delta = self.tags_after[key] - value
1499 tags.append((key + '.delta', delta))
1499 tags.append((key + '.delta', delta))
1500 except Exception:
1500 except Exception:
1501 pass
1501 pass
1502 for key, value in self.tags_after.items():
1502 for key, value in self.tags_after.items():
1503 tags.append((key + '.after', value))
1503 tags.append((key + '.after', value))
1504 self.collect({
1504 self.collect({
1505 'message': "Collected tags",
1505 'message': "Collected tags",
1506 'tags': tags,
1506 'tags': tags,
1507 })
1507 })
1508
1508
1509 response = requests.post(
1509 response = requests.post(
1510 self.url,
1510 self.url,
1511 headers={
1511 headers={
1512 'X-appenlight-api-key': self.api_key},
1512 'X-appenlight-api-key': self.api_key},
1513 json=self.stats,
1513 json=self.stats,
1514 )
1514 )
1515
1515
1516 if not response.status_code == 200:
1516 if not response.status_code == 200:
1517 pprint.pprint(self.stats)
1517 pprint.pprint(self.stats)
1518 print response.headers
1518 print response.headers
1519 print response.text
1519 print response.text
1520 raise Exception('Sending to appenlight failed')
1520 raise Exception('Sending to appenlight failed')
1521
1521
1522
1522
1523 @pytest.fixture
1523 @pytest.fixture
1524 def gist_util(request, pylonsapp):
1524 def gist_util(request, pylonsapp):
1525 """
1525 """
1526 Provides a wired instance of `GistUtility` with integrated cleanup.
1526 Provides a wired instance of `GistUtility` with integrated cleanup.
1527 """
1527 """
1528 utility = GistUtility()
1528 utility = GistUtility()
1529 request.addfinalizer(utility.cleanup)
1529 request.addfinalizer(utility.cleanup)
1530 return utility
1530 return utility
1531
1531
1532
1532
1533 class GistUtility(object):
1533 class GistUtility(object):
1534 def __init__(self):
1534 def __init__(self):
1535 self.fixture = Fixture()
1535 self.fixture = Fixture()
1536 self.gist_ids = []
1536 self.gist_ids = []
1537
1537
1538 def create_gist(self, **kwargs):
1538 def create_gist(self, **kwargs):
1539 gist = self.fixture.create_gist(**kwargs)
1539 gist = self.fixture.create_gist(**kwargs)
1540 self.gist_ids.append(gist.gist_id)
1540 self.gist_ids.append(gist.gist_id)
1541 return gist
1541 return gist
1542
1542
1543 def cleanup(self):
1543 def cleanup(self):
1544 for id_ in self.gist_ids:
1544 for id_ in self.gist_ids:
1545 self.fixture.destroy_gists(str(id_))
1545 self.fixture.destroy_gists(str(id_))
1546
1546
1547
1547
1548 @pytest.fixture
1548 @pytest.fixture
1549 def enabled_backends(request):
1549 def enabled_backends(request):
1550 backends = request.config.option.backends
1550 backends = request.config.option.backends
1551 return backends[:]
1551 return backends[:]
1552
1552
1553
1553
1554 @pytest.fixture
1554 @pytest.fixture
1555 def settings_util(request):
1555 def settings_util(request):
1556 """
1556 """
1557 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1557 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1558 """
1558 """
1559 utility = SettingsUtility()
1559 utility = SettingsUtility()
1560 request.addfinalizer(utility.cleanup)
1560 request.addfinalizer(utility.cleanup)
1561 return utility
1561 return utility
1562
1562
1563
1563
1564 class SettingsUtility(object):
1564 class SettingsUtility(object):
1565 def __init__(self):
1565 def __init__(self):
1566 self.rhodecode_ui_ids = []
1566 self.rhodecode_ui_ids = []
1567 self.rhodecode_setting_ids = []
1567 self.rhodecode_setting_ids = []
1568 self.repo_rhodecode_ui_ids = []
1568 self.repo_rhodecode_ui_ids = []
1569 self.repo_rhodecode_setting_ids = []
1569 self.repo_rhodecode_setting_ids = []
1570
1570
1571 def create_repo_rhodecode_ui(
1571 def create_repo_rhodecode_ui(
1572 self, repo, section, value, key=None, active=True, cleanup=True):
1572 self, repo, section, value, key=None, active=True, cleanup=True):
1573 key = key or hashlib.sha1(
1573 key = key or hashlib.sha1(
1574 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1574 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1575
1575
1576 setting = RepoRhodeCodeUi()
1576 setting = RepoRhodeCodeUi()
1577 setting.repository_id = repo.repo_id
1577 setting.repository_id = repo.repo_id
1578 setting.ui_section = section
1578 setting.ui_section = section
1579 setting.ui_value = value
1579 setting.ui_value = value
1580 setting.ui_key = key
1580 setting.ui_key = key
1581 setting.ui_active = active
1581 setting.ui_active = active
1582 Session().add(setting)
1582 Session().add(setting)
1583 Session().commit()
1583 Session().commit()
1584
1584
1585 if cleanup:
1585 if cleanup:
1586 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1586 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1587 return setting
1587 return setting
1588
1588
1589 def create_rhodecode_ui(
1589 def create_rhodecode_ui(
1590 self, section, value, key=None, active=True, cleanup=True):
1590 self, section, value, key=None, active=True, cleanup=True):
1591 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1591 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1592
1592
1593 setting = RhodeCodeUi()
1593 setting = RhodeCodeUi()
1594 setting.ui_section = section
1594 setting.ui_section = section
1595 setting.ui_value = value
1595 setting.ui_value = value
1596 setting.ui_key = key
1596 setting.ui_key = key
1597 setting.ui_active = active
1597 setting.ui_active = active
1598 Session().add(setting)
1598 Session().add(setting)
1599 Session().commit()
1599 Session().commit()
1600
1600
1601 if cleanup:
1601 if cleanup:
1602 self.rhodecode_ui_ids.append(setting.ui_id)
1602 self.rhodecode_ui_ids.append(setting.ui_id)
1603 return setting
1603 return setting
1604
1604
1605 def create_repo_rhodecode_setting(
1605 def create_repo_rhodecode_setting(
1606 self, repo, name, value, type_, cleanup=True):
1606 self, repo, name, value, type_, cleanup=True):
1607 setting = RepoRhodeCodeSetting(
1607 setting = RepoRhodeCodeSetting(
1608 repo.repo_id, key=name, val=value, type=type_)
1608 repo.repo_id, key=name, val=value, type=type_)
1609 Session().add(setting)
1609 Session().add(setting)
1610 Session().commit()
1610 Session().commit()
1611
1611
1612 if cleanup:
1612 if cleanup:
1613 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1613 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1614 return setting
1614 return setting
1615
1615
1616 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1616 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1617 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1617 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1618 Session().add(setting)
1618 Session().add(setting)
1619 Session().commit()
1619 Session().commit()
1620
1620
1621 if cleanup:
1621 if cleanup:
1622 self.rhodecode_setting_ids.append(setting.app_settings_id)
1622 self.rhodecode_setting_ids.append(setting.app_settings_id)
1623
1623
1624 return setting
1624 return setting
1625
1625
1626 def cleanup(self):
1626 def cleanup(self):
1627 for id_ in self.rhodecode_ui_ids:
1627 for id_ in self.rhodecode_ui_ids:
1628 setting = RhodeCodeUi.get(id_)
1628 setting = RhodeCodeUi.get(id_)
1629 Session().delete(setting)
1629 Session().delete(setting)
1630
1630
1631 for id_ in self.rhodecode_setting_ids:
1631 for id_ in self.rhodecode_setting_ids:
1632 setting = RhodeCodeSetting.get(id_)
1632 setting = RhodeCodeSetting.get(id_)
1633 Session().delete(setting)
1633 Session().delete(setting)
1634
1634
1635 for id_ in self.repo_rhodecode_ui_ids:
1635 for id_ in self.repo_rhodecode_ui_ids:
1636 setting = RepoRhodeCodeUi.get(id_)
1636 setting = RepoRhodeCodeUi.get(id_)
1637 Session().delete(setting)
1637 Session().delete(setting)
1638
1638
1639 for id_ in self.repo_rhodecode_setting_ids:
1639 for id_ in self.repo_rhodecode_setting_ids:
1640 setting = RepoRhodeCodeSetting.get(id_)
1640 setting = RepoRhodeCodeSetting.get(id_)
1641 Session().delete(setting)
1641 Session().delete(setting)
1642
1642
1643 Session().commit()
1643 Session().commit()
1644
1644
1645
1645
1646 @pytest.fixture
1646 @pytest.fixture
1647 def no_notifications(request):
1647 def no_notifications(request):
1648 notification_patcher = mock.patch(
1648 notification_patcher = mock.patch(
1649 'rhodecode.model.notification.NotificationModel.create')
1649 'rhodecode.model.notification.NotificationModel.create')
1650 notification_patcher.start()
1650 notification_patcher.start()
1651 request.addfinalizer(notification_patcher.stop)
1651 request.addfinalizer(notification_patcher.stop)
1652
1652
1653
1653
1654 @pytest.fixture
1654 @pytest.fixture
1655 def silence_action_logger(request):
1655 def silence_action_logger(request):
1656 notification_patcher = mock.patch(
1656 notification_patcher = mock.patch(
1657 'rhodecode.lib.utils.action_logger')
1657 'rhodecode.lib.utils.action_logger')
1658 notification_patcher.start()
1658 notification_patcher.start()
1659 request.addfinalizer(notification_patcher.stop)
1659 request.addfinalizer(notification_patcher.stop)
1660
1660
1661
1661
1662 @pytest.fixture(scope='session')
1662 @pytest.fixture(scope='session')
1663 def repeat(request):
1663 def repeat(request):
1664 """
1664 """
1665 The number of repetitions is based on this fixture.
1665 The number of repetitions is based on this fixture.
1666
1666
1667 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1667 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1668 tests are not too slow in our default test suite.
1668 tests are not too slow in our default test suite.
1669 """
1669 """
1670 return request.config.getoption('--repeat')
1670 return request.config.getoption('--repeat')
1671
1671
1672
1672
1673 @pytest.fixture
1673 @pytest.fixture
1674 def rhodecode_fixtures():
1674 def rhodecode_fixtures():
1675 return Fixture()
1675 return Fixture()
1676
1676
1677
1677
1678 @pytest.fixture
1678 @pytest.fixture
1679 def request_stub():
1679 def request_stub():
1680 """
1680 """
1681 Stub request object.
1681 Stub request object.
1682 """
1682 """
1683 request = pyramid.testing.DummyRequest()
1683 request = pyramid.testing.DummyRequest()
1684 request.scheme = 'https'
1684 request.scheme = 'https'
1685 return request
1685 return request
1686
1686
1687
1687
1688 @pytest.fixture
1688 @pytest.fixture
1689 def config_stub(request, request_stub):
1689 def config_stub(request, request_stub):
1690 """
1690 """
1691 Set up pyramid.testing and return the Configurator.
1691 Set up pyramid.testing and return the Configurator.
1692 """
1692 """
1693 config = pyramid.testing.setUp(request=request_stub)
1693 config = pyramid.testing.setUp(request=request_stub)
1694
1694
1695 @request.addfinalizer
1695 @request.addfinalizer
1696 def cleanup():
1696 def cleanup():
1697 pyramid.testing.tearDown()
1697 pyramid.testing.tearDown()
1698
1698
1699 return config
1699 return config
1700
1700
1701
1701
1702 @pytest.fixture
1702 @pytest.fixture
1703 def StubIntegrationType():
1703 def StubIntegrationType():
1704 class _StubIntegrationType(IntegrationTypeBase):
1704 class _StubIntegrationType(IntegrationTypeBase):
1705 """ Test integration type class """
1705 """ Test integration type class """
1706
1706
1707 key = 'test'
1707 key = 'test'
1708 display_name = 'Test integration type'
1708 display_name = 'Test integration type'
1709 description = 'A test integration type for testing'
1709 description = 'A test integration type for testing'
1710 icon = 'test_icon_html_image'
1710 icon = 'test_icon_html_image'
1711
1711
1712 def __init__(self, settings):
1712 def __init__(self, settings):
1713 super(_StubIntegrationType, self).__init__(settings)
1713 super(_StubIntegrationType, self).__init__(settings)
1714 self.sent_events = [] # for testing
1714 self.sent_events = [] # for testing
1715
1715
1716 def send_event(self, event):
1716 def send_event(self, event):
1717 self.sent_events.append(event)
1717 self.sent_events.append(event)
1718
1718
1719 def settings_schema(self):
1719 def settings_schema(self):
1720 class SettingsSchema(colander.Schema):
1720 class SettingsSchema(colander.Schema):
1721 test_string_field = colander.SchemaNode(
1721 test_string_field = colander.SchemaNode(
1722 colander.String(),
1722 colander.String(),
1723 missing=colander.required,
1723 missing=colander.required,
1724 title='test string field',
1724 title='test string field',
1725 )
1725 )
1726 test_int_field = colander.SchemaNode(
1726 test_int_field = colander.SchemaNode(
1727 colander.Int(),
1727 colander.Int(),
1728 title='some integer setting',
1728 title='some integer setting',
1729 )
1729 )
1730 return SettingsSchema()
1730 return SettingsSchema()
1731
1731
1732
1732
1733 integration_type_registry.register_integration_type(_StubIntegrationType)
1733 integration_type_registry.register_integration_type(_StubIntegrationType)
1734 return _StubIntegrationType
1734 return _StubIntegrationType
1735
1735
1736 @pytest.fixture
1736 @pytest.fixture
1737 def stub_integration_settings():
1737 def stub_integration_settings():
1738 return {
1738 return {
1739 'test_string_field': 'some data',
1739 'test_string_field': 'some data',
1740 'test_int_field': 100,
1740 'test_int_field': 100,
1741 }
1741 }
1742
1742
1743
1743
1744 @pytest.fixture
1744 @pytest.fixture
1745 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1745 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1746 stub_integration_settings):
1746 stub_integration_settings):
1747 integration = IntegrationModel().create(
1747 integration = IntegrationModel().create(
1748 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1748 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1749 name='test repo integration',
1749 name='test repo integration',
1750 repo=repo_stub, repo_group=None, child_repos_only=None)
1750 repo=repo_stub, repo_group=None, child_repos_only=None)
1751
1751
1752 @request.addfinalizer
1752 @request.addfinalizer
1753 def cleanup():
1753 def cleanup():
1754 IntegrationModel().delete(integration)
1754 IntegrationModel().delete(integration)
1755
1755
1756 return integration
1756 return integration
1757
1757
1758
1758
1759 @pytest.fixture
1759 @pytest.fixture
1760 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1760 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1761 stub_integration_settings):
1761 stub_integration_settings):
1762 integration = IntegrationModel().create(
1762 integration = IntegrationModel().create(
1763 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1763 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1764 name='test repogroup integration',
1764 name='test repogroup integration',
1765 repo=None, repo_group=test_repo_group, child_repos_only=True)
1765 repo=None, repo_group=test_repo_group, child_repos_only=True)
1766
1766
1767 @request.addfinalizer
1767 @request.addfinalizer
1768 def cleanup():
1768 def cleanup():
1769 IntegrationModel().delete(integration)
1769 IntegrationModel().delete(integration)
1770
1770
1771 return integration
1771 return integration
1772
1772
1773
1773
1774 @pytest.fixture
1774 @pytest.fixture
1775 def repogroup_recursive_integration_stub(request, test_repo_group,
1775 def repogroup_recursive_integration_stub(request, test_repo_group,
1776 StubIntegrationType, stub_integration_settings):
1776 StubIntegrationType, stub_integration_settings):
1777 integration = IntegrationModel().create(
1777 integration = IntegrationModel().create(
1778 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1778 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1779 name='test recursive repogroup integration',
1779 name='test recursive repogroup integration',
1780 repo=None, repo_group=test_repo_group, child_repos_only=False)
1780 repo=None, repo_group=test_repo_group, child_repos_only=False)
1781
1781
1782 @request.addfinalizer
1782 @request.addfinalizer
1783 def cleanup():
1783 def cleanup():
1784 IntegrationModel().delete(integration)
1784 IntegrationModel().delete(integration)
1785
1785
1786 return integration
1786 return integration
1787
1787
1788
1788
1789 @pytest.fixture
1789 @pytest.fixture
1790 def global_integration_stub(request, StubIntegrationType,
1790 def global_integration_stub(request, StubIntegrationType,
1791 stub_integration_settings):
1791 stub_integration_settings):
1792 integration = IntegrationModel().create(
1792 integration = IntegrationModel().create(
1793 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1793 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1794 name='test global integration',
1794 name='test global integration',
1795 repo=None, repo_group=None, child_repos_only=None)
1795 repo=None, repo_group=None, child_repos_only=None)
1796
1796
1797 @request.addfinalizer
1797 @request.addfinalizer
1798 def cleanup():
1798 def cleanup():
1799 IntegrationModel().delete(integration)
1799 IntegrationModel().delete(integration)
1800
1800
1801 return integration
1801 return integration
1802
1802
1803
1803
1804 @pytest.fixture
1804 @pytest.fixture
1805 def root_repos_integration_stub(request, StubIntegrationType,
1805 def root_repos_integration_stub(request, StubIntegrationType,
1806 stub_integration_settings):
1806 stub_integration_settings):
1807 integration = IntegrationModel().create(
1807 integration = IntegrationModel().create(
1808 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1808 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1809 name='test global integration',
1809 name='test global integration',
1810 repo=None, repo_group=None, child_repos_only=True)
1810 repo=None, repo_group=None, child_repos_only=True)
1811
1811
1812 @request.addfinalizer
1812 @request.addfinalizer
1813 def cleanup():
1813 def cleanup():
1814 IntegrationModel().delete(integration)
1814 IntegrationModel().delete(integration)
1815
1815
1816 return integration
1816 return integration
General Comments 0
You need to be logged in to leave comments. Login now