##// END OF EJS Templates
api: pull-requests, fixed invocation of merge as another user.
ergo -
r3481:b5202911 default
parent child
Show More
@@ -1,158 +1,259
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import UserLog, PullRequest
23 from rhodecode.model.db import UserLog, PullRequest
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok)
27 build_data, api_call, assert_error, assert_ok)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestMergePullRequest(object):
31 class TestMergePullRequest(object):
32
32
33 @pytest.mark.backends("git", "hg")
33 @pytest.mark.backends("git", "hg")
34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
35 pull_request = pr_util.create_pull_request(mergeable=True)
35 pull_request = pr_util.create_pull_request(mergeable=True)
36 pull_request_id = pull_request.pull_request_id
36 pull_request_id = pull_request.pull_request_id
37 pull_request_repo = pull_request.target_repo.repo_name
37 pull_request_repo = pull_request.target_repo.repo_name
38
38
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'merge_pull_request',
40 self.apikey, 'merge_pull_request',
41 repoid=pull_request_repo,
41 repoid=pull_request_repo,
42 pullrequestid=pull_request_id)
42 pullrequestid=pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 # The above api call detaches the pull request DB object from the
46 # The above api call detaches the pull request DB object from the
47 # session because of an unconditional transaction rollback in our
47 # session because of an unconditional transaction rollback in our
48 # middleware. Therefore we need to add it back here if we want to use it.
48 # middleware. Therefore we need to add it back here if we want to use it.
49 Session().add(pull_request)
49 Session().add(pull_request)
50
50
51 expected = 'merge not possible for following reasons: ' \
51 expected = 'merge not possible for following reasons: ' \
52 'Pull request reviewer approval is pending.'
52 'Pull request reviewer approval is pending.'
53 assert_error(id_, expected, given=response.body)
53 assert_error(id_, expected, given=response.body)
54
54
55 @pytest.mark.backends("git", "hg")
55 @pytest.mark.backends("git", "hg")
56 def test_api_merge_pull_request_merge_failed_disallowed_state(
56 def test_api_merge_pull_request_merge_failed_disallowed_state(
57 self, pr_util, no_notifications):
57 self, pr_util, no_notifications):
58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
59 pull_request_id = pull_request.pull_request_id
59 pull_request_id = pull_request.pull_request_id
60 pull_request_repo = pull_request.target_repo.repo_name
60 pull_request_repo = pull_request.target_repo.repo_name
61
61
62 pr = PullRequest.get(pull_request_id)
62 pr = PullRequest.get(pull_request_id)
63 pr.pull_request_state = pull_request.STATE_UPDATING
63 pr.pull_request_state = pull_request.STATE_UPDATING
64 Session().add(pr)
64 Session().add(pr)
65 Session().commit()
65 Session().commit()
66
66
67 id_, params = build_data(
67 id_, params = build_data(
68 self.apikey, 'merge_pull_request',
68 self.apikey, 'merge_pull_request',
69 repoid=pull_request_repo,
69 repoid=pull_request_repo,
70 pullrequestid=pull_request_id)
70 pullrequestid=pull_request_id)
71
71
72 response = api_call(self.app, params)
72 response = api_call(self.app, params)
73 expected = 'Operation forbidden because pull request is in state {}, '\
73 expected = 'Operation forbidden because pull request is in state {}, '\
74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
75 PullRequest.STATE_CREATED)
75 PullRequest.STATE_CREATED)
76 assert_error(id_, expected, given=response.body)
76 assert_error(id_, expected, given=response.body)
77
77
78 @pytest.mark.backends("git", "hg")
78 @pytest.mark.backends("git", "hg")
79 def test_api_merge_pull_request(self, pr_util, no_notifications):
79 def test_api_merge_pull_request(self, pr_util, no_notifications):
80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
81 author = pull_request.user_id
81 author = pull_request.user_id
82 repo = pull_request.target_repo.repo_id
82 repo = pull_request.target_repo.repo_id
83 pull_request_id = pull_request.pull_request_id
83 pull_request_id = pull_request.pull_request_id
84 pull_request_repo = pull_request.target_repo.repo_name
84 pull_request_repo = pull_request.target_repo.repo_name
85
85
86 id_, params = build_data(
86 id_, params = build_data(
87 self.apikey, 'comment_pull_request',
87 self.apikey, 'comment_pull_request',
88 repoid=pull_request_repo,
88 repoid=pull_request_repo,
89 pullrequestid=pull_request_id,
89 pullrequestid=pull_request_id,
90 status='approved')
90 status='approved')
91
91
92 response = api_call(self.app, params)
92 response = api_call(self.app, params)
93 expected = {
93 expected = {
94 'comment_id': response.json.get('result', {}).get('comment_id'),
94 'comment_id': response.json.get('result', {}).get('comment_id'),
95 'pull_request_id': pull_request_id,
95 'pull_request_id': pull_request_id,
96 'status': {'given': 'approved', 'was_changed': True}
96 'status': {'given': 'approved', 'was_changed': True}
97 }
97 }
98 assert_ok(id_, expected, given=response.body)
98 assert_ok(id_, expected, given=response.body)
99
99
100 id_, params = build_data(
100 id_, params = build_data(
101 self.apikey, 'merge_pull_request',
101 self.apikey, 'merge_pull_request',
102 repoid=pull_request_repo,
102 repoid=pull_request_repo,
103 pullrequestid=pull_request_id)
103 pullrequestid=pull_request_id)
104
104
105 response = api_call(self.app, params)
105 response = api_call(self.app, params)
106
106
107 pull_request = PullRequest.get(pull_request_id)
107 pull_request = PullRequest.get(pull_request_id)
108
108
109 expected = {
109 expected = {
110 'executed': True,
110 'executed': True,
111 'failure_reason': 0,
111 'failure_reason': 0,
112 'merge_status_message': 'This pull request can be automatically merged.',
112 'merge_status_message': 'This pull request can be automatically merged.',
113 'possible': True,
113 'possible': True,
114 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
114 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
115 'merge_ref': pull_request.shadow_merge_ref._asdict()
115 'merge_ref': pull_request.shadow_merge_ref._asdict()
116 }
116 }
117
117
118 assert_ok(id_, expected, response.body)
118 assert_ok(id_, expected, response.body)
119
119
120 journal = UserLog.query()\
120 journal = UserLog.query()\
121 .filter(UserLog.user_id == author)\
121 .filter(UserLog.user_id == author)\
122 .filter(UserLog.repository_id == repo) \
122 .filter(UserLog.repository_id == repo) \
123 .order_by('user_log_id') \
123 .order_by('user_log_id') \
124 .all()
124 .all()
125 assert journal[-2].action == 'repo.pull_request.merge'
125 assert journal[-2].action == 'repo.pull_request.merge'
126 assert journal[-1].action == 'repo.pull_request.close'
126 assert journal[-1].action == 'repo.pull_request.close'
127
127
128 id_, params = build_data(
128 id_, params = build_data(
129 self.apikey, 'merge_pull_request',
129 self.apikey, 'merge_pull_request',
130 repoid=pull_request_repo, pullrequestid=pull_request_id)
130 repoid=pull_request_repo, pullrequestid=pull_request_id)
131 response = api_call(self.app, params)
131 response = api_call(self.app, params)
132
132
133 expected = 'merge not possible for following reasons: This pull request is closed.'
133 expected = 'merge not possible for following reasons: This pull request is closed.'
134 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
135
135
136 @pytest.mark.backends("git", "hg")
136 @pytest.mark.backends("git", "hg")
137 def test_api_merge_pull_request_as_another_user_no_perms_to_merge(
138 self, pr_util, no_notifications, user_util):
139 merge_user = user_util.create_user()
140 merge_user_id = merge_user.user_id
141 merge_user_username = merge_user.username
142
143 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
144
145 pull_request_id = pull_request.pull_request_id
146 pull_request_repo = pull_request.target_repo.repo_name
147
148 id_, params = build_data(
149 self.apikey, 'comment_pull_request',
150 repoid=pull_request_repo,
151 pullrequestid=pull_request_id,
152 status='approved')
153
154 response = api_call(self.app, params)
155 expected = {
156 'comment_id': response.json.get('result', {}).get('comment_id'),
157 'pull_request_id': pull_request_id,
158 'status': {'given': 'approved', 'was_changed': True}
159 }
160 assert_ok(id_, expected, given=response.body)
161 id_, params = build_data(
162 self.apikey, 'merge_pull_request',
163 repoid=pull_request_repo,
164 pullrequestid=pull_request_id,
165 userid=merge_user_id
166 )
167
168 response = api_call(self.app, params)
169 expected = 'merge not possible for following reasons: User `{}` ' \
170 'not allowed to perform merge.'.format(merge_user_username)
171 assert_error(id_, expected, response.body)
172
173 @pytest.mark.backends("git", "hg")
174 def test_api_merge_pull_request_as_another_user(self, pr_util, no_notifications, user_util):
175 merge_user = user_util.create_user()
176 merge_user_id = merge_user.user_id
177 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
178 user_util.grant_user_permission_to_repo(
179 pull_request.target_repo, merge_user, 'repository.write')
180 author = pull_request.user_id
181 repo = pull_request.target_repo.repo_id
182 pull_request_id = pull_request.pull_request_id
183 pull_request_repo = pull_request.target_repo.repo_name
184
185 id_, params = build_data(
186 self.apikey, 'comment_pull_request',
187 repoid=pull_request_repo,
188 pullrequestid=pull_request_id,
189 status='approved')
190
191 response = api_call(self.app, params)
192 expected = {
193 'comment_id': response.json.get('result', {}).get('comment_id'),
194 'pull_request_id': pull_request_id,
195 'status': {'given': 'approved', 'was_changed': True}
196 }
197 assert_ok(id_, expected, given=response.body)
198
199 id_, params = build_data(
200 self.apikey, 'merge_pull_request',
201 repoid=pull_request_repo,
202 pullrequestid=pull_request_id,
203 userid=merge_user_id
204 )
205
206 response = api_call(self.app, params)
207
208 pull_request = PullRequest.get(pull_request_id)
209
210 expected = {
211 'executed': True,
212 'failure_reason': 0,
213 'merge_status_message': 'This pull request can be automatically merged.',
214 'possible': True,
215 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
216 'merge_ref': pull_request.shadow_merge_ref._asdict()
217 }
218
219 assert_ok(id_, expected, response.body)
220
221 journal = UserLog.query() \
222 .filter(UserLog.user_id == merge_user_id) \
223 .filter(UserLog.repository_id == repo) \
224 .order_by('user_log_id') \
225 .all()
226 assert journal[-2].action == 'repo.pull_request.merge'
227 assert journal[-1].action == 'repo.pull_request.close'
228
229 id_, params = build_data(
230 self.apikey, 'merge_pull_request',
231 repoid=pull_request_repo, pullrequestid=pull_request_id, userid=merge_user_id)
232 response = api_call(self.app, params)
233
234 expected = 'merge not possible for following reasons: This pull request is closed.'
235 assert_error(id_, expected, given=response.body)
236
237 @pytest.mark.backends("git", "hg")
137 def test_api_merge_pull_request_repo_error(self, pr_util):
238 def test_api_merge_pull_request_repo_error(self, pr_util):
138 pull_request = pr_util.create_pull_request()
239 pull_request = pr_util.create_pull_request()
139 id_, params = build_data(
240 id_, params = build_data(
140 self.apikey, 'merge_pull_request',
241 self.apikey, 'merge_pull_request',
141 repoid=666, pullrequestid=pull_request.pull_request_id)
242 repoid=666, pullrequestid=pull_request.pull_request_id)
142 response = api_call(self.app, params)
243 response = api_call(self.app, params)
143
244
144 expected = 'repository `666` does not exist'
245 expected = 'repository `666` does not exist'
145 assert_error(id_, expected, given=response.body)
246 assert_error(id_, expected, given=response.body)
146
247
147 @pytest.mark.backends("git", "hg")
248 @pytest.mark.backends("git", "hg")
148 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
249 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
149 pull_request = pr_util.create_pull_request(mergeable=True)
250 pull_request = pr_util.create_pull_request(mergeable=True)
150 id_, params = build_data(
251 id_, params = build_data(
151 self.apikey_regular, 'merge_pull_request',
252 self.apikey_regular, 'merge_pull_request',
152 repoid=pull_request.target_repo.repo_name,
253 repoid=pull_request.target_repo.repo_name,
153 pullrequestid=pull_request.pull_request_id,
254 pullrequestid=pull_request.pull_request_id,
154 userid=TEST_USER_ADMIN_LOGIN)
255 userid=TEST_USER_ADMIN_LOGIN)
155 response = api_call(self.app, params)
256 response = api_call(self.app, params)
156
257
157 expected = 'userid is not the same as your user'
258 expected = 'userid is not the same as your user'
158 assert_error(id_, expected, given=response.body)
259 assert_error(id_, expected, given=response.body)
@@ -1,996 +1,997
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 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 import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124
124
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if Optional.extract(repoid):
126 if Optional.extract(repoid):
127 repo = get_repo_or_error(repoid)
127 repo = get_repo_or_error(repoid)
128 else:
128 else:
129 repo = pull_request.target_repo
129 repo = pull_request.target_repo
130
130
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
132 raise JSONRPCError('repository `%s` or pull request `%s` '
132 raise JSONRPCError('repository `%s` or pull request `%s` '
133 'does not exist' % (repoid, pullrequestid))
133 'does not exist' % (repoid, pullrequestid))
134
134
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
136 # otherwise we can lock the repo on calculation of merge state while update/merge
136 # otherwise we can lock the repo on calculation of merge state while update/merge
137 # is happening.
137 # is happening.
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
139 data = pull_request.get_api_data(with_merge_state=merge_state)
139 data = pull_request.get_api_data(with_merge_state=merge_state)
140 return data
140 return data
141
141
142
142
143 @jsonrpc_method()
143 @jsonrpc_method()
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
145 merge_state=Optional(True)):
145 merge_state=Optional(True)):
146 """
146 """
147 Get all pull requests from the repository specified in `repoid`.
147 Get all pull requests from the repository specified in `repoid`.
148
148
149 :param apiuser: This is filled automatically from the |authtoken|.
149 :param apiuser: This is filled automatically from the |authtoken|.
150 :type apiuser: AuthUser
150 :type apiuser: AuthUser
151 :param repoid: Optional repository name or repository ID.
151 :param repoid: Optional repository name or repository ID.
152 :type repoid: str or int
152 :type repoid: str or int
153 :param status: Only return pull requests with the specified status.
153 :param status: Only return pull requests with the specified status.
154 Valid options are.
154 Valid options are.
155 * ``new`` (default)
155 * ``new`` (default)
156 * ``open``
156 * ``open``
157 * ``closed``
157 * ``closed``
158 :type status: str
158 :type status: str
159 :param merge_state: Optional calculate merge state for each repository.
159 :param merge_state: Optional calculate merge state for each repository.
160 This could result in longer time to fetch the data
160 This could result in longer time to fetch the data
161 :type merge_state: bool
161 :type merge_state: bool
162
162
163 Example output:
163 Example output:
164
164
165 .. code-block:: bash
165 .. code-block:: bash
166
166
167 "id": <id_given_in_input>,
167 "id": <id_given_in_input>,
168 "result":
168 "result":
169 [
169 [
170 ...
170 ...
171 {
171 {
172 "pull_request_id": "<pull_request_id>",
172 "pull_request_id": "<pull_request_id>",
173 "url": "<url>",
173 "url": "<url>",
174 "title" : "<title>",
174 "title" : "<title>",
175 "description": "<description>",
175 "description": "<description>",
176 "status": "<status>",
176 "status": "<status>",
177 "created_on": "<date_time_created>",
177 "created_on": "<date_time_created>",
178 "updated_on": "<date_time_updated>",
178 "updated_on": "<date_time_updated>",
179 "commit_ids": [
179 "commit_ids": [
180 ...
180 ...
181 "<commit_id>",
181 "<commit_id>",
182 "<commit_id>",
182 "<commit_id>",
183 ...
183 ...
184 ],
184 ],
185 "review_status": "<review_status>",
185 "review_status": "<review_status>",
186 "mergeable": {
186 "mergeable": {
187 "status": "<bool>",
187 "status": "<bool>",
188 "message: "<message>",
188 "message: "<message>",
189 },
189 },
190 "source": {
190 "source": {
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 "target": {
199 "target": {
200 "clone_url": "<clone_url>",
200 "clone_url": "<clone_url>",
201 "reference":
201 "reference":
202 {
202 {
203 "name": "<name>",
203 "name": "<name>",
204 "type": "<type>",
204 "type": "<type>",
205 "commit_id": "<commit_id>",
205 "commit_id": "<commit_id>",
206 }
206 }
207 },
207 },
208 "merge": {
208 "merge": {
209 "clone_url": "<clone_url>",
209 "clone_url": "<clone_url>",
210 "reference":
210 "reference":
211 {
211 {
212 "name": "<name>",
212 "name": "<name>",
213 "type": "<type>",
213 "type": "<type>",
214 "commit_id": "<commit_id>",
214 "commit_id": "<commit_id>",
215 }
215 }
216 },
216 },
217 "author": <user_obj>,
217 "author": <user_obj>,
218 "reviewers": [
218 "reviewers": [
219 ...
219 ...
220 {
220 {
221 "user": "<user_obj>",
221 "user": "<user_obj>",
222 "review_status": "<review_status>",
222 "review_status": "<review_status>",
223 }
223 }
224 ...
224 ...
225 ]
225 ]
226 }
226 }
227 ...
227 ...
228 ],
228 ],
229 "error": null
229 "error": null
230
230
231 """
231 """
232 repo = get_repo_or_error(repoid)
232 repo = get_repo_or_error(repoid)
233 if not has_superadmin_permission(apiuser):
233 if not has_superadmin_permission(apiuser):
234 _perms = (
234 _perms = (
235 'repository.admin', 'repository.write', 'repository.read',)
235 'repository.admin', 'repository.write', 'repository.read',)
236 validate_repo_permissions(apiuser, repoid, repo, _perms)
236 validate_repo_permissions(apiuser, repoid, repo, _perms)
237
237
238 status = Optional.extract(status)
238 status = Optional.extract(status)
239 merge_state = Optional.extract(merge_state, binary=True)
239 merge_state = Optional.extract(merge_state, binary=True)
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
241 order_by='id', order_dir='desc')
241 order_by='id', order_dir='desc')
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
243 return data
243 return data
244
244
245
245
246 @jsonrpc_method()
246 @jsonrpc_method()
247 def merge_pull_request(
247 def merge_pull_request(
248 request, apiuser, pullrequestid, repoid=Optional(None),
248 request, apiuser, pullrequestid, repoid=Optional(None),
249 userid=Optional(OAttr('apiuser'))):
249 userid=Optional(OAttr('apiuser'))):
250 """
250 """
251 Merge the pull request specified by `pullrequestid` into its target
251 Merge the pull request specified by `pullrequestid` into its target
252 repository.
252 repository.
253
253
254 :param apiuser: This is filled automatically from the |authtoken|.
254 :param apiuser: This is filled automatically from the |authtoken|.
255 :type apiuser: AuthUser
255 :type apiuser: AuthUser
256 :param repoid: Optional, repository name or repository ID of the
256 :param repoid: Optional, repository name or repository ID of the
257 target repository to which the |pr| is to be merged.
257 target repository to which the |pr| is to be merged.
258 :type repoid: str or int
258 :type repoid: str or int
259 :param pullrequestid: ID of the pull request which shall be merged.
259 :param pullrequestid: ID of the pull request which shall be merged.
260 :type pullrequestid: int
260 :type pullrequestid: int
261 :param userid: Merge the pull request as this user.
261 :param userid: Merge the pull request as this user.
262 :type userid: Optional(str or int)
262 :type userid: Optional(str or int)
263
263
264 Example output:
264 Example output:
265
265
266 .. code-block:: bash
266 .. code-block:: bash
267
267
268 "id": <id_given_in_input>,
268 "id": <id_given_in_input>,
269 "result": {
269 "result": {
270 "executed": "<bool>",
270 "executed": "<bool>",
271 "failure_reason": "<int>",
271 "failure_reason": "<int>",
272 "merge_status_message": "<str>",
272 "merge_status_message": "<str>",
273 "merge_commit_id": "<merge_commit_id>",
273 "merge_commit_id": "<merge_commit_id>",
274 "possible": "<bool>",
274 "possible": "<bool>",
275 "merge_ref": {
275 "merge_ref": {
276 "commit_id": "<commit_id>",
276 "commit_id": "<commit_id>",
277 "type": "<type>",
277 "type": "<type>",
278 "name": "<name>"
278 "name": "<name>"
279 }
279 }
280 },
280 },
281 "error": null
281 "error": null
282 """
282 """
283 pull_request = get_pull_request_or_error(pullrequestid)
283 pull_request = get_pull_request_or_error(pullrequestid)
284 if Optional.extract(repoid):
284 if Optional.extract(repoid):
285 repo = get_repo_or_error(repoid)
285 repo = get_repo_or_error(repoid)
286 else:
286 else:
287 repo = pull_request.target_repo
287 repo = pull_request.target_repo
288
288 auth_user = apiuser
289 if not isinstance(userid, Optional):
289 if not isinstance(userid, Optional):
290 if (has_superadmin_permission(apiuser) or
290 if (has_superadmin_permission(apiuser) or
291 HasRepoPermissionAnyApi('repository.admin')(
291 HasRepoPermissionAnyApi('repository.admin')(
292 user=apiuser, repo_name=repo.repo_name)):
292 user=apiuser, repo_name=repo.repo_name)):
293 apiuser = get_user_or_error(userid)
293 apiuser = get_user_or_error(userid)
294 auth_user = apiuser.AuthUser()
294 else:
295 else:
295 raise JSONRPCError('userid is not the same as your user')
296 raise JSONRPCError('userid is not the same as your user')
296
297
297 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
298 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
298 raise JSONRPCError(
299 raise JSONRPCError(
299 'Operation forbidden because pull request is in state {}, '
300 'Operation forbidden because pull request is in state {}, '
300 'only state {} is allowed.'.format(
301 'only state {} is allowed.'.format(
301 pull_request.pull_request_state, PullRequest.STATE_CREATED))
302 pull_request.pull_request_state, PullRequest.STATE_CREATED))
302
303
303 with pull_request.set_state(PullRequest.STATE_UPDATING):
304 with pull_request.set_state(PullRequest.STATE_UPDATING):
304 check = MergeCheck.validate(
305 check = MergeCheck.validate(pull_request, auth_user=auth_user,
305 pull_request, auth_user=apiuser,
306 translator=request.translate)
306 translator=request.translate)
307 merge_possible = not check.failed
307 merge_possible = not check.failed
308
308
309 if not merge_possible:
309 if not merge_possible:
310 error_messages = []
310 error_messages = []
311 for err_type, error_msg in check.errors:
311 for err_type, error_msg in check.errors:
312 error_msg = request.translate(error_msg)
312 error_msg = request.translate(error_msg)
313 error_messages.append(error_msg)
313 error_messages.append(error_msg)
314
314
315 reasons = ','.join(error_messages)
315 reasons = ','.join(error_messages)
316 raise JSONRPCError(
316 raise JSONRPCError(
317 'merge not possible for following reasons: {}'.format(reasons))
317 'merge not possible for following reasons: {}'.format(reasons))
318
318
319 target_repo = pull_request.target_repo
319 target_repo = pull_request.target_repo
320 extras = vcs_operation_context(
320 extras = vcs_operation_context(
321 request.environ, repo_name=target_repo.repo_name,
321 request.environ, repo_name=target_repo.repo_name,
322 username=apiuser.username, action='push',
322 username=auth_user.username, action='push',
323 scm=target_repo.repo_type)
323 scm=target_repo.repo_type)
324 with pull_request.set_state(PullRequest.STATE_UPDATING):
324 with pull_request.set_state(PullRequest.STATE_UPDATING):
325 merge_response = PullRequestModel().merge_repo(
325 merge_response = PullRequestModel().merge_repo(
326 pull_request, apiuser, extras=extras)
326 pull_request, apiuser, extras=extras)
327 if merge_response.executed:
327 if merge_response.executed:
328 PullRequestModel().close_pull_request(
328 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
329 pull_request.pull_request_id, apiuser)
330
329
331 Session().commit()
330 Session().commit()
332
331
333 # In previous versions the merge response directly contained the merge
332 # In previous versions the merge response directly contained the merge
334 # commit id. It is now contained in the merge reference object. To be
333 # commit id. It is now contained in the merge reference object. To be
335 # backwards compatible we have to extract it again.
334 # backwards compatible we have to extract it again.
336 merge_response = merge_response.asdict()
335 merge_response = merge_response.asdict()
337 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
336 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
338
337
339 return merge_response
338 return merge_response
340
339
341
340
342 @jsonrpc_method()
341 @jsonrpc_method()
343 def get_pull_request_comments(
342 def get_pull_request_comments(
344 request, apiuser, pullrequestid, repoid=Optional(None)):
343 request, apiuser, pullrequestid, repoid=Optional(None)):
345 """
344 """
346 Get all comments of pull request specified with the `pullrequestid`
345 Get all comments of pull request specified with the `pullrequestid`
347
346
348 :param apiuser: This is filled automatically from the |authtoken|.
347 :param apiuser: This is filled automatically from the |authtoken|.
349 :type apiuser: AuthUser
348 :type apiuser: AuthUser
350 :param repoid: Optional repository name or repository ID.
349 :param repoid: Optional repository name or repository ID.
351 :type repoid: str or int
350 :type repoid: str or int
352 :param pullrequestid: The pull request ID.
351 :param pullrequestid: The pull request ID.
353 :type pullrequestid: int
352 :type pullrequestid: int
354
353
355 Example output:
354 Example output:
356
355
357 .. code-block:: bash
356 .. code-block:: bash
358
357
359 id : <id_given_in_input>
358 id : <id_given_in_input>
360 result : [
359 result : [
361 {
360 {
362 "comment_author": {
361 "comment_author": {
363 "active": true,
362 "active": true,
364 "full_name_or_username": "Tom Gore",
363 "full_name_or_username": "Tom Gore",
365 "username": "admin"
364 "username": "admin"
366 },
365 },
367 "comment_created_on": "2017-01-02T18:43:45.533",
366 "comment_created_on": "2017-01-02T18:43:45.533",
368 "comment_f_path": null,
367 "comment_f_path": null,
369 "comment_id": 25,
368 "comment_id": 25,
370 "comment_lineno": null,
369 "comment_lineno": null,
371 "comment_status": {
370 "comment_status": {
372 "status": "under_review",
371 "status": "under_review",
373 "status_lbl": "Under Review"
372 "status_lbl": "Under Review"
374 },
373 },
375 "comment_text": "Example text",
374 "comment_text": "Example text",
376 "comment_type": null,
375 "comment_type": null,
377 "pull_request_version": null
376 "pull_request_version": null
378 }
377 }
379 ],
378 ],
380 error : null
379 error : null
381 """