##// END OF EJS Templates
api: creation of pull_request now honor additional reviewers when using reviewer rules.
marcink -
r2881:00ce3d87 default
parent child Browse files
Show More
@@ -1,311 +1,337 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.model.db import User
24 24 from rhodecode.model.pull_request import PullRequestModel
25 25 from rhodecode.model.repo import RepoModel
26 26 from rhodecode.model.user import UserModel
27 27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
28 28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
29 29
30 30
31 31 @pytest.mark.usefixtures("testuser_api", "app")
32 32 class TestCreatePullRequestApi(object):
33 33 finalizers = []
34 34
35 35 def teardown_method(self, method):
36 36 if self.finalizers:
37 37 for finalizer in self.finalizers:
38 38 finalizer()
39 39 self.finalizers = []
40 40
41 41 def test_create_with_wrong_data(self):
42 42 required_data = {
43 43 'source_repo': 'tests/source_repo',
44 44 'target_repo': 'tests/target_repo',
45 45 'source_ref': 'branch:default:initial',
46 46 'target_ref': 'branch:default:new-feature',
47 47 }
48 48 for key in required_data:
49 49 data = required_data.copy()
50 50 data.pop(key)
51 51 id_, params = build_data(
52 52 self.apikey, 'create_pull_request', **data)
53 53 response = api_call(self.app, params)
54 54
55 55 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
56 56 assert_error(id_, expected, given=response.body)
57 57
58 58 @pytest.mark.backends("git", "hg")
59 59 def test_create_with_correct_data(self, backend):
60 60 data = self._prepare_data(backend)
61 61 RepoModel().revoke_user_permission(
62 62 self.source.repo_name, User.DEFAULT_USER)
63 63 id_, params = build_data(
64 64 self.apikey_regular, 'create_pull_request', **data)
65 65 response = api_call(self.app, params)
66 66 expected_message = "Created new pull request `{title}`".format(
67 67 title=data['title'])
68 68 result = response.json
69 69 assert result['result']['msg'] == expected_message
70 70 pull_request_id = result['result']['pull_request_id']
71 71 pull_request = PullRequestModel().get(pull_request_id)
72 72 assert pull_request.title == data['title']
73 73 assert pull_request.description == data['description']
74 74 assert pull_request.source_ref == data['source_ref']
75 75 assert pull_request.target_ref == data['target_ref']
76 76 assert pull_request.source_repo.repo_name == data['source_repo']
77 77 assert pull_request.target_repo.repo_name == data['target_repo']
78 78 assert pull_request.revisions == [self.commit_ids['change']]
79 79 assert len(pull_request.reviewers) == 1
80 80
81 81 @pytest.mark.backends("git", "hg")
82 82 def test_create_with_empty_description(self, backend):
83 83 data = self._prepare_data(backend)
84 84 data.pop('description')
85 85 id_, params = build_data(
86 86 self.apikey_regular, 'create_pull_request', **data)
87 87 response = api_call(self.app, params)
88 88 expected_message = "Created new pull request `{title}`".format(
89 89 title=data['title'])
90 90 result = response.json
91 91 assert result['result']['msg'] == expected_message
92 92 pull_request_id = result['result']['pull_request_id']
93 93 pull_request = PullRequestModel().get(pull_request_id)
94 94 assert pull_request.description == ''
95 95
96 96 @pytest.mark.backends("git", "hg")
97 97 def test_create_with_empty_title(self, backend):
98 98 data = self._prepare_data(backend)
99 99 data.pop('title')
100 100 id_, params = build_data(
101 101 self.apikey_regular, 'create_pull_request', **data)
102 102 response = api_call(self.app, params)
103 103 result = response.json
104 104 pull_request_id = result['result']['pull_request_id']
105 105 pull_request = PullRequestModel().get(pull_request_id)
106 106 data['ref'] = backend.default_branch_name
107 107 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
108 108 assert pull_request.title == title
109 109
110 110 @pytest.mark.backends("git", "hg")
111 111 def test_create_with_reviewers_specified_by_names(
112 112 self, backend, no_notifications):
113 113 data = self._prepare_data(backend)
114 114 reviewers = [
115 115 {'username': TEST_USER_REGULAR_LOGIN,
116 'reasons': ['added manually']},
116 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
117 117 {'username': TEST_USER_ADMIN_LOGIN,
118 'reasons': ['added manually']},
118 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
119 'mandatory': True},
119 120 ]
120 121 data['reviewers'] = reviewers
122
121 123 id_, params = build_data(
122 124 self.apikey_regular, 'create_pull_request', **data)
123 125 response = api_call(self.app, params)
124 126
125 127 expected_message = "Created new pull request `{title}`".format(
126 128 title=data['title'])
127 129 result = response.json
128 130 assert result['result']['msg'] == expected_message
129 131 pull_request_id = result['result']['pull_request_id']
130 132 pull_request = PullRequestModel().get(pull_request_id)
131 actual_reviewers = [
132 {'username': r.user.username,
133 'reasons': ['added manually'],
134 } for r in pull_request.reviewers
135 ]
136 assert sorted(actual_reviewers) == sorted(reviewers)
133
134 actual_reviewers = []
135 for rev in pull_request.reviewers:
136 entry = {
137 'username': rev.user.username,
138 'reasons': rev.reasons,
139 }
140 if rev.mandatory:
141 entry['mandatory'] = rev.mandatory
142 actual_reviewers.append(entry)
143
144 # default reviewer will be added who is an owner of the repo
145 reviewers.append(
146 {'username': pull_request.author.username,
147 'reasons': [u'Default reviewer', u'Repository owner']},
148 )
149 assert sorted(actual_reviewers, key=lambda e: e['username']) \
150 == sorted(reviewers, key=lambda e: e['username'])
137 151
138 152 @pytest.mark.backends("git", "hg")
139 153 def test_create_with_reviewers_specified_by_ids(
140 154 self, backend, no_notifications):
141 155 data = self._prepare_data(backend)
142 156 reviewers = [
143 157 {'username': UserModel().get_by_username(
144 158 TEST_USER_REGULAR_LOGIN).user_id,
145 159 'reasons': ['added manually']},
146 160 {'username': UserModel().get_by_username(
147 161 TEST_USER_ADMIN_LOGIN).user_id,
148 162 'reasons': ['added manually']},
149 163 ]
150 164
151 165 data['reviewers'] = reviewers
152 166 id_, params = build_data(
153 167 self.apikey_regular, 'create_pull_request', **data)
154 168 response = api_call(self.app, params)
155 169
156 170 expected_message = "Created new pull request `{title}`".format(
157 171 title=data['title'])
158 172 result = response.json
159 173 assert result['result']['msg'] == expected_message
160 174 pull_request_id = result['result']['pull_request_id']
161 175 pull_request = PullRequestModel().get(pull_request_id)
162 actual_reviewers = [
163 {'username': r.user.user_id,
164 'reasons': ['added manually'],
165 } for r in pull_request.reviewers
166 ]
167 assert sorted(actual_reviewers) == sorted(reviewers)
176
177 actual_reviewers = []
178 for rev in pull_request.reviewers:
179 entry = {
180 'username': rev.user.user_id,
181 'reasons': rev.reasons,
182 }
183 if rev.mandatory:
184 entry['mandatory'] = rev.mandatory
185 actual_reviewers.append(entry)
186 # default reviewer will be added who is an owner of the repo
187 reviewers.append(
188 {'username': pull_request.author.user_id,
189 'reasons': [u'Default reviewer', u'Repository owner']},
190 )
191 assert sorted(actual_reviewers, key=lambda e: e['username']) \
192 == sorted(reviewers, key=lambda e: e['username'])
168 193
169 194 @pytest.mark.backends("git", "hg")
170 195 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
171 196 data = self._prepare_data(backend)
172 197 data['reviewers'] = [{'username': 'somebody'}]
173 198 id_, params = build_data(
174 199 self.apikey_regular, 'create_pull_request', **data)
175 200 response = api_call(self.app, params)
176 201 expected_message = 'user `somebody` does not exist'
177 202 assert_error(id_, expected_message, given=response.body)
178 203
179 204 @pytest.mark.backends("git", "hg")
180 205 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
181 206 data = self._prepare_data(backend)
182 207 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
183 208 data['reviewers'] = reviewers
184 209 id_, params = build_data(
185 210 self.apikey_regular, 'create_pull_request', **data)
186 211 response = api_call(self.app, params)
187 212 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
188 213 assert_error(id_, expected_message, given=response.body)
189 214
190 215 @pytest.mark.backends("git", "hg")
191 216 def test_create_with_no_commit_hashes(self, backend):
192 217 data = self._prepare_data(backend)
193 218 expected_source_ref = data['source_ref']
194 219 expected_target_ref = data['target_ref']
195 220 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
196 221 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
197 222 id_, params = build_data(
198 223 self.apikey_regular, 'create_pull_request', **data)
199 224 response = api_call(self.app, params)
200 225 expected_message = "Created new pull request `{title}`".format(
201 226 title=data['title'])
202 227 result = response.json
203 228 assert result['result']['msg'] == expected_message
204 229 pull_request_id = result['result']['pull_request_id']
205 230 pull_request = PullRequestModel().get(pull_request_id)
206 231 assert pull_request.source_ref == expected_source_ref
207 232 assert pull_request.target_ref == expected_target_ref
208 233
209 234 @pytest.mark.backends("git", "hg")
210 235 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
211 236 def test_create_fails_with_wrong_repo(self, backend, data_key):
212 237 repo_name = 'fake-repo'
213 238 data = self._prepare_data(backend)
214 239 data[data_key] = repo_name
215 240 id_, params = build_data(
216 241 self.apikey_regular, 'create_pull_request', **data)
217 242 response = api_call(self.app, params)
218 243 expected_message = 'repository `{}` does not exist'.format(repo_name)
219 244 assert_error(id_, expected_message, given=response.body)
220 245
221 246 @pytest.mark.backends("git", "hg")
222 247 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
223 248 def test_create_fails_with_non_existing_branch(self, backend, data_key):
224 249 branch_name = 'test-branch'
225 250 data = self._prepare_data(backend)
226 251 data[data_key] = "branch:{}".format(branch_name)
227 252 id_, params = build_data(
228 253 self.apikey_regular, 'create_pull_request', **data)
229 254 response = api_call(self.app, params)
230 255 expected_message = 'The specified branch `{}` does not exist'.format(
231 256 branch_name)
232 257 assert_error(id_, expected_message, given=response.body)
233 258
234 259 @pytest.mark.backends("git", "hg")
235 260 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
236 261 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
237 262 data = self._prepare_data(backend)
238 263 ref = 'stange-ref'
239 264 data[data_key] = ref
240 265 id_, params = build_data(
241 266 self.apikey_regular, 'create_pull_request', **data)
242 267 response = api_call(self.app, params)
243 268 expected_message = (
244 269 'Ref `{ref}` given in a wrong format. Please check the API'
245 270 ' documentation for more details'.format(ref=ref))
246 271 assert_error(id_, expected_message, given=response.body)
247 272
248 273 @pytest.mark.backends("git", "hg")
249 274 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
250 275 def test_create_fails_with_non_existing_ref(self, backend, data_key):
251 276 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
252 277 ref = self._get_full_ref(backend, commit_id)
253 278 data = self._prepare_data(backend)
254 279 data[data_key] = ref
255 280 id_, params = build_data(
256 281 self.apikey_regular, 'create_pull_request', **data)
257 282 response = api_call(self.app, params)
258 283 expected_message = 'Ref `{}` does not exist'.format(ref)
259 284 assert_error(id_, expected_message, given=response.body)
260 285
261 286 @pytest.mark.backends("git", "hg")
262 287 def test_create_fails_when_no_revisions(self, backend):
263 288 data = self._prepare_data(backend, source_head='initial')
264 289 id_, params = build_data(
265 290 self.apikey_regular, 'create_pull_request', **data)
266 291 response = api_call(self.app, params)
267 292 expected_message = 'no commits found'
268 293 assert_error(id_, expected_message, given=response.body)
269 294
270 295 @pytest.mark.backends("git", "hg")
271 296 def test_create_fails_when_no_permissions(self, backend):
272 297 data = self._prepare_data(backend)
273 298 RepoModel().revoke_user_permission(
274 self.source.repo_name, User.DEFAULT_USER)
299 self.source.repo_name, self.test_user)
275 300 RepoModel().revoke_user_permission(
276 self.source.repo_name, self.test_user)
301 self.source.repo_name, User.DEFAULT_USER)
302
277 303 id_, params = build_data(
278 304 self.apikey_regular, 'create_pull_request', **data)
279 305 response = api_call(self.app, params)
280 306 expected_message = 'repository `{}` does not exist'.format(
281 307 self.source.repo_name)
282 308 assert_error(id_, expected_message, given=response.body)
283 309
284 310 def _prepare_data(
285 311 self, backend, source_head='change', target_head='initial'):
286 312 commits = [
287 313 {'message': 'initial'},
288 314 {'message': 'change'},
289 315 {'message': 'new-feature', 'parents': ['initial']},
290 316 ]
291 317 self.commit_ids = backend.create_master_repo(commits)
292 318 self.source = backend.create_repo(heads=[source_head])
293 319 self.target = backend.create_repo(heads=[target_head])
294 320
295 321 data = {
296 322 'source_repo': self.source.repo_name,
297 323 'target_repo': self.target.repo_name,
298 324 'source_ref': self._get_full_ref(
299 325 backend, self.commit_ids[source_head]),
300 326 'target_ref': self._get_full_ref(
301 327 backend, self.commit_ids[target_head]),
302 328 'title': 'Test PR 1',
303 329 'description': 'Test'
304 330 }
305 331 RepoModel().grant_user_permission(
306 332 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
307 333 return data
308 334
309 335 def _get_full_ref(self, backend, commit_id):
310 336 return 'branch:{branch}:{commit_id}'.format(
311 337 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,919 +1,919 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from rhodecode import events
25 25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 26 from rhodecode.api.utils import (
27 27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 29 validate_repo_permissions, resolve_ref_or_error)
30 30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 31 from rhodecode.lib.base import vcs_operation_context
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 34 from rhodecode.model.comment import CommentsModel
35 35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
36 36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 37 from rhodecode.model.settings import SettingsModel
38 38 from rhodecode.model.validation_schema import Invalid
39 39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 40 ReviewerListSchema)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 @jsonrpc_method()
46 46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 47 """
48 48 Get a pull request based on the given ID.
49 49
50 50 :param apiuser: This is filled automatically from the |authtoken|.
51 51 :type apiuser: AuthUser
52 52 :param repoid: Optional, repository name or repository ID from where
53 53 the pull request was opened.
54 54 :type repoid: str or int
55 55 :param pullrequestid: ID of the requested pull request.
56 56 :type pullrequestid: int
57 57
58 58 Example output:
59 59
60 60 .. code-block:: bash
61 61
62 62 "id": <id_given_in_input>,
63 63 "result":
64 64 {
65 65 "pull_request_id": "<pull_request_id>",
66 66 "url": "<url>",
67 67 "title": "<title>",
68 68 "description": "<description>",
69 69 "status" : "<status>",
70 70 "created_on": "<date_time_created>",
71 71 "updated_on": "<date_time_updated>",
72 72 "commit_ids": [
73 73 ...
74 74 "<commit_id>",
75 75 "<commit_id>",
76 76 ...
77 77 ],
78 78 "review_status": "<review_status>",
79 79 "mergeable": {
80 80 "status": "<bool>",
81 81 "message": "<message>",
82 82 },
83 83 "source": {
84 84 "clone_url": "<clone_url>",
85 85 "repository": "<repository_name>",
86 86 "reference":
87 87 {
88 88 "name": "<name>",
89 89 "type": "<type>",
90 90 "commit_id": "<commit_id>",
91 91 }
92 92 },
93 93 "target": {
94 94 "clone_url": "<clone_url>",
95 95 "repository": "<repository_name>",
96 96 "reference":
97 97 {
98 98 "name": "<name>",
99 99 "type": "<type>",
100 100 "commit_id": "<commit_id>",
101 101 }
102 102 },
103 103 "merge": {
104 104 "clone_url": "<clone_url>",
105 105 "reference":
106 106 {
107 107 "name": "<name>",
108 108 "type": "<type>",
109 109 "commit_id": "<commit_id>",
110 110 }
111 111 },
112 112 "author": <user_obj>,
113 113 "reviewers": [
114 114 ...
115 115 {
116 116 "user": "<user_obj>",
117 117 "review_status": "<review_status>",
118 118 }
119 119 ...
120 120 ]
121 121 },
122 122 "error": null
123 123 """
124 124
125 125 pull_request = get_pull_request_or_error(pullrequestid)
126 126 if Optional.extract(repoid):
127 127 repo = get_repo_or_error(repoid)
128 128 else:
129 129 repo = pull_request.target_repo
130 130
131 131 if not PullRequestModel().check_user_read(
132 132 pull_request, apiuser, api=True):
133 133 raise JSONRPCError('repository `%s` or pull request `%s` '
134 134 'does not exist' % (repoid, pullrequestid))
135 135 data = pull_request.get_api_data()
136 136 return data
137 137
138 138
139 139 @jsonrpc_method()
140 140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
141 141 """
142 142 Get all pull requests from the repository specified in `repoid`.
143 143
144 144 :param apiuser: This is filled automatically from the |authtoken|.
145 145 :type apiuser: AuthUser
146 146 :param repoid: Optional repository name or repository ID.
147 147 :type repoid: str or int
148 148 :param status: Only return pull requests with the specified status.
149 149 Valid options are.
150 150 * ``new`` (default)
151 151 * ``open``
152 152 * ``closed``
153 153 :type status: str
154 154
155 155 Example output:
156 156
157 157 .. code-block:: bash
158 158
159 159 "id": <id_given_in_input>,
160 160 "result":
161 161 [
162 162 ...
163 163 {
164 164 "pull_request_id": "<pull_request_id>",
165 165 "url": "<url>",
166 166 "title" : "<title>",
167 167 "description": "<description>",
168 168 "status": "<status>",
169 169 "created_on": "<date_time_created>",
170 170 "updated_on": "<date_time_updated>",
171 171 "commit_ids": [
172 172 ...
173 173 "<commit_id>",
174 174 "<commit_id>",
175 175 ...
176 176 ],
177 177 "review_status": "<review_status>",
178 178 "mergeable": {
179 179 "status": "<bool>",
180 180 "message: "<message>",
181 181 },
182 182 "source": {
183 183 "clone_url": "<clone_url>",
184 184 "reference":
185 185 {
186 186 "name": "<name>",
187 187 "type": "<type>",
188 188 "commit_id": "<commit_id>",
189 189 }
190 190 },
191 191 "target": {
192 192 "clone_url": "<clone_url>",
193 193 "reference":
194 194 {
195 195 "name": "<name>",
196 196 "type": "<type>",
197 197 "commit_id": "<commit_id>",
198 198 }
199 199 },
200 200 "merge": {
201 201 "clone_url": "<clone_url>",
202 202 "reference":
203 203 {
204 204 "name": "<name>",
205 205 "type": "<type>",
206 206 "commit_id": "<commit_id>",
207 207 }
208 208 },
209 209 "author": <user_obj>,
210 210 "reviewers": [
211 211 ...
212 212 {
213 213 "user": "<user_obj>",
214 214 "review_status": "<review_status>",
215 215 }
216 216 ...
217 217 ]
218 218 }
219 219 ...
220 220 ],
221 221 "error": null
222 222
223 223 """
224 224 repo = get_repo_or_error(repoid)
225 225 if not has_superadmin_permission(apiuser):
226 226 _perms = (
227 227 'repository.admin', 'repository.write', 'repository.read',)
228 228 validate_repo_permissions(apiuser, repoid, repo, _perms)
229 229
230 230 status = Optional.extract(status)
231 231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
232 232 data = [pr.get_api_data() for pr in pull_requests]
233 233 return data
234 234
235 235
236 236 @jsonrpc_method()
237 237 def merge_pull_request(
238 238 request, apiuser, pullrequestid, repoid=Optional(None),
239 239 userid=Optional(OAttr('apiuser'))):
240 240 """
241 241 Merge the pull request specified by `pullrequestid` into its target
242 242 repository.
243 243
244 244 :param apiuser: This is filled automatically from the |authtoken|.
245 245 :type apiuser: AuthUser
246 246 :param repoid: Optional, repository name or repository ID of the
247 247 target repository to which the |pr| is to be merged.
248 248 :type repoid: str or int
249 249 :param pullrequestid: ID of the pull request which shall be merged.
250 250 :type pullrequestid: int
251 251 :param userid: Merge the pull request as this user.
252 252 :type userid: Optional(str or int)
253 253
254 254 Example output:
255 255
256 256 .. code-block:: bash
257 257
258 258 "id": <id_given_in_input>,
259 259 "result": {
260 260 "executed": "<bool>",
261 261 "failure_reason": "<int>",
262 262 "merge_commit_id": "<merge_commit_id>",
263 263 "possible": "<bool>",
264 264 "merge_ref": {
265 265 "commit_id": "<commit_id>",
266 266 "type": "<type>",
267 267 "name": "<name>"
268 268 }
269 269 },
270 270 "error": null
271 271 """
272 272 pull_request = get_pull_request_or_error(pullrequestid)
273 273 if Optional.extract(repoid):
274 274 repo = get_repo_or_error(repoid)
275 275 else:
276 276 repo = pull_request.target_repo
277 277
278 278 if not isinstance(userid, Optional):
279 279 if (has_superadmin_permission(apiuser) or
280 280 HasRepoPermissionAnyApi('repository.admin')(
281 281 user=apiuser, repo_name=repo.repo_name)):
282 282 apiuser = get_user_or_error(userid)
283 283 else:
284 284 raise JSONRPCError('userid is not the same as your user')
285 285
286 286 check = MergeCheck.validate(
287 287 pull_request, user=apiuser, translator=request.translate)
288 288 merge_possible = not check.failed
289 289
290 290 if not merge_possible:
291 291 error_messages = []
292 292 for err_type, error_msg in check.errors:
293 293 error_msg = request.translate(error_msg)
294 294 error_messages.append(error_msg)
295 295
296 296 reasons = ','.join(error_messages)
297 297 raise JSONRPCError(
298 298 'merge not possible for following reasons: {}'.format(reasons))
299 299
300 300 target_repo = pull_request.target_repo
301 301 extras = vcs_operation_context(
302 302 request.environ, repo_name=target_repo.repo_name,
303 303 username=apiuser.username, action='push',
304 304 scm=target_repo.repo_type)
305 305 merge_response = PullRequestModel().merge_repo(
306 306 pull_request, apiuser, extras=extras)
307 307 if merge_response.executed:
308 308 PullRequestModel().close_pull_request(
309 309 pull_request.pull_request_id, apiuser)
310 310
311 311 Session().commit()
312 312
313 313 # In previous versions the merge response directly contained the merge
314 314 # commit id. It is now contained in the merge reference object. To be
315 315 # backwards compatible we have to extract it again.
316 316 merge_response = merge_response._asdict()
317 317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318 318
319 319 return merge_response
320 320
321 321
322 322 @jsonrpc_method()
323 323 def get_pull_request_comments(
324 324 request, apiuser, pullrequestid, repoid=Optional(None)):
325 325 """
326 326 Get all comments of pull request specified with the `pullrequestid`
327 327
328 328 :param apiuser: This is filled automatically from the |authtoken|.
329 329 :type apiuser: AuthUser
330 330 :param repoid: Optional repository name or repository ID.
331 331 :type repoid: str or int
332 332 :param pullrequestid: The pull request ID.
333 333 :type pullrequestid: int
334 334
335 335 Example output:
336 336
337 337 .. code-block:: bash
338 338
339 339 id : <id_given_in_input>
340 340 result : [
341 341 {
342 342 "comment_author": {
343 343 "active": true,
344 344 "full_name_or_username": "Tom Gore",
345 345 "username": "admin"
346 346 },
347 347 "comment_created_on": "2017-01-02T18:43:45.533",
348 348 "comment_f_path": null,
349 349 "comment_id": 25,
350 350 "comment_lineno": null,
351 351 "comment_status": {
352 352 "status": "under_review",
353 353 "status_lbl": "Under Review"
354 354 },
355 355 "comment_text": "Example text",
356 356 "comment_type": null,
357 357 "pull_request_version": null
358 358 }
359 359 ],
360 360 error : null
361 361 """
362 362
363 363 pull_request = get_pull_request_or_error(pullrequestid)
364 364 if Optional.extract(repoid):
365 365 repo = get_repo_or_error(repoid)
366 366 else:
367 367 repo = pull_request.target_repo
368 368
369 369 if not PullRequestModel().check_user_read(
370 370 pull_request, apiuser, api=True):
371 371 raise JSONRPCError('repository `%s` or pull request `%s` '
372 372 'does not exist' % (repoid, pullrequestid))
373 373
374 374 (pull_request_latest,
375 375 pull_request_at_ver,
376 376 pull_request_display_obj,
377 377 at_version) = PullRequestModel().get_pr_version(
378 378 pull_request.pull_request_id, version=None)
379 379
380 380 versions = pull_request_display_obj.versions()
381 381 ver_map = {
382 382 ver.pull_request_version_id: cnt
383 383 for cnt, ver in enumerate(versions, 1)
384 384 }
385 385
386 386 # GENERAL COMMENTS with versions #
387 387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
388 388 q = q.order_by(ChangesetComment.comment_id.asc())
389 389 general_comments = q.all()
390 390
391 391 # INLINE COMMENTS with versions #
392 392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
393 393 q = q.order_by(ChangesetComment.comment_id.asc())
394 394 inline_comments = q.all()
395 395
396 396 data = []
397 397 for comment in inline_comments + general_comments:
398 398 full_data = comment.get_api_data()
399 399 pr_version_id = None
400 400 if comment.pull_request_version_id:
401 401 pr_version_id = 'v{}'.format(
402 402 ver_map[comment.pull_request_version_id])
403 403
404 404 # sanitize some entries
405 405
406 406 full_data['pull_request_version'] = pr_version_id
407 407 full_data['comment_author'] = {
408 408 'username': full_data['comment_author'].username,
409 409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
410 410 'active': full_data['comment_author'].active,
411 411 }
412 412
413 413 if full_data['comment_status']:
414 414 full_data['comment_status'] = {
415 415 'status': full_data['comment_status'][0].status,
416 416 'status_lbl': full_data['comment_status'][0].status_lbl,
417 417 }
418 418 else:
419 419 full_data['comment_status'] = {}
420 420
421 421 data.append(full_data)
422 422 return data
423 423
424 424
425 425 @jsonrpc_method()
426 426 def comment_pull_request(
427 427 request, apiuser, pullrequestid, repoid=Optional(None),
428 428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
429 429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
430 430 resolves_comment_id=Optional(None),
431 431 userid=Optional(OAttr('apiuser'))):
432 432 """
433 433 Comment on the pull request specified with the `pullrequestid`,
434 434 in the |repo| specified by the `repoid`, and optionally change the
435 435 review status.
436 436
437 437 :param apiuser: This is filled automatically from the |authtoken|.
438 438 :type apiuser: AuthUser
439 439 :param repoid: Optional repository name or repository ID.
440 440 :type repoid: str or int
441 441 :param pullrequestid: The pull request ID.
442 442 :type pullrequestid: int
443 443 :param commit_id: Specify the commit_id for which to set a comment. If
444 444 given commit_id is different than latest in the PR status
445 445 change won't be performed.
446 446 :type commit_id: str
447 447 :param message: The text content of the comment.
448 448 :type message: str
449 449 :param status: (**Optional**) Set the approval status of the pull
450 450 request. One of: 'not_reviewed', 'approved', 'rejected',
451 451 'under_review'
452 452 :type status: str
453 453 :param comment_type: Comment type, one of: 'note', 'todo'
454 454 :type comment_type: Optional(str), default: 'note'
455 455 :param userid: Comment on the pull request as this user
456 456 :type userid: Optional(str or int)
457 457
458 458 Example output:
459 459
460 460 .. code-block:: bash
461 461
462 462 id : <id_given_in_input>
463 463 result : {
464 464 "pull_request_id": "<Integer>",
465 465 "comment_id": "<Integer>",
466 466 "status": {"given": <given_status>,
467 467 "was_changed": <bool status_was_actually_changed> },
468 468 },
469 469 error : null
470 470 """
471 471 pull_request = get_pull_request_or_error(pullrequestid)
472 472 if Optional.extract(repoid):
473 473 repo = get_repo_or_error(repoid)
474 474 else:
475 475 repo = pull_request.target_repo
476 476
477 477 if not isinstance(userid, Optional):
478 478 if (has_superadmin_permission(apiuser) or
479 479 HasRepoPermissionAnyApi('repository.admin')(
480 480 user=apiuser, repo_name=repo.repo_name)):
481 481 apiuser = get_user_or_error(userid)
482 482 else:
483 483 raise JSONRPCError('userid is not the same as your user')
484 484
485 485 if not PullRequestModel().check_user_read(
486 486 pull_request, apiuser, api=True):
487 487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
488 488 message = Optional.extract(message)
489 489 status = Optional.extract(status)
490 490 commit_id = Optional.extract(commit_id)
491 491 comment_type = Optional.extract(comment_type)
492 492 resolves_comment_id = Optional.extract(resolves_comment_id)
493 493
494 494 if not message and not status:
495 495 raise JSONRPCError(
496 496 'Both message and status parameters are missing. '
497 497 'At least one is required.')
498 498
499 499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
500 500 status is not None):
501 501 raise JSONRPCError('Unknown comment status: `%s`' % status)
502 502
503 503 if commit_id and commit_id not in pull_request.revisions:
504 504 raise JSONRPCError(
505 505 'Invalid commit_id `%s` for this pull request.' % commit_id)
506 506
507 507 allowed_to_change_status = PullRequestModel().check_user_change_status(
508 508 pull_request, apiuser)
509 509
510 510 # if commit_id is passed re-validated if user is allowed to change status
511 511 # based on latest commit_id from the PR
512 512 if commit_id:
513 513 commit_idx = pull_request.revisions.index(commit_id)
514 514 if commit_idx != 0:
515 515 allowed_to_change_status = False
516 516
517 517 if resolves_comment_id:
518 518 comment = ChangesetComment.get(resolves_comment_id)
519 519 if not comment:
520 520 raise JSONRPCError(
521 521 'Invalid resolves_comment_id `%s` for this pull request.'
522 522 % resolves_comment_id)
523 523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
524 524 raise JSONRPCError(
525 525 'Comment `%s` is wrong type for setting status to resolved.'
526 526 % resolves_comment_id)
527 527
528 528 text = message
529 529 status_label = ChangesetStatus.get_status_lbl(status)
530 530 if status and allowed_to_change_status:
531 531 st_message = ('Status change %(transition_icon)s %(status)s'
532 532 % {'transition_icon': '>', 'status': status_label})
533 533 text = message or st_message
534 534
535 535 rc_config = SettingsModel().get_all_settings()
536 536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
537 537
538 538 status_change = status and allowed_to_change_status
539 539 comment = CommentsModel().create(
540 540 text=text,
541 541 repo=pull_request.target_repo.repo_id,
542 542 user=apiuser.user_id,
543 543 pull_request=pull_request.pull_request_id,
544 544 f_path=None,
545 545 line_no=None,
546 546 status_change=(status_label if status_change else None),
547 547 status_change_type=(status if status_change else None),
548 548 closing_pr=False,
549 549 renderer=renderer,
550 550 comment_type=comment_type,
551 551 resolves_comment_id=resolves_comment_id,
552 552 auth_user=apiuser
553 553 )
554 554
555 555 if allowed_to_change_status and status:
556 556 ChangesetStatusModel().set_status(
557 557 pull_request.target_repo.repo_id,
558 558 status,
559 559 apiuser.user_id,
560 560 comment,
561 561 pull_request=pull_request.pull_request_id
562 562 )
563 563 Session().flush()
564 564
565 565 Session().commit()
566 566 data = {
567 567 'pull_request_id': pull_request.pull_request_id,
568 568 'comment_id': comment.comment_id if comment else None,
569 569 'status': {'given': status, 'was_changed': status_change},
570 570 }
571 571 return data
572 572
573 573
574 574 @jsonrpc_method()
575 575 def create_pull_request(
576 576 request, apiuser, source_repo, target_repo, source_ref, target_ref,
577 577 title=Optional(''), description=Optional(''), reviewers=Optional(None)):
578 578 """
579 579 Creates a new pull request.
580 580
581 581 Accepts refs in the following formats:
582 582
583 583 * branch:<branch_name>:<sha>
584 584 * branch:<branch_name>
585 585 * bookmark:<bookmark_name>:<sha> (Mercurial only)
586 586 * bookmark:<bookmark_name> (Mercurial only)
587 587
588 588 :param apiuser: This is filled automatically from the |authtoken|.
589 589 :type apiuser: AuthUser
590 590 :param source_repo: Set the source repository name.
591 591 :type source_repo: str
592 592 :param target_repo: Set the target repository name.
593 593 :type target_repo: str
594 594 :param source_ref: Set the source ref name.
595 595 :type source_ref: str
596 596 :param target_ref: Set the target ref name.
597 597 :type target_ref: str
598 598 :param title: Optionally Set the pull request title, it's generated otherwise
599 599 :type title: str
600 600 :param description: Set the pull request description.
601 601 :type description: Optional(str)
602 602 :param reviewers: Set the new pull request reviewers list.
603 603 Reviewer defined by review rules will be added automatically to the
604 604 defined list.
605 605 :type reviewers: Optional(list)
606 606 Accepts username strings or objects of the format:
607 607
608 608 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
609 609 """
610 610
611 611 source_db_repo = get_repo_or_error(source_repo)
612 612 target_db_repo = get_repo_or_error(target_repo)
613 613 if not has_superadmin_permission(apiuser):
614 614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
615 615 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
616 616
617 617 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
618 618 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
619 619
620 620 source_scm = source_db_repo.scm_instance()
621 621 target_scm = target_db_repo.scm_instance()
622 622
623 623 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
624 624 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
625 625
626 626 ancestor = source_scm.get_common_ancestor(
627 627 source_commit.raw_id, target_commit.raw_id, target_scm)
628 628 if not ancestor:
629 629 raise JSONRPCError('no common ancestor found')
630 630
631 631 # recalculate target ref based on ancestor
632 632 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
633 633 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
634 634
635 635 commit_ranges = target_scm.compare(
636 636 target_commit.raw_id, source_commit.raw_id, source_scm,
637 637 merge=True, pre_load=[])
638 638
639 639 if not commit_ranges:
640 640 raise JSONRPCError('no commits found')
641 641
642 642 reviewer_objects = Optional.extract(reviewers) or []
643 643
644 # serialize and validate passed in given reviewers
644 645 if reviewer_objects:
645 646 schema = ReviewerListSchema()
646 647 try:
647 648 reviewer_objects = schema.deserialize(reviewer_objects)
648 649 except Invalid as err:
649 650 raise JSONRPCValidationError(colander_exc=err)
650 651
651 652 # validate users
652 653 for reviewer_object in reviewer_objects:
653 654 user = get_user_or_error(reviewer_object['username'])
654 655 reviewer_object['user_id'] = user.user_id
655 656
656 get_default_reviewers_data, get_validated_reviewers = \
657 get_default_reviewers_data, validate_default_reviewers = \
657 658 PullRequestModel().get_reviewer_functions()
658 659
660 # recalculate reviewers logic, to make sure we can validate this
659 661 reviewer_rules = get_default_reviewers_data(
660 662 apiuser.get_instance(), source_db_repo,
661 663 source_commit, target_db_repo, target_commit)
662 664
663 # specified rules are later re-validated, thus we can assume users will
664 # eventually provide those that meet the reviewer criteria.
665 if not reviewer_objects:
666 reviewer_objects = reviewer_rules['reviewers']
665 # now MERGE our given with the calculated
666 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
667 667
668 668 try:
669 reviewers = get_validated_reviewers(
669 reviewers = validate_default_reviewers(
670 670 reviewer_objects, reviewer_rules)
671 671 except ValueError as e:
672 672 raise JSONRPCError('Reviewers Validation: {}'.format(e))
673 673
674 674 title = Optional.extract(title)
675 675 if not title:
676 676 title_source_ref = source_ref.split(':', 2)[1]
677 677 title = PullRequestModel().generate_pullrequest_title(
678 678 source=source_repo,
679 679 source_ref=title_source_ref,
680 680 target=target_repo
681 681 )
682 682 description = Optional.extract(description)
683 683
684 684 pull_request = PullRequestModel().create(
685 685 created_by=apiuser.user_id,
686 686 source_repo=source_repo,
687 687 source_ref=full_source_ref,
688 688 target_repo=target_repo,
689 689 target_ref=full_target_ref,
690 690 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
691 691 reviewers=reviewers,
692 692 title=title,
693 693 description=description,
694 694 reviewer_data=reviewer_rules,
695 695 auth_user=apiuser
696 696 )
697 697
698 698 Session().commit()
699 699 data = {
700 700 'msg': 'Created new pull request `{}`'.format(title),
701 701 'pull_request_id': pull_request.pull_request_id,
702 702 }
703 703 return data
704 704
705 705
706 706 @jsonrpc_method()
707 707 def update_pull_request(
708 708 request, apiuser, pullrequestid, repoid=Optional(None),
709 709 title=Optional(''), description=Optional(''), reviewers=Optional(None),
710 710 update_commits=Optional(None)):
711 711 """
712 712 Updates a pull request.
713 713
714 714 :param apiuser: This is filled automatically from the |authtoken|.
715 715 :type apiuser: AuthUser
716 716 :param repoid: Optional repository name or repository ID.
717 717 :type repoid: str or int
718 718 :param pullrequestid: The pull request ID.
719 719 :type pullrequestid: int
720 720 :param title: Set the pull request title.
721 721 :type title: str
722 722 :param description: Update pull request description.
723 723 :type description: Optional(str)
724 724 :param reviewers: Update pull request reviewers list with new value.
725 725 :type reviewers: Optional(list)
726 726 Accepts username strings or objects of the format:
727 727
728 728 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
729 729
730 730 :param update_commits: Trigger update of commits for this pull request
731 731 :type: update_commits: Optional(bool)
732 732
733 733 Example output:
734 734
735 735 .. code-block:: bash
736 736
737 737 id : <id_given_in_input>
738 738 result : {
739 739 "msg": "Updated pull request `63`",
740 740 "pull_request": <pull_request_object>,
741 741 "updated_reviewers": {
742 742 "added": [
743 743 "username"
744 744 ],
745 745 "removed": []
746 746 },
747 747 "updated_commits": {
748 748 "added": [
749 749 "<sha1_hash>"
750 750 ],
751 751 "common": [
752 752 "<sha1_hash>",
753 753 "<sha1_hash>",
754 754 ],
755 755 "removed": []
756 756 }
757 757 }
758 758 error : null
759 759 """
760 760
761 761 pull_request = get_pull_request_or_error(pullrequestid)
762 762 if Optional.extract(repoid):
763 763 repo = get_repo_or_error(repoid)
764 764 else:
765 765 repo = pull_request.target_repo
766 766
767 767 if not PullRequestModel().check_user_update(
768 768 pull_request, apiuser, api=True):
769 769 raise JSONRPCError(
770 770 'pull request `%s` update failed, no permission to update.' % (
771 771 pullrequestid,))
772 772 if pull_request.is_closed():
773 773 raise JSONRPCError(
774 774 'pull request `%s` update failed, pull request is closed' % (
775 775 pullrequestid,))
776 776
777 777 reviewer_objects = Optional.extract(reviewers) or []
778 778
779 779 if reviewer_objects:
780 780 schema = ReviewerListSchema()
781 781 try:
782 782 reviewer_objects = schema.deserialize(reviewer_objects)
783 783 except Invalid as err:
784 784 raise JSONRPCValidationError(colander_exc=err)
785 785
786 786 # validate users
787 787 for reviewer_object in reviewer_objects:
788 788 user = get_user_or_error(reviewer_object['username'])
789 789 reviewer_object['user_id'] = user.user_id
790 790
791 791 get_default_reviewers_data, get_validated_reviewers = \
792 792 PullRequestModel().get_reviewer_functions()
793 793
794 794 # re-use stored rules
795 795 reviewer_rules = pull_request.reviewer_data
796 796 try:
797 797 reviewers = get_validated_reviewers(
798 798 reviewer_objects, reviewer_rules)
799 799 except ValueError as e:
800 800 raise JSONRPCError('Reviewers Validation: {}'.format(e))
801 801 else:
802 802 reviewers = []
803 803
804 804 title = Optional.extract(title)
805 805 description = Optional.extract(description)
806 806 if title or description:
807 807 PullRequestModel().edit(
808 808 pull_request, title or pull_request.title,
809 809 description or pull_request.description, apiuser)
810 810 Session().commit()
811 811
812 812 commit_changes = {"added": [], "common": [], "removed": []}
813 813 if str2bool(Optional.extract(update_commits)):
814 814 if PullRequestModel().has_valid_update_type(pull_request):
815 815 update_response = PullRequestModel().update_commits(
816 816 pull_request)
817 817 commit_changes = update_response.changes or commit_changes
818 818 Session().commit()
819 819
820 820 reviewers_changes = {"added": [], "removed": []}
821 821 if reviewers:
822 822 added_reviewers, removed_reviewers = \
823 823 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
824 824
825 825 reviewers_changes['added'] = sorted(
826 826 [get_user_or_error(n).username for n in added_reviewers])
827 827 reviewers_changes['removed'] = sorted(
828 828 [get_user_or_error(n).username for n in removed_reviewers])
829 829 Session().commit()
830 830
831 831 data = {
832 832 'msg': 'Updated pull request `{}`'.format(
833 833 pull_request.pull_request_id),
834 834 'pull_request': pull_request.get_api_data(),
835 835 'updated_commits': commit_changes,
836 836 'updated_reviewers': reviewers_changes
837 837 }
838 838
839 839 return data
840 840
841 841
842 842 @jsonrpc_method()
843 843 def close_pull_request(
844 844 request, apiuser, pullrequestid, repoid=Optional(None),
845 845 userid=Optional(OAttr('apiuser')), message=Optional('')):
846 846 """
847 847 Close the pull request specified by `pullrequestid`.
848 848
849 849 :param apiuser: This is filled automatically from the |authtoken|.
850 850 :type apiuser: AuthUser
851 851 :param repoid: Repository name or repository ID to which the pull
852 852 request belongs.
853 853 :type repoid: str or int
854 854 :param pullrequestid: ID of the pull request to be closed.
855 855 :type pullrequestid: int
856 856 :param userid: Close the pull request as this user.
857 857 :type userid: Optional(str or int)
858 858 :param message: Optional message to close the Pull Request with. If not
859 859 specified it will be generated automatically.
860 860 :type message: Optional(str)
861 861
862 862 Example output:
863 863
864 864 .. code-block:: bash
865 865
866 866 "id": <id_given_in_input>,
867 867 "result": {
868 868 "pull_request_id": "<int>",
869 869 "close_status": "<str:status_lbl>,
870 870 "closed": "<bool>"
871 871 },
872 872 "error": null
873 873
874 874 """
875 875 _ = request.translate
876 876
877 877 pull_request = get_pull_request_or_error(pullrequestid)
878 878 if Optional.extract(repoid):
879 879 repo = get_repo_or_error(repoid)
880 880 else:
881 881 repo = pull_request.target_repo
882 882
883 883 if not isinstance(userid, Optional):
884 884 if (has_superadmin_permission(apiuser) or
885 885 HasRepoPermissionAnyApi('repository.admin')(
886 886 user=apiuser, repo_name=repo.repo_name)):
887 887 apiuser = get_user_or_error(userid)
888 888 else:
889 889 raise JSONRPCError('userid is not the same as your user')
890 890
891 891 if pull_request.is_closed():
892 892 raise JSONRPCError(
893 893 'pull request `%s` is already closed' % (pullrequestid,))
894 894
895 895 # only owner or admin or person with write permissions
896 896 allowed_to_close = PullRequestModel().check_user_update(
897 897 pull_request, apiuser, api=True)
898 898
899 899 if not allowed_to_close:
900 900 raise JSONRPCError(
901 901 'pull request `%s` close failed, no permission to close.' % (
902 902 pullrequestid,))
903 903
904 904 # message we're using to close the PR, else it's automatically generated
905 905 message = Optional.extract(message)
906 906
907 907 # finally close the PR, with proper message comment
908 908 comment, status = PullRequestModel().close_pull_request_with_comment(
909 909 pull_request, apiuser, repo, message=message)
910 910 status_lbl = ChangesetStatus.get_status_lbl(status)
911 911
912 912 Session().commit()
913 913
914 914 data = {
915 915 'pull_request_id': pull_request.pull_request_id,
916 916 'close_status': status_lbl,
917 917 'closed': True,
918 918 }
919 919 return data
@@ -1,1315 +1,1316 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 45 RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 49 ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 111 pr.pull_request_id, pr.target_repo.repo_name),
112 112 'name_raw': pr.pull_request_id,
113 113 'status': _render('pullrequest_status',
114 114 pr.calculated_review_status()),
115 115 'title': _render(
116 116 'pullrequest_title', pr.title, pr.description),
117 117 'description': h.escape(pr.description),
118 118 'updated_on': _render('pullrequest_updated_on',
119 119 h.datetime_to_time(pr.updated_on)),
120 120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 121 'created_on': _render('pullrequest_updated_on',
122 122 h.datetime_to_time(pr.created_on)),
123 123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 124 'author': _render('pullrequest_author',
125 125 pr.author.full_contact, ),
126 126 'author_raw': pr.author.full_name,
127 127 'comments': _render('pullrequest_comments', len(comments)),
128 128 'comments_raw': len(comments),
129 129 'closed': pr.is_closed(),
130 130 })
131 131
132 132 data = ({
133 133 'draw': draw,
134 134 'data': data,
135 135 'recordsTotal': pull_requests_total_count,
136 136 'recordsFiltered': pull_requests_total_count,
137 137 })
138 138 return data
139 139
140 140 @LoginRequired()
141 141 @HasRepoPermissionAnyDecorator(
142 142 'repository.read', 'repository.write', 'repository.admin')
143 143 @view_config(
144 144 route_name='pullrequest_show_all', request_method='GET',
145 145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 146 def pull_request_list(self):
147 147 c = self.load_default_context()
148 148
149 149 req_get = self.request.GET
150 150 c.source = str2bool(req_get.get('source'))
151 151 c.closed = str2bool(req_get.get('closed'))
152 152 c.my = str2bool(req_get.get('my'))
153 153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 155
156 156 c.active = 'open'
157 157 if c.my:
158 158 c.active = 'my'
159 159 if c.closed:
160 160 c.active = 'closed'
161 161 if c.awaiting_review and not c.source:
162 162 c.active = 'awaiting'
163 163 if c.source and not c.awaiting_review:
164 164 c.active = 'source'
165 165 if c.awaiting_my_review:
166 166 c.active = 'awaiting_my'
167 167
168 168 return self._get_template_context(c)
169 169
170 170 @LoginRequired()
171 171 @HasRepoPermissionAnyDecorator(
172 172 'repository.read', 'repository.write', 'repository.admin')
173 173 @view_config(
174 174 route_name='pullrequest_show_all_data', request_method='GET',
175 175 renderer='json_ext', xhr=True)
176 176 def pull_request_list_data(self):
177 177 self.load_default_context()
178 178
179 179 # additional filters
180 180 req_get = self.request.GET
181 181 source = str2bool(req_get.get('source'))
182 182 closed = str2bool(req_get.get('closed'))
183 183 my = str2bool(req_get.get('my'))
184 184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 186
187 187 filter_type = 'awaiting_review' if awaiting_review \
188 188 else 'awaiting_my_review' if awaiting_my_review \
189 189 else None
190 190
191 191 opened_by = None
192 192 if my:
193 193 opened_by = [self._rhodecode_user.user_id]
194 194
195 195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 196 if closed:
197 197 statuses = [PullRequest.STATUS_CLOSED]
198 198
199 199 data = self._get_pull_requests_list(
200 200 repo_name=self.db_repo_name, source=source,
201 201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 202
203 203 return data
204 204
205 205 def _is_diff_cache_enabled(self, target_repo):
206 206 caching_enabled = self._get_general_setting(
207 207 target_repo, 'rhodecode_diff_cache')
208 208 log.debug('Diff caching enabled: %s', caching_enabled)
209 209 return caching_enabled
210 210
211 211 def _get_diffset(self, source_repo_name, source_repo,
212 212 source_ref_id, target_ref_id,
213 213 target_commit, source_commit, diff_limit, file_limit,
214 214 fulldiff):
215 215
216 216 vcs_diff = PullRequestModel().get_diff(
217 217 source_repo, source_ref_id, target_ref_id)
218 218
219 219 diff_processor = diffs.DiffProcessor(
220 220 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 221 file_limit=file_limit, show_full_diff=fulldiff)
222 222
223 223 _parsed = diff_processor.prepare()
224 224
225 225 diffset = codeblocks.DiffSet(
226 226 repo_name=self.db_repo_name,
227 227 source_repo_name=source_repo_name,
228 228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 230 )
231 231 diffset = self.path_filter.render_patchset_filtered(
232 232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 233
234 234 return diffset
235 235
236 236 @LoginRequired()
237 237 @HasRepoPermissionAnyDecorator(
238 238 'repository.read', 'repository.write', 'repository.admin')
239 239 @view_config(
240 240 route_name='pullrequest_show', request_method='GET',
241 241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
242 242 def pull_request_show(self):
243 243 pull_request_id = self.request.matchdict['pull_request_id']
244 244
245 245 c = self.load_default_context()
246 246
247 247 version = self.request.GET.get('version')
248 248 from_version = self.request.GET.get('from_version') or version
249 249 merge_checks = self.request.GET.get('merge_checks')
250 250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
251 251 force_refresh = str2bool(self.request.GET.get('force_refresh'))
252 252
253 253 (pull_request_latest,
254 254 pull_request_at_ver,
255 255 pull_request_display_obj,
256 256 at_version) = PullRequestModel().get_pr_version(
257 257 pull_request_id, version=version)
258 258 pr_closed = pull_request_latest.is_closed()
259 259
260 260 if pr_closed and (version or from_version):
261 261 # not allow to browse versions
262 262 raise HTTPFound(h.route_path(
263 263 'pullrequest_show', repo_name=self.db_repo_name,
264 264 pull_request_id=pull_request_id))
265 265
266 266 versions = pull_request_display_obj.versions()
267 267
268 268 c.at_version = at_version
269 269 c.at_version_num = (at_version
270 270 if at_version and at_version != 'latest'
271 271 else None)
272 272 c.at_version_pos = ChangesetComment.get_index_from_version(
273 273 c.at_version_num, versions)
274 274
275 275 (prev_pull_request_latest,
276 276 prev_pull_request_at_ver,
277 277 prev_pull_request_display_obj,
278 278 prev_at_version) = PullRequestModel().get_pr_version(
279 279 pull_request_id, version=from_version)
280 280
281 281 c.from_version = prev_at_version
282 282 c.from_version_num = (prev_at_version
283 283 if prev_at_version and prev_at_version != 'latest'
284 284 else None)
285 285 c.from_version_pos = ChangesetComment.get_index_from_version(
286 286 c.from_version_num, versions)
287 287
288 288 # define if we're in COMPARE mode or VIEW at version mode
289 289 compare = at_version != prev_at_version
290 290
291 291 # pull_requests repo_name we opened it against
292 292 # ie. target_repo must match
293 293 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
294 294 raise HTTPNotFound()
295 295
296 296 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
297 297 pull_request_at_ver)
298 298
299 299 c.pull_request = pull_request_display_obj
300 300 c.pull_request_latest = pull_request_latest
301 301
302 302 if compare or (at_version and not at_version == 'latest'):
303 303 c.allowed_to_change_status = False
304 304 c.allowed_to_update = False
305 305 c.allowed_to_merge = False
306 306 c.allowed_to_delete = False
307 307 c.allowed_to_comment = False
308 308 c.allowed_to_close = False
309 309 else:
310 310 can_change_status = PullRequestModel().check_user_change_status(
311 311 pull_request_at_ver, self._rhodecode_user)
312 312 c.allowed_to_change_status = can_change_status and not pr_closed
313 313
314 314 c.allowed_to_update = PullRequestModel().check_user_update(
315 315 pull_request_latest, self._rhodecode_user) and not pr_closed
316 316 c.allowed_to_merge = PullRequestModel().check_user_merge(
317 317 pull_request_latest, self._rhodecode_user) and not pr_closed
318 318 c.allowed_to_delete = PullRequestModel().check_user_delete(
319 319 pull_request_latest, self._rhodecode_user) and not pr_closed
320 320 c.allowed_to_comment = not pr_closed
321 321 c.allowed_to_close = c.allowed_to_merge and not pr_closed
322 322
323 323 c.forbid_adding_reviewers = False
324 324 c.forbid_author_to_review = False
325 325 c.forbid_commit_author_to_review = False
326 326
327 327 if pull_request_latest.reviewer_data and \
328 328 'rules' in pull_request_latest.reviewer_data:
329 329 rules = pull_request_latest.reviewer_data['rules'] or {}
330 330 try:
331 331 c.forbid_adding_reviewers = rules.get(
332 332 'forbid_adding_reviewers')
333 333 c.forbid_author_to_review = rules.get(
334 334 'forbid_author_to_review')
335 335 c.forbid_commit_author_to_review = rules.get(
336 336 'forbid_commit_author_to_review')
337 337 except Exception:
338 338 pass
339 339
340 340 # check merge capabilities
341 341 _merge_check = MergeCheck.validate(
342 342 pull_request_latest, user=self._rhodecode_user,
343 343 translator=self.request.translate,
344 344 force_shadow_repo_refresh=force_refresh)
345 345 c.pr_merge_errors = _merge_check.error_details
346 346 c.pr_merge_possible = not _merge_check.failed
347 347 c.pr_merge_message = _merge_check.merge_msg
348 348
349 349 c.pr_merge_info = MergeCheck.get_merge_conditions(
350 350 pull_request_latest, translator=self.request.translate)
351 351
352 352 c.pull_request_review_status = _merge_check.review_status
353 353 if merge_checks:
354 354 self.request.override_renderer = \
355 355 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
356 356 return self._get_template_context(c)
357 357
358 358 comments_model = CommentsModel()
359 359
360 360 # reviewers and statuses
361 361 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
362 362 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
363 363
364 364 # GENERAL COMMENTS with versions #
365 365 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
366 366 q = q.order_by(ChangesetComment.comment_id.asc())
367 367 general_comments = q
368 368
369 369 # pick comments we want to render at current version
370 370 c.comment_versions = comments_model.aggregate_comments(
371 371 general_comments, versions, c.at_version_num)
372 372 c.comments = c.comment_versions[c.at_version_num]['until']
373 373
374 374 # INLINE COMMENTS with versions #
375 375 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
376 376 q = q.order_by(ChangesetComment.comment_id.asc())
377 377 inline_comments = q
378 378
379 379 c.inline_versions = comments_model.aggregate_comments(
380 380 inline_comments, versions, c.at_version_num, inline=True)
381 381
382 382 # inject latest version
383 383 latest_ver = PullRequest.get_pr_display_object(
384 384 pull_request_latest, pull_request_latest)
385 385
386 386 c.versions = versions + [latest_ver]
387 387
388 388 # if we use version, then do not show later comments
389 389 # than current version
390 390 display_inline_comments = collections.defaultdict(
391 391 lambda: collections.defaultdict(list))
392 392 for co in inline_comments:
393 393 if c.at_version_num:
394 394 # pick comments that are at least UPTO given version, so we
395 395 # don't render comments for higher version
396 396 should_render = co.pull_request_version_id and \
397 397 co.pull_request_version_id <= c.at_version_num
398 398 else:
399 399 # showing all, for 'latest'
400 400 should_render = True
401 401
402 402 if should_render:
403 403 display_inline_comments[co.f_path][co.line_no].append(co)
404 404
405 405 # load diff data into template context, if we use compare mode then
406 406 # diff is calculated based on changes between versions of PR
407 407
408 408 source_repo = pull_request_at_ver.source_repo
409 409 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
410 410
411 411 target_repo = pull_request_at_ver.target_repo
412 412 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
413 413
414 414 if compare:
415 415 # in compare switch the diff base to latest commit from prev version
416 416 target_ref_id = prev_pull_request_display_obj.revisions[0]
417 417
418 418 # despite opening commits for bookmarks/branches/tags, we always
419 419 # convert this to rev to prevent changes after bookmark or branch change
420 420 c.source_ref_type = 'rev'
421 421 c.source_ref = source_ref_id
422 422
423 423 c.target_ref_type = 'rev'
424 424 c.target_ref = target_ref_id
425 425
426 426 c.source_repo = source_repo
427 427 c.target_repo = target_repo
428 428
429 429 c.commit_ranges = []
430 430 source_commit = EmptyCommit()
431 431 target_commit = EmptyCommit()
432 432 c.missing_requirements = False
433 433
434 434 source_scm = source_repo.scm_instance()
435 435 target_scm = target_repo.scm_instance()
436 436
437 437 shadow_scm = None
438 438 try:
439 439 shadow_scm = pull_request_latest.get_shadow_repo()
440 440 except Exception:
441 441 log.debug('Failed to get shadow repo', exc_info=True)
442 442 # try first the existing source_repo, and then shadow
443 443 # repo if we can obtain one
444 444 commits_source_repo = source_scm or shadow_scm
445 445
446 446 c.commits_source_repo = commits_source_repo
447 447 c.ancestor = None # set it to None, to hide it from PR view
448 448
449 449 # empty version means latest, so we keep this to prevent
450 450 # double caching
451 451 version_normalized = version or 'latest'
452 452 from_version_normalized = from_version or 'latest'
453 453
454 454 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
455 455 target_repo)
456 456 cache_file_path = diff_cache_exist(
457 457 cache_path, 'pull_request', pull_request_id, version_normalized,
458 458 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
459 459
460 460 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
461 461 force_recache = str2bool(self.request.GET.get('force_recache'))
462 462
463 463 cached_diff = None
464 464 if caching_enabled:
465 465 cached_diff = load_cached_diff(cache_file_path)
466 466
467 467 has_proper_commit_cache = (
468 468 cached_diff and cached_diff.get('commits')
469 469 and len(cached_diff.get('commits', [])) == 5
470 470 and cached_diff.get('commits')[0]
471 471 and cached_diff.get('commits')[3])
472 472 if not force_recache and has_proper_commit_cache:
473 473 diff_commit_cache = \
474 474 (ancestor_commit, commit_cache, missing_requirements,
475 475 source_commit, target_commit) = cached_diff['commits']
476 476 else:
477 477 diff_commit_cache = \
478 478 (ancestor_commit, commit_cache, missing_requirements,
479 479 source_commit, target_commit) = self.get_commits(
480 480 commits_source_repo,
481 481 pull_request_at_ver,
482 482 source_commit,
483 483 source_ref_id,
484 484 source_scm,
485 485 target_commit,
486 486 target_ref_id,
487 487 target_scm)
488 488
489 489 # register our commit range
490 490 for comm in commit_cache.values():
491 491 c.commit_ranges.append(comm)
492 492
493 493 c.missing_requirements = missing_requirements
494 494 c.ancestor_commit = ancestor_commit
495 495 c.statuses = source_repo.statuses(
496 496 [x.raw_id for x in c.commit_ranges])
497 497
498 498 # auto collapse if we have more than limit
499 499 collapse_limit = diffs.DiffProcessor._collapse_commits_over
500 500 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
501 501 c.compare_mode = compare
502 502
503 503 # diff_limit is the old behavior, will cut off the whole diff
504 504 # if the limit is applied otherwise will just hide the
505 505 # big files from the front-end
506 506 diff_limit = c.visual.cut_off_limit_diff
507 507 file_limit = c.visual.cut_off_limit_file
508 508
509 509 c.missing_commits = False
510 510 if (c.missing_requirements
511 511 or isinstance(source_commit, EmptyCommit)
512 512 or source_commit == target_commit):
513 513
514 514 c.missing_commits = True
515 515 else:
516 516 c.inline_comments = display_inline_comments
517 517
518 518 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
519 519 if not force_recache and has_proper_diff_cache:
520 520 c.diffset = cached_diff['diff']
521 521 (ancestor_commit, commit_cache, missing_requirements,
522 522 source_commit, target_commit) = cached_diff['commits']
523 523 else:
524 524 c.diffset = self._get_diffset(
525 525 c.source_repo.repo_name, commits_source_repo,
526 526 source_ref_id, target_ref_id,
527 527 target_commit, source_commit,
528 528 diff_limit, file_limit, c.fulldiff)
529 529
530 530 # save cached diff
531 531 if caching_enabled:
532 532 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
533 533
534 534 c.limited_diff = c.diffset.limited_diff
535 535
536 536 # calculate removed files that are bound to comments
537 537 comment_deleted_files = [
538 538 fname for fname in display_inline_comments
539 539 if fname not in c.diffset.file_stats]
540 540
541 541 c.deleted_files_comments = collections.defaultdict(dict)
542 542 for fname, per_line_comments in display_inline_comments.items():
543 543 if fname in comment_deleted_files:
544 544 c.deleted_files_comments[fname]['stats'] = 0
545 545 c.deleted_files_comments[fname]['comments'] = list()
546 546 for lno, comments in per_line_comments.items():
547 547 c.deleted_files_comments[fname]['comments'].extend(
548 548 comments)
549 549
550 550 # this is a hack to properly display links, when creating PR, the
551 551 # compare view and others uses different notation, and
552 552 # compare_commits.mako renders links based on the target_repo.
553 553 # We need to swap that here to generate it properly on the html side
554 554 c.target_repo = c.source_repo
555 555
556 556 c.commit_statuses = ChangesetStatus.STATUSES
557 557
558 558 c.show_version_changes = not pr_closed
559 559 if c.show_version_changes:
560 560 cur_obj = pull_request_at_ver
561 561 prev_obj = prev_pull_request_at_ver
562 562
563 563 old_commit_ids = prev_obj.revisions
564 564 new_commit_ids = cur_obj.revisions
565 565 commit_changes = PullRequestModel()._calculate_commit_id_changes(
566 566 old_commit_ids, new_commit_ids)
567 567 c.commit_changes_summary = commit_changes
568 568
569 569 # calculate the diff for commits between versions
570 570 c.commit_changes = []
571 571 mark = lambda cs, fw: list(
572 572 h.itertools.izip_longest([], cs, fillvalue=fw))
573 573 for c_type, raw_id in mark(commit_changes.added, 'a') \
574 574 + mark(commit_changes.removed, 'r') \
575 575 + mark(commit_changes.common, 'c'):
576 576
577 577 if raw_id in commit_cache:
578 578 commit = commit_cache[raw_id]
579 579 else:
580 580 try:
581 581 commit = commits_source_repo.get_commit(raw_id)
582 582 except CommitDoesNotExistError:
583 583 # in case we fail extracting still use "dummy" commit
584 584 # for display in commit diff
585 585 commit = h.AttributeDict(
586 586 {'raw_id': raw_id,
587 587 'message': 'EMPTY or MISSING COMMIT'})
588 588 c.commit_changes.append([c_type, commit])
589 589
590 590 # current user review statuses for each version
591 591 c.review_versions = {}
592 592 if self._rhodecode_user.user_id in allowed_reviewers:
593 593 for co in general_comments:
594 594 if co.author.user_id == self._rhodecode_user.user_id:
595 595 status = co.status_change
596 596 if status:
597 597 _ver_pr = status[0].comment.pull_request_version_id
598 598 c.review_versions[_ver_pr] = status[0]
599 599
600 600 return self._get_template_context(c)
601 601
602 602 def get_commits(
603 603 self, commits_source_repo, pull_request_at_ver, source_commit,
604 604 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
605 605 commit_cache = collections.OrderedDict()
606 606 missing_requirements = False
607 607 try:
608 608 pre_load = ["author", "branch", "date", "message"]
609 609 show_revs = pull_request_at_ver.revisions
610 610 for rev in show_revs:
611 611 comm = commits_source_repo.get_commit(
612 612 commit_id=rev, pre_load=pre_load)
613 613 commit_cache[comm.raw_id] = comm
614 614
615 615 # Order here matters, we first need to get target, and then
616 616 # the source
617 617 target_commit = commits_source_repo.get_commit(
618 618 commit_id=safe_str(target_ref_id))
619 619
620 620 source_commit = commits_source_repo.get_commit(
621 621 commit_id=safe_str(source_ref_id))
622 622 except CommitDoesNotExistError:
623 623 log.warning(
624 624 'Failed to get commit from `{}` repo'.format(
625 625 commits_source_repo), exc_info=True)
626 626 except RepositoryRequirementError:
627 627 log.warning(
628 628 'Failed to get all required data from repo', exc_info=True)
629 629 missing_requirements = True
630 630 ancestor_commit = None
631 631 try:
632 632 ancestor_id = source_scm.get_common_ancestor(
633 633 source_commit.raw_id, target_commit.raw_id, target_scm)
634 634 ancestor_commit = source_scm.get_commit(ancestor_id)
635 635 except Exception:
636 636 ancestor_commit = None
637 637 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
638 638
639 639 def assure_not_empty_repo(self):
640 640 _ = self.request.translate
641 641
642 642 try:
643 643 self.db_repo.scm_instance().get_commit()
644 644 except EmptyRepositoryError:
645 645 h.flash(h.literal(_('There are no commits yet')),
646 646 category='warning')
647 647 raise HTTPFound(
648 648 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
649 649
650 650 @LoginRequired()
651 651 @NotAnonymous()
652 652 @HasRepoPermissionAnyDecorator(
653 653 'repository.read', 'repository.write', 'repository.admin')
654 654 @view_config(
655 655 route_name='pullrequest_new', request_method='GET',
656 656 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
657 657 def pull_request_new(self):
658 658 _ = self.request.translate
659 659 c = self.load_default_context()
660 660
661 661 self.assure_not_empty_repo()
662 662 source_repo = self.db_repo
663 663
664 664 commit_id = self.request.GET.get('commit')
665 665 branch_ref = self.request.GET.get('branch')
666 666 bookmark_ref = self.request.GET.get('bookmark')
667 667
668 668 try:
669 669 source_repo_data = PullRequestModel().generate_repo_data(
670 670 source_repo, commit_id=commit_id,
671 671 branch=branch_ref, bookmark=bookmark_ref,
672 672 translator=self.request.translate)
673 673 except CommitDoesNotExistError as e:
674 674 log.exception(e)
675 675 h.flash(_('Commit does not exist'), 'error')
676 676 raise HTTPFound(
677 677 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
678 678
679 679 default_target_repo = source_repo
680 680
681 681 if source_repo.parent:
682 682 parent_vcs_obj = source_repo.parent.scm_instance()
683 683 if parent_vcs_obj and not parent_vcs_obj.is_empty():
684 684 # change default if we have a parent repo
685 685 default_target_repo = source_repo.parent
686 686
687 687 target_repo_data = PullRequestModel().generate_repo_data(
688 688 default_target_repo, translator=self.request.translate)
689 689
690 690 selected_source_ref = source_repo_data['refs']['selected_ref']
691 691 title_source_ref = ''
692 692 if selected_source_ref:
693 693 title_source_ref = selected_source_ref.split(':', 2)[1]
694 694 c.default_title = PullRequestModel().generate_pullrequest_title(
695 695 source=source_repo.repo_name,
696 696 source_ref=title_source_ref,
697 697 target=default_target_repo.repo_name
698 698 )
699 699
700 700 c.default_repo_data = {
701 701 'source_repo_name': source_repo.repo_name,
702 702 'source_refs_json': json.dumps(source_repo_data),
703 703 'target_repo_name': default_target_repo.repo_name,
704 704 'target_refs_json': json.dumps(target_repo_data),
705 705 }
706 706 c.default_source_ref = selected_source_ref
707 707
708 708 return self._get_template_context(c)
709 709
710 710 @LoginRequired()
711 711 @NotAnonymous()
712 712 @HasRepoPermissionAnyDecorator(
713 713 'repository.read', 'repository.write', 'repository.admin')
714 714 @view_config(
715 715 route_name='pullrequest_repo_refs', request_method='GET',
716 716 renderer='json_ext', xhr=True)
717 717 def pull_request_repo_refs(self):
718 718 self.load_default_context()
719 719 target_repo_name = self.request.matchdict['target_repo_name']
720 720 repo = Repository.get_by_repo_name(target_repo_name)
721 721 if not repo:
722 722 raise HTTPNotFound()
723 723
724 724 target_perm = HasRepoPermissionAny(
725 725 'repository.read', 'repository.write', 'repository.admin')(
726 726 target_repo_name)
727 727 if not target_perm:
728 728 raise HTTPNotFound()
729 729
730 730 return PullRequestModel().generate_repo_data(
731 731 repo, translator=self.request.translate)
732 732
733 733 @LoginRequired()
734 734 @NotAnonymous()
735 735 @HasRepoPermissionAnyDecorator(
736 736 'repository.read', 'repository.write', 'repository.admin')
737 737 @view_config(
738 738 route_name='pullrequest_repo_destinations', request_method='GET',
739 739 renderer='json_ext', xhr=True)
740 740 def pull_request_repo_destinations(self):
741 741 _ = self.request.translate
742 742 filter_query = self.request.GET.get('query')
743 743
744 744 query = Repository.query() \
745 745 .order_by(func.length(Repository.repo_name)) \
746 746 .filter(
747 747 or_(Repository.repo_name == self.db_repo.repo_name,
748 748 Repository.fork_id == self.db_repo.repo_id))
749 749
750 750 if filter_query:
751 751 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
752 752 query = query.filter(
753 753 Repository.repo_name.ilike(ilike_expression))
754 754
755 755 add_parent = False
756 756 if self.db_repo.parent:
757 757 if filter_query in self.db_repo.parent.repo_name:
758 758 parent_vcs_obj = self.db_repo.parent.scm_instance()
759 759 if parent_vcs_obj and not parent_vcs_obj.is_empty():
760 760 add_parent = True
761 761
762 762 limit = 20 - 1 if add_parent else 20
763 763 all_repos = query.limit(limit).all()
764 764 if add_parent:
765 765 all_repos += [self.db_repo.parent]
766 766
767 767 repos = []
768 768 for obj in ScmModel().get_repos(all_repos):
769 769 repos.append({
770 770 'id': obj['name'],
771 771 'text': obj['name'],
772 772 'type': 'repo',
773 773 'repo_id': obj['dbrepo']['repo_id'],
774 774 'repo_type': obj['dbrepo']['repo_type'],
775 775 'private': obj['dbrepo']['private'],
776 776
777 777 })
778 778
779 779 data = {
780 780 'more': False,
781 781 'results': [{
782 782 'text': _('Repositories'),
783 783 'children': repos
784 784 }] if repos else []
785 785 }
786 786 return data
787 787
788 788 @LoginRequired()
789 789 @NotAnonymous()
790 790 @HasRepoPermissionAnyDecorator(
791 791 'repository.read', 'repository.write', 'repository.admin')
792 792 @CSRFRequired()
793 793 @view_config(
794 794 route_name='pullrequest_create', request_method='POST',
795 795 renderer=None)
796 796 def pull_request_create(self):
797 797 _ = self.request.translate
798 798 self.assure_not_empty_repo()
799 799 self.load_default_context()
800 800
801 801 controls = peppercorn.parse(self.request.POST.items())
802 802
803 803 try:
804 804 form = PullRequestForm(
805 805 self.request.translate, self.db_repo.repo_id)()
806 806 _form = form.to_python(controls)
807 807 except formencode.Invalid as errors:
808 808 if errors.error_dict.get('revisions'):
809 809 msg = 'Revisions: %s' % errors.error_dict['revisions']
810 810 elif errors.error_dict.get('pullrequest_title'):
811 811 msg = errors.error_dict.get('pullrequest_title')
812 812 else:
813 813 msg = _('Error creating pull request: {}').format(errors)
814 814 log.exception(msg)
815 815 h.flash(msg, 'error')
816 816
817 817 # would rather just go back to form ...
818 818 raise HTTPFound(
819 819 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
820 820
821 821 source_repo = _form['source_repo']
822 822 source_ref = _form['source_ref']
823 823 target_repo = _form['target_repo']
824 824 target_ref = _form['target_ref']
825 825 commit_ids = _form['revisions'][::-1]
826 826
827 827 # find the ancestor for this pr
828 828 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
829 829 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
830 830
831 831 # re-check permissions again here
832 832 # source_repo we must have read permissions
833 833
834 834 source_perm = HasRepoPermissionAny(
835 835 'repository.read',
836 836 'repository.write', 'repository.admin')(source_db_repo.repo_name)
837 837 if not source_perm:
838 838 msg = _('Not Enough permissions to source repo `{}`.'.format(
839 839 source_db_repo.repo_name))
840 840 h.flash(msg, category='error')
841 841 # copy the args back to redirect
842 842 org_query = self.request.GET.mixed()
843 843 raise HTTPFound(
844 844 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
845 845 _query=org_query))
846 846
847 847 # target repo we must have read permissions, and also later on
848 848 # we want to check branch permissions here
849 849 target_perm = HasRepoPermissionAny(
850 850 'repository.read',
851 851 'repository.write', 'repository.admin')(target_db_repo.repo_name)
852 852 if not target_perm:
853 853 msg = _('Not Enough permissions to target repo `{}`.'.format(
854 854 target_db_repo.repo_name))
855 855 h.flash(msg, category='error')
856 856 # copy the args back to redirect
857 857 org_query = self.request.GET.mixed()
858 858 raise HTTPFound(
859 859 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
860 860 _query=org_query))
861 861
862 862 source_scm = source_db_repo.scm_instance()
863 863 target_scm = target_db_repo.scm_instance()
864 864
865 865 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
866 866 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
867 867
868 868 ancestor = source_scm.get_common_ancestor(
869 869 source_commit.raw_id, target_commit.raw_id, target_scm)
870 870
871 871 # recalculate target ref based on ancestor
872 872 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
873 873 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
874 874
875 875 get_default_reviewers_data, validate_default_reviewers = \
876 876 PullRequestModel().get_reviewer_functions()
877 877
878 878 # recalculate reviewers logic, to make sure we can validate this
879 879 reviewer_rules = get_default_reviewers_data(
880 880 self._rhodecode_db_user, source_db_repo,
881 881 source_commit, target_db_repo, target_commit)
882 882
883 883 given_reviewers = _form['review_members']
884 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
884 reviewers = validate_default_reviewers(
885 given_reviewers, reviewer_rules)
885 886
886 887 pullrequest_title = _form['pullrequest_title']
887 888 title_source_ref = source_ref.split(':', 2)[1]
888 889 if not pullrequest_title:
889 890 pullrequest_title = PullRequestModel().generate_pullrequest_title(
890 891 source=source_repo,
891 892 source_ref=title_source_ref,
892 893 target=target_repo
893 894 )
894 895
895 896 description = _form['pullrequest_desc']
896 897
897 898 try:
898 899 pull_request = PullRequestModel().create(
899 900 created_by=self._rhodecode_user.user_id,
900 901 source_repo=source_repo,
901 902 source_ref=source_ref,
902 903 target_repo=target_repo,
903 904 target_ref=target_ref,
904 905 revisions=commit_ids,
905 906 reviewers=reviewers,
906 907 title=pullrequest_title,
907 908 description=description,
908 909 reviewer_data=reviewer_rules,
909 910 auth_user=self._rhodecode_user
910 911 )
911 912 Session().commit()
912 913
913 914 h.flash(_('Successfully opened new pull request'),
914 915 category='success')
915 916 except Exception:
916 917 msg = _('Error occurred during creation of this pull request.')
917 918 log.exception(msg)
918 919 h.flash(msg, category='error')
919 920
920 921 # copy the args back to redirect
921 922 org_query = self.request.GET.mixed()
922 923 raise HTTPFound(
923 924 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
924 925 _query=org_query))
925 926
926 927 raise HTTPFound(
927 928 h.route_path('pullrequest_show', repo_name=target_repo,
928 929 pull_request_id=pull_request.pull_request_id))
929 930
930 931 @LoginRequired()
931 932 @NotAnonymous()
932 933 @HasRepoPermissionAnyDecorator(
933 934 'repository.read', 'repository.write', 'repository.admin')
934 935 @CSRFRequired()
935 936 @view_config(
936 937 route_name='pullrequest_update', request_method='POST',
937 938 renderer='json_ext')
938 939 def pull_request_update(self):
939 940 pull_request = PullRequest.get_or_404(
940 941 self.request.matchdict['pull_request_id'])
941 942 _ = self.request.translate
942 943
943 944 self.load_default_context()
944 945
945 946 if pull_request.is_closed():
946 947 log.debug('update: forbidden because pull request is closed')
947 948 msg = _(u'Cannot update closed pull requests.')
948 949 h.flash(msg, category='error')
949 950 return True
950 951
951 952 # only owner or admin can update it
952 953 allowed_to_update = PullRequestModel().check_user_update(
953 954 pull_request, self._rhodecode_user)
954 955 if allowed_to_update:
955 956 controls = peppercorn.parse(self.request.POST.items())
956 957
957 958 if 'review_members' in controls:
958 959 self._update_reviewers(
959 960 pull_request, controls['review_members'],
960 961 pull_request.reviewer_data)
961 962 elif str2bool(self.request.POST.get('update_commits', 'false')):
962 963 self._update_commits(pull_request)
963 964 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
964 965 self._edit_pull_request(pull_request)
965 966 else:
966 967 raise HTTPBadRequest()
967 968 return True
968 969 raise HTTPForbidden()
969 970
970 971 def _edit_pull_request(self, pull_request):
971 972 _ = self.request.translate
972 973 try:
973 974 PullRequestModel().edit(
974 975 pull_request, self.request.POST.get('title'),
975 976 self.request.POST.get('description'), self._rhodecode_user)
976 977 except ValueError:
977 978 msg = _(u'Cannot update closed pull requests.')
978 979 h.flash(msg, category='error')
979 980 return
980 981 else:
981 982 Session().commit()
982 983
983 984 msg = _(u'Pull request title & description updated.')
984 985 h.flash(msg, category='success')
985 986 return
986 987
987 988 def _update_commits(self, pull_request):
988 989 _ = self.request.translate
989 990 resp = PullRequestModel().update_commits(pull_request)
990 991
991 992 if resp.executed:
992 993
993 994 if resp.target_changed and resp.source_changed:
994 995 changed = 'target and source repositories'
995 996 elif resp.target_changed and not resp.source_changed:
996 997 changed = 'target repository'
997 998 elif not resp.target_changed and resp.source_changed:
998 999 changed = 'source repository'
999 1000 else:
1000 1001 changed = 'nothing'
1001 1002
1002 1003 msg = _(
1003 1004 u'Pull request updated to "{source_commit_id}" with '
1004 1005 u'{count_added} added, {count_removed} removed commits. '
1005 1006 u'Source of changes: {change_source}')
1006 1007 msg = msg.format(
1007 1008 source_commit_id=pull_request.source_ref_parts.commit_id,
1008 1009 count_added=len(resp.changes.added),
1009 1010 count_removed=len(resp.changes.removed),
1010 1011 change_source=changed)
1011 1012 h.flash(msg, category='success')
1012 1013
1013 1014 channel = '/repo${}$/pr/{}'.format(
1014 1015 pull_request.target_repo.repo_name,
1015 1016 pull_request.pull_request_id)
1016 1017 message = msg + (
1017 1018 ' - <a onclick="window.location.reload()">'
1018 1019 '<strong>{}</strong></a>'.format(_('Reload page')))
1019 1020 channelstream.post_message(
1020 1021 channel, message, self._rhodecode_user.username,
1021 1022 registry=self.request.registry)
1022 1023 else:
1023 1024 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1024 1025 warning_reasons = [
1025 1026 UpdateFailureReason.NO_CHANGE,
1026 1027 UpdateFailureReason.WRONG_REF_TYPE,
1027 1028 ]
1028 1029 category = 'warning' if resp.reason in warning_reasons else 'error'
1029 1030 h.flash(msg, category=category)
1030 1031
1031 1032 @LoginRequired()
1032 1033 @NotAnonymous()
1033 1034 @HasRepoPermissionAnyDecorator(
1034 1035 'repository.read', 'repository.write', 'repository.admin')
1035 1036 @CSRFRequired()
1036 1037 @view_config(
1037 1038 route_name='pullrequest_merge', request_method='POST',
1038 1039 renderer='json_ext')
1039 1040 def pull_request_merge(self):
1040 1041 """
1041 1042 Merge will perform a server-side merge of the specified
1042 1043 pull request, if the pull request is approved and mergeable.
1043 1044 After successful merging, the pull request is automatically
1044 1045 closed, with a relevant comment.
1045 1046 """
1046 1047 pull_request = PullRequest.get_or_404(
1047 1048 self.request.matchdict['pull_request_id'])
1048 1049
1049 1050 self.load_default_context()
1050 1051 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1051 1052 translator=self.request.translate)
1052 1053 merge_possible = not check.failed
1053 1054
1054 1055 for err_type, error_msg in check.errors:
1055 1056 h.flash(error_msg, category=err_type)
1056 1057
1057 1058 if merge_possible:
1058 1059 log.debug("Pre-conditions checked, trying to merge.")
1059 1060 extras = vcs_operation_context(
1060 1061 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1061 1062 username=self._rhodecode_db_user.username, action='push',
1062 1063 scm=pull_request.target_repo.repo_type)
1063 1064 self._merge_pull_request(
1064 1065 pull_request, self._rhodecode_db_user, extras)
1065 1066 else:
1066 1067 log.debug("Pre-conditions failed, NOT merging.")
1067 1068
1068 1069 raise HTTPFound(
1069 1070 h.route_path('pullrequest_show',
1070 1071 repo_name=pull_request.target_repo.repo_name,
1071 1072 pull_request_id=pull_request.pull_request_id))
1072 1073
1073 1074 def _merge_pull_request(self, pull_request, user, extras):
1074 1075 _ = self.request.translate
1075 1076 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1076 1077
1077 1078 if merge_resp.executed:
1078 1079 log.debug("The merge was successful, closing the pull request.")
1079 1080 PullRequestModel().close_pull_request(
1080 1081 pull_request.pull_request_id, user)
1081 1082 Session().commit()
1082 1083 msg = _('Pull request was successfully merged and closed.')
1083 1084 h.flash(msg, category='success')
1084 1085 else:
1085 1086 log.debug(
1086 1087 "The merge was not successful. Merge response: %s",
1087 1088 merge_resp)
1088 1089 msg = PullRequestModel().merge_status_message(
1089 1090 merge_resp.failure_reason)
1090 1091 h.flash(msg, category='error')
1091 1092
1092 1093 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1093 1094 _ = self.request.translate
1094 1095 get_default_reviewers_data, validate_default_reviewers = \
1095 1096 PullRequestModel().get_reviewer_functions()
1096 1097
1097 1098 try:
1098 1099 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1099 1100 except ValueError as e:
1100 1101 log.error('Reviewers Validation: {}'.format(e))
1101 1102 h.flash(e, category='error')
1102 1103 return
1103 1104
1104 1105 PullRequestModel().update_reviewers(
1105 1106 pull_request, reviewers, self._rhodecode_user)
1106 1107 h.flash(_('Pull request reviewers updated.'), category='success')
1107 1108 Session().commit()
1108 1109
1109 1110 @LoginRequired()
1110 1111 @NotAnonymous()
1111 1112 @HasRepoPermissionAnyDecorator(
1112 1113 'repository.read', 'repository.write', 'repository.admin')
1113 1114 @CSRFRequired()
1114 1115 @view_config(
1115 1116 route_name='pullrequest_delete', request_method='POST',
1116 1117 renderer='json_ext')
1117 1118 def pull_request_delete(self):
1118 1119 _ = self.request.translate
1119 1120
1120 1121 pull_request = PullRequest.get_or_404(
1121 1122 self.request.matchdict['pull_request_id'])
1122 1123 self.load_default_context()
1123 1124
1124 1125 pr_closed = pull_request.is_closed()
1125 1126 allowed_to_delete = PullRequestModel().check_user_delete(
1126 1127 pull_request, self._rhodecode_user) and not pr_closed
1127 1128
1128 1129 # only owner can delete it !
1129 1130 if allowed_to_delete:
1130 1131 PullRequestModel().delete(pull_request, self._rhodecode_user)
1131 1132 Session().commit()
1132 1133 h.flash(_('Successfully deleted pull request'),
1133 1134 category='success')
1134 1135 raise HTTPFound(h.route_path('pullrequest_show_all',
1135 1136 repo_name=self.db_repo_name))
1136 1137
1137 1138 log.warning('user %s tried to delete pull request without access',
1138 1139 self._rhodecode_user)
1139 1140 raise HTTPNotFound()
1140 1141
1141 1142 @LoginRequired()
1142 1143 @NotAnonymous()
1143 1144 @HasRepoPermissionAnyDecorator(
1144 1145 'repository.read', 'repository.write', 'repository.admin')
1145 1146 @CSRFRequired()
1146 1147 @view_config(
1147 1148 route_name='pullrequest_comment_create', request_method='POST',
1148 1149 renderer='json_ext')
1149 1150 def pull_request_comment_create(self):
1150 1151 _ = self.request.translate
1151 1152
1152 1153 pull_request = PullRequest.get_or_404(
1153 1154 self.request.matchdict['pull_request_id'])
1154 1155 pull_request_id = pull_request.pull_request_id
1155 1156
1156 1157 if pull_request.is_closed():
1157 1158 log.debug('comment: forbidden because pull request is closed')
1158 1159 raise HTTPForbidden()
1159 1160
1160 1161 allowed_to_comment = PullRequestModel().check_user_comment(
1161 1162 pull_request, self._rhodecode_user)
1162 1163 if not allowed_to_comment:
1163 1164 log.debug(
1164 1165 'comment: forbidden because pull request is from forbidden repo')
1165 1166 raise HTTPForbidden()
1166 1167
1167 1168 c = self.load_default_context()
1168 1169
1169 1170 status = self.request.POST.get('changeset_status', None)
1170 1171 text = self.request.POST.get('text')
1171 1172 comment_type = self.request.POST.get('comment_type')
1172 1173 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1173 1174 close_pull_request = self.request.POST.get('close_pull_request')
1174 1175
1175 1176 # the logic here should work like following, if we submit close
1176 1177 # pr comment, use `close_pull_request_with_comment` function
1177 1178 # else handle regular comment logic
1178 1179
1179 1180 if close_pull_request:
1180 1181 # only owner or admin or person with write permissions
1181 1182 allowed_to_close = PullRequestModel().check_user_update(
1182 1183 pull_request, self._rhodecode_user)
1183 1184 if not allowed_to_close:
1184 1185 log.debug('comment: forbidden because not allowed to close '
1185 1186 'pull request %s', pull_request_id)
1186 1187 raise HTTPForbidden()
1187 1188 comment, status = PullRequestModel().close_pull_request_with_comment(
1188 1189 pull_request, self._rhodecode_user, self.db_repo, message=text)
1189 1190 Session().flush()
1190 1191 events.trigger(
1191 1192 events.PullRequestCommentEvent(pull_request, comment))
1192 1193
1193 1194 else:
1194 1195 # regular comment case, could be inline, or one with status.
1195 1196 # for that one we check also permissions
1196 1197
1197 1198 allowed_to_change_status = PullRequestModel().check_user_change_status(
1198 1199 pull_request, self._rhodecode_user)
1199 1200
1200 1201 if status and allowed_to_change_status:
1201 1202 message = (_('Status change %(transition_icon)s %(status)s')
1202 1203 % {'transition_icon': '>',
1203 1204 'status': ChangesetStatus.get_status_lbl(status)})
1204 1205 text = text or message
1205 1206
1206 1207 comment = CommentsModel().create(
1207 1208 text=text,
1208 1209 repo=self.db_repo.repo_id,
1209 1210 user=self._rhodecode_user.user_id,
1210 1211 pull_request=pull_request,
1211 1212 f_path=self.request.POST.get('f_path'),
1212 1213 line_no=self.request.POST.get('line'),
1213 1214 status_change=(ChangesetStatus.get_status_lbl(status)
1214 1215 if status and allowed_to_change_status else None),
1215 1216 status_change_type=(status
1216 1217 if status and allowed_to_change_status else None),
1217 1218 comment_type=comment_type,
1218 1219 resolves_comment_id=resolves_comment_id,
1219 1220 auth_user=self._rhodecode_user
1220 1221 )
1221 1222
1222 1223 if allowed_to_change_status:
1223 1224 # calculate old status before we change it
1224 1225 old_calculated_status = pull_request.calculated_review_status()
1225 1226
1226 1227 # get status if set !
1227 1228 if status:
1228 1229 ChangesetStatusModel().set_status(
1229 1230 self.db_repo.repo_id,
1230 1231 status,
1231 1232 self._rhodecode_user.user_id,
1232 1233 comment,
1233 1234 pull_request=pull_request
1234 1235 )
1235 1236
1236 1237 Session().flush()
1237 1238 # this is somehow required to get access to some relationship
1238 1239 # loaded on comment
1239 1240 Session().refresh(comment)
1240 1241
1241 1242 events.trigger(
1242 1243 events.PullRequestCommentEvent(pull_request, comment))
1243 1244
1244 1245 # we now calculate the status of pull request, and based on that
1245 1246 # calculation we set the commits status
1246 1247 calculated_status = pull_request.calculated_review_status()
1247 1248 if old_calculated_status != calculated_status:
1248 1249 PullRequestModel()._trigger_pull_request_hook(
1249 1250 pull_request, self._rhodecode_user, 'review_status_change')
1250 1251
1251 1252 Session().commit()
1252 1253
1253 1254 data = {
1254 1255 'target_id': h.safeid(h.safe_unicode(
1255 1256 self.request.POST.get('f_path'))),
1256 1257 }
1257 1258 if comment:
1258 1259 c.co = comment
1259 1260 rendered_comment = render(
1260 1261 'rhodecode:templates/changeset/changeset_comment_block.mako',
1261 1262 self._get_template_context(c), self.request)
1262 1263
1263 1264 data.update(comment.get_dict())
1264 1265 data.update({'rendered_text': rendered_comment})
1265 1266
1266 1267 return data
1267 1268
1268 1269 @LoginRequired()
1269 1270 @NotAnonymous()
1270 1271 @HasRepoPermissionAnyDecorator(
1271 1272 'repository.read', 'repository.write', 'repository.admin')
1272 1273 @CSRFRequired()
1273 1274 @view_config(
1274 1275 route_name='pullrequest_comment_delete', request_method='POST',
1275 1276 renderer='json_ext')
1276 1277 def pull_request_comment_delete(self):
1277 1278 pull_request = PullRequest.get_or_404(
1278 1279 self.request.matchdict['pull_request_id'])
1279 1280
1280 1281 comment = ChangesetComment.get_or_404(
1281 1282 self.request.matchdict['comment_id'])
1282 1283 comment_id = comment.comment_id
1283 1284
1284 1285 if pull_request.is_closed():
1285 1286 log.debug('comment: forbidden because pull request is closed')
1286 1287 raise HTTPForbidden()
1287 1288
1288 1289 if not comment:
1289 1290 log.debug('Comment with id:%s not found, skipping', comment_id)
1290 1291 # comment already deleted in another call probably
1291 1292 return True
1292 1293
1293 1294 if comment.pull_request.is_closed():
1294 1295 # don't allow deleting comments on closed pull request
1295 1296 raise HTTPForbidden()
1296 1297
1297 1298 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1298 1299 super_admin = h.HasPermissionAny('hg.admin')()
1299 1300 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1300 1301 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1301 1302 comment_repo_admin = is_repo_admin and is_repo_comment
1302 1303
1303 1304 if super_admin or comment_owner or comment_repo_admin:
1304 1305 old_calculated_status = comment.pull_request.calculated_review_status()
1305 1306 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1306 1307 Session().commit()
1307 1308 calculated_status = comment.pull_request.calculated_review_status()
1308 1309 if old_calculated_status != calculated_status:
1309 1310 PullRequestModel()._trigger_pull_request_hook(
1310 1311 comment.pull_request, self._rhodecode_user, 'review_status_change')
1311 1312 return True
1312 1313 else:
1313 1314 log.warning('No permissions for user %s to delete comment_id: %s',
1314 1315 self._rhodecode_db_user, comment_id)
1315 1316 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now