##// END OF EJS Templates
pull-requests: add merge validation to prevent merges to protected branches.
marcink -
r2981:1e14730d default
parent child Browse files
Show More
@@ -1,337 +1,338 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import User
23 from rhodecode.model.db import User
24 from rhodecode.model.pull_request import PullRequestModel
24 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.repo import RepoModel
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.user import UserModel
26 from rhodecode.model.user import UserModel
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCreatePullRequestApi(object):
32 class TestCreatePullRequestApi(object):
33 finalizers = []
33 finalizers = []
34
34
35 def teardown_method(self, method):
35 def teardown_method(self, method):
36 if self.finalizers:
36 if self.finalizers:
37 for finalizer in self.finalizers:
37 for finalizer in self.finalizers:
38 finalizer()
38 finalizer()
39 self.finalizers = []
39 self.finalizers = []
40
40
41 def test_create_with_wrong_data(self):
41 def test_create_with_wrong_data(self):
42 required_data = {
42 required_data = {
43 'source_repo': 'tests/source_repo',
43 'source_repo': 'tests/source_repo',
44 'target_repo': 'tests/target_repo',
44 'target_repo': 'tests/target_repo',
45 'source_ref': 'branch:default:initial',
45 'source_ref': 'branch:default:initial',
46 'target_ref': 'branch:default:new-feature',
46 'target_ref': 'branch:default:new-feature',
47 }
47 }
48 for key in required_data:
48 for key in required_data:
49 data = required_data.copy()
49 data = required_data.copy()
50 data.pop(key)
50 data.pop(key)
51 id_, params = build_data(
51 id_, params = build_data(
52 self.apikey, 'create_pull_request', **data)
52 self.apikey, 'create_pull_request', **data)
53 response = api_call(self.app, params)
53 response = api_call(self.app, params)
54
54
55 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
55 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
56 assert_error(id_, expected, given=response.body)
56 assert_error(id_, expected, given=response.body)
57
57
58 @pytest.mark.backends("git", "hg")
58 @pytest.mark.backends("git", "hg")
59 def test_create_with_correct_data(self, backend):
59 def test_create_with_correct_data(self, backend):
60 data = self._prepare_data(backend)
60 data = self._prepare_data(backend)
61 RepoModel().revoke_user_permission(
61 RepoModel().revoke_user_permission(
62 self.source.repo_name, User.DEFAULT_USER)
62 self.source.repo_name, User.DEFAULT_USER)
63 id_, params = build_data(
63 id_, params = build_data(
64 self.apikey_regular, 'create_pull_request', **data)
64 self.apikey_regular, 'create_pull_request', **data)
65 response = api_call(self.app, params)
65 response = api_call(self.app, params)
66 expected_message = "Created new pull request `{title}`".format(
66 expected_message = "Created new pull request `{title}`".format(
67 title=data['title'])
67 title=data['title'])
68 result = response.json
68 result = response.json
69 assert result['result']['msg'] == expected_message
69 assert result['result']['msg'] == expected_message
70 pull_request_id = result['result']['pull_request_id']
70 pull_request_id = result['result']['pull_request_id']
71 pull_request = PullRequestModel().get(pull_request_id)
71 pull_request = PullRequestModel().get(pull_request_id)
72 assert pull_request.title == data['title']
72 assert pull_request.title == data['title']
73 assert pull_request.description == data['description']
73 assert pull_request.description == data['description']
74 assert pull_request.source_ref == data['source_ref']
74 assert pull_request.source_ref == data['source_ref']
75 assert pull_request.target_ref == data['target_ref']
75 assert pull_request.target_ref == data['target_ref']
76 assert pull_request.source_repo.repo_name == data['source_repo']
76 assert pull_request.source_repo.repo_name == data['source_repo']
77 assert pull_request.target_repo.repo_name == data['target_repo']
77 assert pull_request.target_repo.repo_name == data['target_repo']
78 assert pull_request.revisions == [self.commit_ids['change']]
78 assert pull_request.revisions == [self.commit_ids['change']]
79 assert len(pull_request.reviewers) == 1
79 assert len(pull_request.reviewers) == 1
80
80
81 @pytest.mark.backends("git", "hg")
81 @pytest.mark.backends("git", "hg")
82 def test_create_with_empty_description(self, backend):
82 def test_create_with_empty_description(self, backend):
83 data = self._prepare_data(backend)
83 data = self._prepare_data(backend)
84 data.pop('description')
84 data.pop('description')
85 id_, params = build_data(
85 id_, params = build_data(
86 self.apikey_regular, 'create_pull_request', **data)
86 self.apikey_regular, 'create_pull_request', **data)
87 response = api_call(self.app, params)
87 response = api_call(self.app, params)
88 expected_message = "Created new pull request `{title}`".format(
88 expected_message = "Created new pull request `{title}`".format(
89 title=data['title'])
89 title=data['title'])
90 result = response.json
90 result = response.json
91 assert result['result']['msg'] == expected_message
91 assert result['result']['msg'] == expected_message
92 pull_request_id = result['result']['pull_request_id']
92 pull_request_id = result['result']['pull_request_id']
93 pull_request = PullRequestModel().get(pull_request_id)
93 pull_request = PullRequestModel().get(pull_request_id)
94 assert pull_request.description == ''
94 assert pull_request.description == ''
95
95
96 @pytest.mark.backends("git", "hg")
96 @pytest.mark.backends("git", "hg")
97 def test_create_with_empty_title(self, backend):
97 def test_create_with_empty_title(self, backend):
98 data = self._prepare_data(backend)
98 data = self._prepare_data(backend)
99 data.pop('title')
99 data.pop('title')
100 id_, params = build_data(
100 id_, params = build_data(
101 self.apikey_regular, 'create_pull_request', **data)
101 self.apikey_regular, 'create_pull_request', **data)
102 response = api_call(self.app, params)
102 response = api_call(self.app, params)
103 result = response.json
103 result = response.json
104 pull_request_id = result['result']['pull_request_id']
104 pull_request_id = result['result']['pull_request_id']
105 pull_request = PullRequestModel().get(pull_request_id)
105 pull_request = PullRequestModel().get(pull_request_id)
106 data['ref'] = backend.default_branch_name
106 data['ref'] = backend.default_branch_name
107 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
107 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
108 assert pull_request.title == title
108 assert pull_request.title == title
109
109
110 @pytest.mark.backends("git", "hg")
110 @pytest.mark.backends("git", "hg")
111 def test_create_with_reviewers_specified_by_names(
111 def test_create_with_reviewers_specified_by_names(
112 self, backend, no_notifications):
112 self, backend, no_notifications):
113 data = self._prepare_data(backend)
113 data = self._prepare_data(backend)
114 reviewers = [
114 reviewers = [
115 {'username': TEST_USER_REGULAR_LOGIN,
115 {'username': TEST_USER_REGULAR_LOGIN,
116 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
116 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
117 {'username': TEST_USER_ADMIN_LOGIN,
117 {'username': TEST_USER_ADMIN_LOGIN,
118 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
118 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
119 'mandatory': True},
119 'mandatory': True},
120 ]
120 ]
121 data['reviewers'] = reviewers
121 data['reviewers'] = reviewers
122
122
123 id_, params = build_data(
123 id_, params = build_data(
124 self.apikey_regular, 'create_pull_request', **data)
124 self.apikey_regular, 'create_pull_request', **data)
125 response = api_call(self.app, params)
125 response = api_call(self.app, params)
126
126
127 expected_message = "Created new pull request `{title}`".format(
127 expected_message = "Created new pull request `{title}`".format(
128 title=data['title'])
128 title=data['title'])
129 result = response.json
129 result = response.json
130 assert result['result']['msg'] == expected_message
130 assert result['result']['msg'] == expected_message
131 pull_request_id = result['result']['pull_request_id']
131 pull_request_id = result['result']['pull_request_id']
132 pull_request = PullRequestModel().get(pull_request_id)
132 pull_request = PullRequestModel().get(pull_request_id)
133
133
134 actual_reviewers = []
134 actual_reviewers = []
135 for rev in pull_request.reviewers:
135 for rev in pull_request.reviewers:
136 entry = {
136 entry = {
137 'username': rev.user.username,
137 'username': rev.user.username,
138 'reasons': rev.reasons,
138 'reasons': rev.reasons,
139 }
139 }
140 if rev.mandatory:
140 if rev.mandatory:
141 entry['mandatory'] = rev.mandatory
141 entry['mandatory'] = rev.mandatory
142 actual_reviewers.append(entry)
142 actual_reviewers.append(entry)
143
143
144 # default reviewer will be added who is an owner of the repo
144 # default reviewer will be added who is an owner of the repo
145 reviewers.append(
145 reviewers.append(
146 {'username': pull_request.author.username,
146 {'username': pull_request.author.username,
147 'reasons': [u'Default reviewer', u'Repository owner']},
147 'reasons': [u'Default reviewer', u'Repository owner']},
148 )
148 )
149 assert sorted(actual_reviewers, key=lambda e: e['username']) \
149 assert sorted(actual_reviewers, key=lambda e: e['username']) \
150 == sorted(reviewers, key=lambda e: e['username'])
150 == sorted(reviewers, key=lambda e: e['username'])
151
151
152 @pytest.mark.backends("git", "hg")
152 @pytest.mark.backends("git", "hg")
153 def test_create_with_reviewers_specified_by_ids(
153 def test_create_with_reviewers_specified_by_ids(
154 self, backend, no_notifications):
154 self, backend, no_notifications):
155 data = self._prepare_data(backend)
155 data = self._prepare_data(backend)
156 reviewers = [
156 reviewers = [
157 {'username': UserModel().get_by_username(
157 {'username': UserModel().get_by_username(
158 TEST_USER_REGULAR_LOGIN).user_id,
158 TEST_USER_REGULAR_LOGIN).user_id,
159 'reasons': ['added manually']},
159 'reasons': ['added manually']},
160 {'username': UserModel().get_by_username(
160 {'username': UserModel().get_by_username(
161 TEST_USER_ADMIN_LOGIN).user_id,
161 TEST_USER_ADMIN_LOGIN).user_id,
162 'reasons': ['added manually']},
162 'reasons': ['added manually']},
163 ]
163 ]
164
164
165 data['reviewers'] = reviewers
165 data['reviewers'] = reviewers
166 id_, params = build_data(
166 id_, params = build_data(
167 self.apikey_regular, 'create_pull_request', **data)
167 self.apikey_regular, 'create_pull_request', **data)
168 response = api_call(self.app, params)
168 response = api_call(self.app, params)
169
169
170 expected_message = "Created new pull request `{title}`".format(
170 expected_message = "Created new pull request `{title}`".format(
171 title=data['title'])
171 title=data['title'])
172 result = response.json
172 result = response.json
173 assert result['result']['msg'] == expected_message
173 assert result['result']['msg'] == expected_message
174 pull_request_id = result['result']['pull_request_id']
174 pull_request_id = result['result']['pull_request_id']
175 pull_request = PullRequestModel().get(pull_request_id)
175 pull_request = PullRequestModel().get(pull_request_id)
176
176
177 actual_reviewers = []
177 actual_reviewers = []
178 for rev in pull_request.reviewers:
178 for rev in pull_request.reviewers:
179 entry = {
179 entry = {
180 'username': rev.user.user_id,
180 'username': rev.user.user_id,
181 'reasons': rev.reasons,
181 'reasons': rev.reasons,
182 }
182 }
183 if rev.mandatory:
183 if rev.mandatory:
184 entry['mandatory'] = rev.mandatory
184 entry['mandatory'] = rev.mandatory
185 actual_reviewers.append(entry)
185 actual_reviewers.append(entry)
186 # default reviewer will be added who is an owner of the repo
186 # default reviewer will be added who is an owner of the repo
187 reviewers.append(
187 reviewers.append(
188 {'username': pull_request.author.user_id,
188 {'username': pull_request.author.user_id,
189 'reasons': [u'Default reviewer', u'Repository owner']},
189 'reasons': [u'Default reviewer', u'Repository owner']},
190 )
190 )
191 assert sorted(actual_reviewers, key=lambda e: e['username']) \
191 assert sorted(actual_reviewers, key=lambda e: e['username']) \
192 == sorted(reviewers, key=lambda e: e['username'])
192 == sorted(reviewers, key=lambda e: e['username'])
193
193
194 @pytest.mark.backends("git", "hg")
194 @pytest.mark.backends("git", "hg")
195 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
195 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
196 data = self._prepare_data(backend)
196 data = self._prepare_data(backend)
197 data['reviewers'] = [{'username': 'somebody'}]
197 data['reviewers'] = [{'username': 'somebody'}]
198 id_, params = build_data(
198 id_, params = build_data(
199 self.apikey_regular, 'create_pull_request', **data)
199 self.apikey_regular, 'create_pull_request', **data)
200 response = api_call(self.app, params)
200 response = api_call(self.app, params)
201 expected_message = 'user `somebody` does not exist'
201 expected_message = 'user `somebody` does not exist'
202 assert_error(id_, expected_message, given=response.body)
202 assert_error(id_, expected_message, given=response.body)
203
203
204 @pytest.mark.backends("git", "hg")
204 @pytest.mark.backends("git", "hg")
205 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
205 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
206 data = self._prepare_data(backend)
206 data = self._prepare_data(backend)
207 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
207 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
208 data['reviewers'] = reviewers
208 data['reviewers'] = reviewers
209 id_, params = build_data(
209 id_, params = build_data(
210 self.apikey_regular, 'create_pull_request', **data)
210 self.apikey_regular, 'create_pull_request', **data)
211 response = api_call(self.app, params)
211 response = api_call(self.app, params)
212 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
212 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
213 assert_error(id_, expected_message, given=response.body)
213 assert_error(id_, expected_message, given=response.body)
214
214
215 @pytest.mark.backends("git", "hg")
215 @pytest.mark.backends("git", "hg")
216 def test_create_with_no_commit_hashes(self, backend):
216 def test_create_with_no_commit_hashes(self, backend):
217 data = self._prepare_data(backend)
217 data = self._prepare_data(backend)
218 expected_source_ref = data['source_ref']
218 expected_source_ref = data['source_ref']
219 expected_target_ref = data['target_ref']
219 expected_target_ref = data['target_ref']
220 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
220 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
221 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
221 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
222 id_, params = build_data(
222 id_, params = build_data(
223 self.apikey_regular, 'create_pull_request', **data)
223 self.apikey_regular, 'create_pull_request', **data)
224 response = api_call(self.app, params)
224 response = api_call(self.app, params)
225 expected_message = "Created new pull request `{title}`".format(
225 expected_message = "Created new pull request `{title}`".format(
226 title=data['title'])
226 title=data['title'])
227 result = response.json
227 result = response.json
228 assert result['result']['msg'] == expected_message
228 assert result['result']['msg'] == expected_message
229 pull_request_id = result['result']['pull_request_id']
229 pull_request_id = result['result']['pull_request_id']
230 pull_request = PullRequestModel().get(pull_request_id)
230 pull_request = PullRequestModel().get(pull_request_id)
231 assert pull_request.source_ref == expected_source_ref
231 assert pull_request.source_ref == expected_source_ref
232 assert pull_request.target_ref == expected_target_ref
232 assert pull_request.target_ref == expected_target_ref
233
233
234 @pytest.mark.backends("git", "hg")
234 @pytest.mark.backends("git", "hg")
235 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
235 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
236 def test_create_fails_with_wrong_repo(self, backend, data_key):
236 def test_create_fails_with_wrong_repo(self, backend, data_key):
237 repo_name = 'fake-repo'
237 repo_name = 'fake-repo'
238 data = self._prepare_data(backend)
238 data = self._prepare_data(backend)
239 data[data_key] = repo_name
239 data[data_key] = repo_name
240 id_, params = build_data(
240 id_, params = build_data(
241 self.apikey_regular, 'create_pull_request', **data)
241 self.apikey_regular, 'create_pull_request', **data)
242 response = api_call(self.app, params)
242 response = api_call(self.app, params)
243 expected_message = 'repository `{}` does not exist'.format(repo_name)
243 expected_message = 'repository `{}` does not exist'.format(repo_name)
244 assert_error(id_, expected_message, given=response.body)
244 assert_error(id_, expected_message, given=response.body)
245
245
246 @pytest.mark.backends("git", "hg")
246 @pytest.mark.backends("git", "hg")
247 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
247 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
248 def test_create_fails_with_non_existing_branch(self, backend, data_key):
248 def test_create_fails_with_non_existing_branch(self, backend, data_key):
249 branch_name = 'test-branch'
249 branch_name = 'test-branch'
250 data = self._prepare_data(backend)
250 data = self._prepare_data(backend)
251 data[data_key] = "branch:{}".format(branch_name)
251 data[data_key] = "branch:{}".format(branch_name)
252 id_, params = build_data(
252 id_, params = build_data(
253 self.apikey_regular, 'create_pull_request', **data)
253 self.apikey_regular, 'create_pull_request', **data)
254 response = api_call(self.app, params)
254 response = api_call(self.app, params)
255 expected_message = 'The specified branch `{}` does not exist'.format(
255 expected_message = 'The specified value:{type}:`{name}` ' \
256 branch_name)
256 'does not exist, or is not allowed.'.format(type='branch',
257 name=branch_name)
257 assert_error(id_, expected_message, given=response.body)
258 assert_error(id_, expected_message, given=response.body)
258
259
259 @pytest.mark.backends("git", "hg")
260 @pytest.mark.backends("git", "hg")
260 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
261 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
261 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
262 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
262 data = self._prepare_data(backend)
263 data = self._prepare_data(backend)
263 ref = 'stange-ref'
264 ref = 'stange-ref'
264 data[data_key] = ref
265 data[data_key] = ref
265 id_, params = build_data(
266 id_, params = build_data(
266 self.apikey_regular, 'create_pull_request', **data)
267 self.apikey_regular, 'create_pull_request', **data)
267 response = api_call(self.app, params)
268 response = api_call(self.app, params)
268 expected_message = (
269 expected_message = (
269 'Ref `{ref}` given in a wrong format. Please check the API'
270 'Ref `{ref}` given in a wrong format. Please check the API'
270 ' documentation for more details'.format(ref=ref))
271 ' documentation for more details'.format(ref=ref))
271 assert_error(id_, expected_message, given=response.body)
272 assert_error(id_, expected_message, given=response.body)
272
273
273 @pytest.mark.backends("git", "hg")
274 @pytest.mark.backends("git", "hg")
274 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
275 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
275 def test_create_fails_with_non_existing_ref(self, backend, data_key):
276 def test_create_fails_with_non_existing_ref(self, backend, data_key):
276 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
277 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
277 ref = self._get_full_ref(backend, commit_id)
278 ref = self._get_full_ref(backend, commit_id)
278 data = self._prepare_data(backend)
279 data = self._prepare_data(backend)
279 data[data_key] = ref
280 data[data_key] = ref
280 id_, params = build_data(
281 id_, params = build_data(
281 self.apikey_regular, 'create_pull_request', **data)
282 self.apikey_regular, 'create_pull_request', **data)
282 response = api_call(self.app, params)
283 response = api_call(self.app, params)
283 expected_message = 'Ref `{}` does not exist'.format(ref)
284 expected_message = 'Ref `{}` does not exist'.format(ref)
284 assert_error(id_, expected_message, given=response.body)
285 assert_error(id_, expected_message, given=response.body)
285
286
286 @pytest.mark.backends("git", "hg")
287 @pytest.mark.backends("git", "hg")
287 def test_create_fails_when_no_revisions(self, backend):
288 def test_create_fails_when_no_revisions(self, backend):
288 data = self._prepare_data(backend, source_head='initial')
289 data = self._prepare_data(backend, source_head='initial')
289 id_, params = build_data(
290 id_, params = build_data(
290 self.apikey_regular, 'create_pull_request', **data)
291 self.apikey_regular, 'create_pull_request', **data)
291 response = api_call(self.app, params)
292 response = api_call(self.app, params)
292 expected_message = 'no commits found'
293 expected_message = 'no commits found'
293 assert_error(id_, expected_message, given=response.body)
294 assert_error(id_, expected_message, given=response.body)
294
295
295 @pytest.mark.backends("git", "hg")
296 @pytest.mark.backends("git", "hg")
296 def test_create_fails_when_no_permissions(self, backend):
297 def test_create_fails_when_no_permissions(self, backend):
297 data = self._prepare_data(backend)
298 data = self._prepare_data(backend)
298 RepoModel().revoke_user_permission(
299 RepoModel().revoke_user_permission(
299 self.source.repo_name, self.test_user)
300 self.source.repo_name, self.test_user)
300 RepoModel().revoke_user_permission(
301 RepoModel().revoke_user_permission(
301 self.source.repo_name, User.DEFAULT_USER)
302 self.source.repo_name, User.DEFAULT_USER)
302
303
303 id_, params = build_data(
304 id_, params = build_data(
304 self.apikey_regular, 'create_pull_request', **data)
305 self.apikey_regular, 'create_pull_request', **data)
305 response = api_call(self.app, params)
306 response = api_call(self.app, params)
306 expected_message = 'repository `{}` does not exist'.format(
307 expected_message = 'repository `{}` does not exist'.format(
307 self.source.repo_name)
308 self.source.repo_name)
308 assert_error(id_, expected_message, given=response.body)
309 assert_error(id_, expected_message, given=response.body)
309
310
310 def _prepare_data(
311 def _prepare_data(
311 self, backend, source_head='change', target_head='initial'):
312 self, backend, source_head='change', target_head='initial'):
312 commits = [
313 commits = [
313 {'message': 'initial'},
314 {'message': 'initial'},
314 {'message': 'change'},
315 {'message': 'change'},
315 {'message': 'new-feature', 'parents': ['initial']},
316 {'message': 'new-feature', 'parents': ['initial']},
316 ]
317 ]
317 self.commit_ids = backend.create_master_repo(commits)
318 self.commit_ids = backend.create_master_repo(commits)
318 self.source = backend.create_repo(heads=[source_head])
319 self.source = backend.create_repo(heads=[source_head])
319 self.target = backend.create_repo(heads=[target_head])
320 self.target = backend.create_repo(heads=[target_head])
320
321
321 data = {
322 data = {
322 'source_repo': self.source.repo_name,
323 'source_repo': self.source.repo_name,
323 'target_repo': self.target.repo_name,
324 'target_repo': self.target.repo_name,
324 'source_ref': self._get_full_ref(
325 'source_ref': self._get_full_ref(
325 backend, self.commit_ids[source_head]),
326 backend, self.commit_ids[source_head]),
326 'target_ref': self._get_full_ref(
327 'target_ref': self._get_full_ref(
327 backend, self.commit_ids[target_head]),
328 backend, self.commit_ids[target_head]),
328 'title': 'Test PR 1',
329 'title': 'Test PR 1',
329 'description': 'Test'
330 'description': 'Test'
330 }
331 }
331 RepoModel().grant_user_permission(
332 RepoModel().grant_user_permission(
332 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
333 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
333 return data
334 return data
334
335
335 def _get_full_ref(self, backend, commit_id):
336 def _get_full_ref(self, backend, commit_id):
336 return 'branch:{branch}:{commit_id}'.format(
337 return 'branch:{branch}:{commit_id}'.format(
337 branch=backend.default_branch_name, commit_id=commit_id)
338 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,294 +1,295 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23 from mock import Mock, patch
23 from mock import Mock, patch
24
24
25 from rhodecode.api import utils
25 from rhodecode.api import utils
26 from rhodecode.api import JSONRPCError
26 from rhodecode.api import JSONRPCError
27 from rhodecode.lib.vcs.exceptions import RepositoryError
27 from rhodecode.lib.vcs.exceptions import RepositoryError
28
28
29
29
30 class TestGetCommitOrError(object):
30 class TestGetCommitOrError(object):
31 def setup(self):
31 def setup(self):
32 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
32 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
33
33
34 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d', 'branch:name'])
34 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d', 'branch:name'])
35 def test_ref_cannot_be_parsed(self, ref):
35 def test_ref_cannot_be_parsed(self, ref):
36 repo = Mock()
36 repo = Mock()
37 with pytest.raises(JSONRPCError) as excinfo:
37 with pytest.raises(JSONRPCError) as excinfo:
38 utils.get_commit_or_error(ref, repo)
38 utils.get_commit_or_error(ref, repo)
39 expected_message = (
39 expected_message = (
40 'Ref `{ref}` given in a wrong format. Please check the API'
40 'Ref `{ref}` given in a wrong format. Please check the API'
41 ' documentation for more details'.format(ref=ref)
41 ' documentation for more details'.format(ref=ref)
42 )
42 )
43 assert excinfo.value.message == expected_message
43 assert excinfo.value.message == expected_message
44
44
45 def test_success_with_hash_specified(self):
45 def test_success_with_hash_specified(self):
46 repo = Mock()
46 repo = Mock()
47 ref_type = 'branch'
47 ref_type = 'branch'
48 ref = '{}:master:{}'.format(ref_type, self.commit_hash)
48 ref = '{}:master:{}'.format(ref_type, self.commit_hash)
49
49
50 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
50 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
51 result = utils.get_commit_or_error(ref, repo)
51 result = utils.get_commit_or_error(ref, repo)
52 get_commit.assert_called_once_with(
52 get_commit.assert_called_once_with(
53 repo, self.commit_hash)
53 repo, self.commit_hash)
54 assert result == get_commit()
54 assert result == get_commit()
55
55
56 def test_raises_an_error_when_commit_not_found(self):
56 def test_raises_an_error_when_commit_not_found(self):
57 repo = Mock()
57 repo = Mock()
58 ref = 'branch:master:{}'.format(self.commit_hash)
58 ref = 'branch:master:{}'.format(self.commit_hash)
59
59
60 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
60 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
61 get_commit.side_effect = RepositoryError('Commit not found')
61 get_commit.side_effect = RepositoryError('Commit not found')
62 with pytest.raises(JSONRPCError) as excinfo:
62 with pytest.raises(JSONRPCError) as excinfo:
63 utils.get_commit_or_error(ref, repo)
63 utils.get_commit_or_error(ref, repo)
64 expected_message = 'Ref `{}` does not exist'.format(ref)
64 expected_message = 'Ref `{}` does not exist'.format(ref)
65 assert excinfo.value.message == expected_message
65 assert excinfo.value.message == expected_message
66
66
67
67
68 class TestResolveRefOrError(object):
68 class TestResolveRefOrError(object):
69 def setup(self):
69 def setup(self):
70 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
70 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
71
71
72 def test_success_with_no_hash_specified(self):
72 def test_success_with_no_hash_specified(self):
73 repo = Mock()
73 repo = Mock()
74 ref_type = 'branch'
74 ref_type = 'branch'
75 ref_name = 'master'
75 ref_name = 'master'
76 ref = '{}:{}'.format(ref_type, ref_name)
76 ref = '{}:{}'.format(ref_type, ref_name)
77
77
78 with patch('rhodecode.api.utils._get_ref_hash') \
78 with patch('rhodecode.api.utils._get_ref_hash') \
79 as _get_ref_hash:
79 as _get_ref_hash:
80 _get_ref_hash.return_value = self.commit_hash
80 _get_ref_hash.return_value = self.commit_hash
81 result = utils.resolve_ref_or_error(ref, repo)
81 result = utils.resolve_ref_or_error(ref, repo)
82 _get_ref_hash.assert_called_once_with(repo, ref_type, ref_name)
82 _get_ref_hash.assert_called_once_with(repo, ref_type, ref_name)
83 assert result == '{}:{}'.format(ref, self.commit_hash)
83 assert result == '{}:{}'.format(ref, self.commit_hash)
84
84
85 def test_non_supported_refs(self):
85 def test_non_supported_refs(self):
86 repo = Mock()
86 repo = Mock()
87 ref = 'ancestor:ref'
87 ref = 'ancestor:ref'
88 with pytest.raises(JSONRPCError) as excinfo:
88 with pytest.raises(JSONRPCError) as excinfo:
89 utils.resolve_ref_or_error(ref, repo)
89 utils.resolve_ref_or_error(ref, repo)
90 expected_message = 'The specified ancestor `ref` does not exist'
90 expected_message = (
91 'The specified value:ancestor:`ref` does not exist, or is not allowed.')
91 assert excinfo.value.message == expected_message
92 assert excinfo.value.message == expected_message
92
93
93 def test_branch_is_not_found(self):
94 def test_branch_is_not_found(self):
94 repo = Mock()
95 repo = Mock()
95 ref = 'branch:non-existing-one'
96 ref = 'branch:non-existing-one'
96 with patch('rhodecode.api.utils._get_ref_hash')\
97 with patch('rhodecode.api.utils._get_ref_hash')\
97 as _get_ref_hash:
98 as _get_ref_hash:
98 _get_ref_hash.side_effect = KeyError()
99 _get_ref_hash.side_effect = KeyError()
99 with pytest.raises(JSONRPCError) as excinfo:
100 with pytest.raises(JSONRPCError) as excinfo:
100 utils.resolve_ref_or_error(ref, repo)
101 utils.resolve_ref_or_error(ref, repo)
101 expected_message = (
102 expected_message = (
102 'The specified branch `non-existing-one` does not exist')
103 'The specified value:branch:`non-existing-one` does not exist, or is not allowed.')
103 assert excinfo.value.message == expected_message
104 assert excinfo.value.message == expected_message
104
105
105 def test_bookmark_is_not_found(self):
106 def test_bookmark_is_not_found(self):
106 repo = Mock()
107 repo = Mock()
107 ref = 'bookmark:non-existing-one'
108 ref = 'bookmark:non-existing-one'
108 with patch('rhodecode.api.utils._get_ref_hash')\
109 with patch('rhodecode.api.utils._get_ref_hash')\
109 as _get_ref_hash:
110 as _get_ref_hash:
110 _get_ref_hash.side_effect = KeyError()
111 _get_ref_hash.side_effect = KeyError()
111 with pytest.raises(JSONRPCError) as excinfo:
112 with pytest.raises(JSONRPCError) as excinfo:
112 utils.resolve_ref_or_error(ref, repo)
113 utils.resolve_ref_or_error(ref, repo)
113 expected_message = (
114 expected_message = (
114 'The specified bookmark `non-existing-one` does not exist')
115 'The specified value:bookmark:`non-existing-one` does not exist, or is not allowed.')
115 assert excinfo.value.message == expected_message
116 assert excinfo.value.message == expected_message
116
117
117 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
118 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
118 def test_ref_cannot_be_parsed(self, ref):
119 def test_ref_cannot_be_parsed(self, ref):
119 repo = Mock()
120 repo = Mock()
120 with pytest.raises(JSONRPCError) as excinfo:
121 with pytest.raises(JSONRPCError) as excinfo:
121 utils.resolve_ref_or_error(ref, repo)
122 utils.resolve_ref_or_error(ref, repo)
122 expected_message = (
123 expected_message = (
123 'Ref `{ref}` given in a wrong format. Please check the API'
124 'Ref `{ref}` given in a wrong format. Please check the API'
124 ' documentation for more details'.format(ref=ref)
125 ' documentation for more details'.format(ref=ref)
125 )
126 )
126 assert excinfo.value.message == expected_message
127 assert excinfo.value.message == expected_message
127
128
128
129
129 class TestGetRefHash(object):
130 class TestGetRefHash(object):
130 def setup(self):
131 def setup(self):
131 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
132 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
132 self.bookmark_name = 'test-bookmark'
133 self.bookmark_name = 'test-bookmark'
133
134
134 @pytest.mark.parametrize("alias, branch_name", [
135 @pytest.mark.parametrize("alias, branch_name", [
135 ("git", "master"),
136 ("git", "master"),
136 ("hg", "default")
137 ("hg", "default")
137 ])
138 ])
138 def test_returns_hash_by_branch_name(self, alias, branch_name):
139 def test_returns_hash_by_branch_name(self, alias, branch_name):
139 with patch('rhodecode.model.db.Repository') as repo:
140 with patch('rhodecode.model.db.Repository') as repo:
140 repo.scm_instance().alias = alias
141 repo.scm_instance().alias = alias
141 repo.scm_instance().branches = {branch_name: self.commit_hash}
142 repo.scm_instance().branches = {branch_name: self.commit_hash}
142 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
143 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
143 assert result_hash == self.commit_hash
144 assert result_hash == self.commit_hash
144
145
145 @pytest.mark.parametrize("alias, branch_name", [
146 @pytest.mark.parametrize("alias, branch_name", [
146 ("git", "master"),
147 ("git", "master"),
147 ("hg", "default")
148 ("hg", "default")
148 ])
149 ])
149 def test_raises_error_when_branch_is_not_found(self, alias, branch_name):
150 def test_raises_error_when_branch_is_not_found(self, alias, branch_name):
150 with patch('rhodecode.model.db.Repository') as repo:
151 with patch('rhodecode.model.db.Repository') as repo:
151 repo.scm_instance().alias = alias
152 repo.scm_instance().alias = alias
152 repo.scm_instance().branches = {}
153 repo.scm_instance().branches = {}
153 with pytest.raises(KeyError):
154 with pytest.raises(KeyError):
154 utils._get_ref_hash(repo, 'branch', branch_name)
155 utils._get_ref_hash(repo, 'branch', branch_name)
155
156
156 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
157 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
157 with patch('rhodecode.model.db.Repository') as repo:
158 with patch('rhodecode.model.db.Repository') as repo:
158 repo.scm_instance().alias = 'hg'
159 repo.scm_instance().alias = 'hg'
159 repo.scm_instance().bookmarks = {
160 repo.scm_instance().bookmarks = {
160 self.bookmark_name: self.commit_hash}
161 self.bookmark_name: self.commit_hash}
161 result_hash = utils._get_ref_hash(
162 result_hash = utils._get_ref_hash(
162 repo, 'bookmark', self.bookmark_name)
163 repo, 'bookmark', self.bookmark_name)
163 assert result_hash == self.commit_hash
164 assert result_hash == self.commit_hash
164
165
165 def test_raises_error_when_bookmark_is_not_found_in_hg_repo(self):
166 def test_raises_error_when_bookmark_is_not_found_in_hg_repo(self):
166 with patch('rhodecode.model.db.Repository') as repo:
167 with patch('rhodecode.model.db.Repository') as repo:
167 repo.scm_instance().alias = 'hg'
168 repo.scm_instance().alias = 'hg'
168 repo.scm_instance().bookmarks = {}
169 repo.scm_instance().bookmarks = {}
169 with pytest.raises(KeyError):
170 with pytest.raises(KeyError):
170 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
171 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
171
172
172 def test_raises_error_when_bookmark_is_specified_for_git(self):
173 def test_raises_error_when_bookmark_is_specified_for_git(self):
173 with patch('rhodecode.model.db.Repository') as repo:
174 with patch('rhodecode.model.db.Repository') as repo:
174 repo.scm_instance().alias = 'git'
175 repo.scm_instance().alias = 'git'
175 repo.scm_instance().bookmarks = {
176 repo.scm_instance().bookmarks = {
176 self.bookmark_name: self.commit_hash}
177 self.bookmark_name: self.commit_hash}
177 with pytest.raises(ValueError):
178 with pytest.raises(ValueError):
178 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
179 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
179
180
180
181
181 class TestUserByNameOrError(object):
182 class TestUserByNameOrError(object):
182 def test_user_found_by_id(self):
183 def test_user_found_by_id(self):
183 fake_user = Mock(id=123)
184 fake_user = Mock(id=123)
184
185
185 patcher = patch('rhodecode.model.user.UserModel.get_user')
186 patcher = patch('rhodecode.model.user.UserModel.get_user')
186 with patcher as get_user:
187 with patcher as get_user:
187 get_user.return_value = fake_user
188 get_user.return_value = fake_user
188
189
189 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
190 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
190 with patcher as get_by_username:
191 with patcher as get_by_username:
191 result = utils.get_user_or_error(123)
192 result = utils.get_user_or_error(123)
192 assert result == fake_user
193 assert result == fake_user
193
194
194 def test_user_not_found_by_id_as_str(self):
195 def test_user_not_found_by_id_as_str(self):
195 fake_user = Mock(id=123)
196 fake_user = Mock(id=123)
196
197
197 patcher = patch('rhodecode.model.user.UserModel.get_user')
198 patcher = patch('rhodecode.model.user.UserModel.get_user')
198 with patcher as get_user:
199 with patcher as get_user:
199 get_user.return_value = fake_user
200 get_user.return_value = fake_user
200 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
201 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
201 with patcher as get_by_username:
202 with patcher as get_by_username:
202 get_by_username.return_value = None
203 get_by_username.return_value = None
203
204
204 with pytest.raises(JSONRPCError):
205 with pytest.raises(JSONRPCError):
205 utils.get_user_or_error('123')
206 utils.get_user_or_error('123')
206
207
207 def test_user_found_by_name(self):
208 def test_user_found_by_name(self):
208 fake_user = Mock(id=123)
209 fake_user = Mock(id=123)
209
210
210 patcher = patch('rhodecode.model.user.UserModel.get_user')
211 patcher = patch('rhodecode.model.user.UserModel.get_user')
211 with patcher as get_user:
212 with patcher as get_user:
212 get_user.return_value = None
213 get_user.return_value = None
213
214
214 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
215 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
215 with patcher as get_by_username:
216 with patcher as get_by_username:
216 get_by_username.return_value = fake_user
217 get_by_username.return_value = fake_user
217
218
218 result = utils.get_user_or_error('test')
219 result = utils.get_user_or_error('test')
219 assert result == fake_user
220 assert result == fake_user
220
221
221 def test_user_not_found_by_id(self):
222 def test_user_not_found_by_id(self):
222 patcher = patch('rhodecode.model.user.UserModel.get_user')
223 patcher = patch('rhodecode.model.user.UserModel.get_user')
223 with patcher as get_user:
224 with patcher as get_user:
224 get_user.return_value = None
225 get_user.return_value = None
225 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
226 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
226 with patcher as get_by_username:
227 with patcher as get_by_username:
227 get_by_username.return_value = None
228 get_by_username.return_value = None
228
229
229 with pytest.raises(JSONRPCError) as excinfo:
230 with pytest.raises(JSONRPCError) as excinfo:
230 utils.get_user_or_error(123)
231 utils.get_user_or_error(123)
231
232
232 expected_message = 'user `123` does not exist'
233 expected_message = 'user `123` does not exist'
233 assert excinfo.value.message == expected_message
234 assert excinfo.value.message == expected_message
234
235
235 def test_user_not_found_by_name(self):
236 def test_user_not_found_by_name(self):
236 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
237 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
237 with patcher as get_by_username:
238 with patcher as get_by_username:
238 get_by_username.return_value = None
239 get_by_username.return_value = None
239 with pytest.raises(JSONRPCError) as excinfo:
240 with pytest.raises(JSONRPCError) as excinfo:
240 utils.get_user_or_error('test')
241 utils.get_user_or_error('test')
241
242
242 expected_message = 'user `test` does not exist'
243 expected_message = 'user `test` does not exist'
243 assert excinfo.value.message == expected_message
244 assert excinfo.value.message == expected_message
244
245
245
246
246 class TestGetCommitDict(object):
247 class TestGetCommitDict(object):
247 @pytest.mark.parametrize('filename, expected', [
248 @pytest.mark.parametrize('filename, expected', [
248 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
249 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
249 (b'sp\xa4cial', u'sp\ufffdcial'),
250 (b'sp\xa4cial', u'sp\ufffdcial'),
250 ])
251 ])
251 def test_decodes_filenames_to_unicode(self, filename, expected):
252 def test_decodes_filenames_to_unicode(self, filename, expected):
252 result = utils._get_commit_dict(filename=filename, op='A')
253 result = utils._get_commit_dict(filename=filename, op='A')
253 assert result['filename'] == expected
254 assert result['filename'] == expected
254
255
255
256
256 class TestRepoAccess(object):
257 class TestRepoAccess(object):
257 def setup_method(self, method):
258 def setup_method(self, method):
258
259
259 self.admin_perm_patch = patch(
260 self.admin_perm_patch = patch(
260 'rhodecode.api.utils.HasPermissionAnyApi')
261 'rhodecode.api.utils.HasPermissionAnyApi')
261 self.repo_perm_patch = patch(
262 self.repo_perm_patch = patch(
262 'rhodecode.api.utils.HasRepoPermissionAnyApi')
263 'rhodecode.api.utils.HasRepoPermissionAnyApi')
263
264
264 def test_has_superadmin_permission_checks_for_admin(self):
265 def test_has_superadmin_permission_checks_for_admin(self):
265 admin_mock = Mock()
266 admin_mock = Mock()
266 with self.admin_perm_patch as amock:
267 with self.admin_perm_patch as amock:
267 amock.return_value = admin_mock
268 amock.return_value = admin_mock
268 assert utils.has_superadmin_permission('fake_user')
269 assert utils.has_superadmin_permission('fake_user')
269 amock.assert_called_once_with('hg.admin')
270 amock.assert_called_once_with('hg.admin')
270
271
271 admin_mock.assert_called_once_with(user='fake_user')
272 admin_mock.assert_called_once_with(user='fake_user')
272
273
273 def test_has_repo_permissions_checks_for_repo_access(self):
274 def test_has_repo_permissions_checks_for_repo_access(self):
274 repo_mock = Mock()
275 repo_mock = Mock()
275 fake_repo = Mock()
276 fake_repo = Mock()
276 with self.repo_perm_patch as rmock:
277 with self.repo_perm_patch as rmock:
277 rmock.return_value = repo_mock
278 rmock.return_value = repo_mock
278 assert utils.validate_repo_permissions(
279 assert utils.validate_repo_permissions(
279 'fake_user', 'fake_repo_id', fake_repo,
280 'fake_user', 'fake_repo_id', fake_repo,
280 ['perm1', 'perm2'])
281 ['perm1', 'perm2'])
281 rmock.assert_called_once_with(*['perm1', 'perm2'])
282 rmock.assert_called_once_with(*['perm1', 'perm2'])
282
283
283 repo_mock.assert_called_once_with(
284 repo_mock.assert_called_once_with(
284 user='fake_user', repo_name=fake_repo.repo_name)
285 user='fake_user', repo_name=fake_repo.repo_name)
285
286
286 def test_has_repo_permissions_raises_not_found(self):
287 def test_has_repo_permissions_raises_not_found(self):
287 repo_mock = Mock(return_value=False)
288 repo_mock = Mock(return_value=False)
288 fake_repo = Mock()
289 fake_repo = Mock()
289 with self.repo_perm_patch as rmock:
290 with self.repo_perm_patch as rmock:
290 rmock.return_value = repo_mock
291 rmock.return_value = repo_mock
291 with pytest.raises(JSONRPCError) as excinfo:
292 with pytest.raises(JSONRPCError) as excinfo:
292 utils.validate_repo_permissions(
293 utils.validate_repo_permissions(
293 'fake_user', 'fake_repo_id', fake_repo, 'perms')
294 'fake_user', 'fake_repo_id', fake_repo, 'perms')
294 assert 'fake_repo_id' in excinfo
295 assert 'fake_repo_id' in excinfo
@@ -1,442 +1,441 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 JSON RPC utils
22 JSON RPC utils
23 """
23 """
24
24
25 import collections
25 import collections
26 import logging
26 import logging
27
27
28 from rhodecode.api.exc import JSONRPCError
28 from rhodecode.api.exc import JSONRPCError
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
31 from rhodecode.lib.utils import safe_unicode
31 from rhodecode.lib.utils import safe_unicode
32 from rhodecode.lib.vcs.exceptions import RepositoryError
32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.controllers.utils import get_commit_from_ref_name
33 from rhodecode.controllers.utils import get_commit_from_ref_name
34 from rhodecode.lib.utils2 import str2bool
34 from rhodecode.lib.utils2 import str2bool
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class OAttr(object):
39 class OAttr(object):
40 """
40 """
41 Special Option that defines other attribute, and can default to them
41 Special Option that defines other attribute, and can default to them
42
42
43 Example::
43 Example::
44
44
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
46 user = Optional.extract(userid, evaluate_locals=local())
46 user = Optional.extract(userid, evaluate_locals=local())
47 #if we pass in userid, we get it, else it will default to apiuser
47 #if we pass in userid, we get it, else it will default to apiuser
48 #attribute
48 #attribute
49 """
49 """
50
50
51 def __init__(self, attr_name):
51 def __init__(self, attr_name):
52 self.attr_name = attr_name
52 self.attr_name = attr_name
53
53
54 def __repr__(self):
54 def __repr__(self):
55 return '<OptionalAttr:%s>' % self.attr_name
55 return '<OptionalAttr:%s>' % self.attr_name
56
56
57 def __call__(self):
57 def __call__(self):
58 return self
58 return self
59
59
60
60
61 class Optional(object):
61 class Optional(object):
62 """
62 """
63 Defines an optional parameter::
63 Defines an optional parameter::
64
64
65 param = param.getval() if isinstance(param, Optional) else param
65 param = param.getval() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
67
67
68 is equivalent of::
68 is equivalent of::
69
69
70 param = Optional.extract(param)
70 param = Optional.extract(param)
71
71
72 """
72 """
73
73
74 def __init__(self, type_):
74 def __init__(self, type_):
75 self.type_ = type_
75 self.type_ = type_
76
76
77 def __repr__(self):
77 def __repr__(self):
78 return '<Optional:%s>' % self.type_.__repr__()
78 return '<Optional:%s>' % self.type_.__repr__()
79
79
80 def __call__(self):
80 def __call__(self):
81 return self.getval()
81 return self.getval()
82
82
83 def getval(self, evaluate_locals=None):
83 def getval(self, evaluate_locals=None):
84 """
84 """
85 returns value from this Optional instance
85 returns value from this Optional instance
86 """
86 """
87 if isinstance(self.type_, OAttr):
87 if isinstance(self.type_, OAttr):
88 param_name = self.type_.attr_name
88 param_name = self.type_.attr_name
89 if evaluate_locals:
89 if evaluate_locals:
90 return evaluate_locals[param_name]
90 return evaluate_locals[param_name]
91 # use params name
91 # use params name
92 return param_name
92 return param_name
93 return self.type_
93 return self.type_
94
94
95 @classmethod
95 @classmethod
96 def extract(cls, val, evaluate_locals=None, binary=None):
96 def extract(cls, val, evaluate_locals=None, binary=None):
97 """
97 """
98 Extracts value from Optional() instance
98 Extracts value from Optional() instance
99
99
100 :param val:
100 :param val:
101 :return: original value if it's not Optional instance else
101 :return: original value if it's not Optional instance else
102 value of instance
102 value of instance
103 """
103 """
104 if isinstance(val, cls):
104 if isinstance(val, cls):
105 val = val.getval(evaluate_locals)
105 val = val.getval(evaluate_locals)
106
106
107 if binary:
107 if binary:
108 val = str2bool(val)
108 val = str2bool(val)
109
109
110 return val
110 return val
111
111
112
112
113 def parse_args(cli_args, key_prefix=''):
113 def parse_args(cli_args, key_prefix=''):
114 from rhodecode.lib.utils2 import (escape_split)
114 from rhodecode.lib.utils2 import (escape_split)
115 kwargs = collections.defaultdict(dict)
115 kwargs = collections.defaultdict(dict)
116 for el in escape_split(cli_args, ','):
116 for el in escape_split(cli_args, ','):
117 kv = escape_split(el, '=', 1)
117 kv = escape_split(el, '=', 1)
118 if len(kv) == 2:
118 if len(kv) == 2:
119 k, v = kv
119 k, v = kv
120 kwargs[key_prefix + k] = v
120 kwargs[key_prefix + k] = v
121 return kwargs
121 return kwargs
122
122
123
123
124 def get_origin(obj):
124 def get_origin(obj):
125 """
125 """
126 Get origin of permission from object.
126 Get origin of permission from object.
127
127
128 :param obj:
128 :param obj:
129 """
129 """
130 origin = 'permission'
130 origin = 'permission'
131
131
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
133 # admin and owner case, maybe we should use dual string ?
133 # admin and owner case, maybe we should use dual string ?
134 origin = 'owner'
134 origin = 'owner'
135 elif getattr(obj, 'owner_row', ''):
135 elif getattr(obj, 'owner_row', ''):
136 origin = 'owner'
136 origin = 'owner'
137 elif getattr(obj, 'admin_row', ''):
137 elif getattr(obj, 'admin_row', ''):
138 origin = 'super-admin'
138 origin = 'super-admin'
139 return origin
139 return origin
140
140
141
141
142 def store_update(updates, attr, name):
142 def store_update(updates, attr, name):
143 """
143 """
144 Stores param in updates dict if it's not instance of Optional
144 Stores param in updates dict if it's not instance of Optional
145 allows easy updates of passed in params
145 allows easy updates of passed in params
146 """
146 """
147 if not isinstance(attr, Optional):
147 if not isinstance(attr, Optional):
148 updates[name] = attr
148 updates[name] = attr
149
149
150
150
151 def has_superadmin_permission(apiuser):
151 def has_superadmin_permission(apiuser):
152 """
152 """
153 Return True if apiuser is admin or return False
153 Return True if apiuser is admin or return False
154
154
155 :param apiuser:
155 :param apiuser:
156 """
156 """
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
158 return True
158 return True
159 return False
159 return False
160
160
161
161
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
163 """
163 """
164 Raise JsonRPCError if apiuser is not authorized or return True
164 Raise JsonRPCError if apiuser is not authorized or return True
165
165
166 :param apiuser:
166 :param apiuser:
167 :param repoid:
167 :param repoid:
168 :param repo:
168 :param repo:
169 :param perms:
169 :param perms:
170 """
170 """
171 if not HasRepoPermissionAnyApi(*perms)(
171 if not HasRepoPermissionAnyApi(*perms)(
172 user=apiuser, repo_name=repo.repo_name):
172 user=apiuser, repo_name=repo.repo_name):
173 raise JSONRPCError(
173 raise JSONRPCError(
174 'repository `%s` does not exist' % repoid)
174 'repository `%s` does not exist' % repoid)
175
175
176 return True
176 return True
177
177
178
178
179 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
179 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
180 """
180 """
181 Raise JsonRPCError if apiuser is not authorized or return True
181 Raise JsonRPCError if apiuser is not authorized or return True
182
182
183 :param apiuser:
183 :param apiuser:
184 :param repogroupid: just the id of repository group
184 :param repogroupid: just the id of repository group
185 :param repo_group: instance of repo_group
185 :param repo_group: instance of repo_group
186 :param perms:
186 :param perms:
187 """
187 """
188 if not HasRepoGroupPermissionAnyApi(*perms)(
188 if not HasRepoGroupPermissionAnyApi(*perms)(
189 user=apiuser, group_name=repo_group.group_name):
189 user=apiuser, group_name=repo_group.group_name):
190 raise JSONRPCError(
190 raise JSONRPCError(
191 'repository group `%s` does not exist' % repogroupid)
191 'repository group `%s` does not exist' % repogroupid)
192
192
193 return True
193 return True
194
194
195
195
196 def validate_set_owner_permissions(apiuser, owner):
196 def validate_set_owner_permissions(apiuser, owner):
197 if isinstance(owner, Optional):
197 if isinstance(owner, Optional):
198 owner = get_user_or_error(apiuser.user_id)
198 owner = get_user_or_error(apiuser.user_id)
199 else:
199 else:
200 if has_superadmin_permission(apiuser):
200 if has_superadmin_permission(apiuser):
201 owner = get_user_or_error(owner)
201 owner = get_user_or_error(owner)
202 else:
202 else:
203 # forbid setting owner for non-admins
203 # forbid setting owner for non-admins
204 raise JSONRPCError(
204 raise JSONRPCError(
205 'Only RhodeCode super-admin can specify `owner` param')
205 'Only RhodeCode super-admin can specify `owner` param')
206 return owner
206 return owner
207
207
208
208
209 def get_user_or_error(userid):
209 def get_user_or_error(userid):
210 """
210 """
211 Get user by id or name or return JsonRPCError if not found
211 Get user by id or name or return JsonRPCError if not found
212
212
213 :param userid:
213 :param userid:
214 """
214 """
215 from rhodecode.model.user import UserModel
215 from rhodecode.model.user import UserModel
216 user_model = UserModel()
216 user_model = UserModel()
217
217
218 if isinstance(userid, (int, long)):
218 if isinstance(userid, (int, long)):
219 try:
219 try:
220 user = user_model.get_user(userid)
220 user = user_model.get_user(userid)
221 except ValueError:
221 except ValueError:
222 user = None
222 user = None
223 else:
223 else:
224 user = user_model.get_by_username(userid)
224 user = user_model.get_by_username(userid)
225
225
226 if user is None:
226 if user is None:
227 raise JSONRPCError(
227 raise JSONRPCError(
228 'user `%s` does not exist' % (userid,))
228 'user `%s` does not exist' % (userid,))
229 return user
229 return user
230
230
231
231
232 def get_repo_or_error(repoid):
232 def get_repo_or_error(repoid):
233 """
233 """
234 Get repo by id or name or return JsonRPCError if not found
234 Get repo by id or name or return JsonRPCError if not found
235
235
236 :param repoid:
236 :param repoid:
237 """
237 """
238 from rhodecode.model.repo import RepoModel
238 from rhodecode.model.repo import RepoModel
239 repo_model = RepoModel()
239 repo_model = RepoModel()
240
240
241 if isinstance(repoid, (int, long)):
241 if isinstance(repoid, (int, long)):
242 try:
242 try:
243 repo = repo_model.get_repo(repoid)
243 repo = repo_model.get_repo(repoid)
244 except ValueError:
244 except ValueError:
245 repo = None
245 repo = None
246 else:
246 else:
247 repo = repo_model.get_by_repo_name(repoid)
247 repo = repo_model.get_by_repo_name(repoid)
248
248
249 if repo is None:
249 if repo is None:
250 raise JSONRPCError(
250 raise JSONRPCError(
251 'repository `%s` does not exist' % (repoid,))
251 'repository `%s` does not exist' % (repoid,))
252 return repo
252 return repo
253
253
254
254
255 def get_repo_group_or_error(repogroupid):
255 def get_repo_group_or_error(repogroupid):
256 """
256 """
257 Get repo group by id or name or return JsonRPCError if not found
257 Get repo group by id or name or return JsonRPCError if not found
258
258
259 :param repogroupid:
259 :param repogroupid:
260 """
260 """
261 from rhodecode.model.repo_group import RepoGroupModel
261 from rhodecode.model.repo_group import RepoGroupModel
262 repo_group_model = RepoGroupModel()
262 repo_group_model = RepoGroupModel()
263
263
264 if isinstance(repogroupid, (int, long)):
264 if isinstance(repogroupid, (int, long)):
265 try:
265 try:
266 repo_group = repo_group_model._get_repo_group(repogroupid)
266 repo_group = repo_group_model._get_repo_group(repogroupid)
267 except ValueError:
267 except ValueError:
268 repo_group = None
268 repo_group = None
269 else:
269 else:
270 repo_group = repo_group_model.get_by_group_name(repogroupid)
270 repo_group = repo_group_model.get_by_group_name(repogroupid)
271
271
272 if repo_group is None:
272 if repo_group is None:
273 raise JSONRPCError(
273 raise JSONRPCError(
274 'repository group `%s` does not exist' % (repogroupid,))
274 'repository group `%s` does not exist' % (repogroupid,))
275 return repo_group
275 return repo_group
276
276
277
277
278 def get_user_group_or_error(usergroupid):
278 def get_user_group_or_error(usergroupid):
279 """
279 """
280 Get user group by id or name or return JsonRPCError if not found
280 Get user group by id or name or return JsonRPCError if not found
281
281
282 :param usergroupid:
282 :param usergroupid:
283 """
283 """
284 from rhodecode.model.user_group import UserGroupModel
284 from rhodecode.model.user_group import UserGroupModel
285 user_group_model = UserGroupModel()
285 user_group_model = UserGroupModel()
286
286
287 if isinstance(usergroupid, (int, long)):
287 if isinstance(usergroupid, (int, long)):
288 try:
288 try:
289 user_group = user_group_model.get_group(usergroupid)
289 user_group = user_group_model.get_group(usergroupid)
290 except ValueError:
290 except ValueError:
291 user_group = None
291 user_group = None
292 else:
292 else:
293 user_group = user_group_model.get_by_name(usergroupid)
293 user_group = user_group_model.get_by_name(usergroupid)
294
294
295 if user_group is None:
295 if user_group is None:
296 raise JSONRPCError(
296 raise JSONRPCError(
297 'user group `%s` does not exist' % (usergroupid,))
297 'user group `%s` does not exist' % (usergroupid,))
298 return user_group
298 return user_group
299
299
300
300
301 def get_perm_or_error(permid, prefix=None):
301 def get_perm_or_error(permid, prefix=None):
302 """
302 """
303 Get permission by id or name or return JsonRPCError if not found
303 Get permission by id or name or return JsonRPCError if not found
304
304
305 :param permid:
305 :param permid:
306 """
306 """
307 from rhodecode.model.permission import PermissionModel
307 from rhodecode.model.permission import PermissionModel
308
308
309 perm = PermissionModel.cls.get_by_key(permid)
309 perm = PermissionModel.cls.get_by_key(permid)
310 if perm is None:
310 if perm is None:
311 raise JSONRPCError('permission `%s` does not exist' % (permid,))
311 raise JSONRPCError('permission `%s` does not exist' % (permid,))
312 if prefix:
312 if prefix:
313 if not perm.permission_name.startswith(prefix):
313 if not perm.permission_name.startswith(prefix):
314 raise JSONRPCError('permission `%s` is invalid, '
314 raise JSONRPCError('permission `%s` is invalid, '
315 'should start with %s' % (permid, prefix))
315 'should start with %s' % (permid, prefix))
316 return perm
316 return perm
317
317
318
318
319 def get_gist_or_error(gistid):
319 def get_gist_or_error(gistid):
320 """
320 """
321 Get gist by id or gist_access_id or return JsonRPCError if not found
321 Get gist by id or gist_access_id or return JsonRPCError if not found
322
322
323 :param gistid:
323 :param gistid:
324 """
324 """
325 from rhodecode.model.gist import GistModel
325 from rhodecode.model.gist import GistModel
326
326
327 gist = GistModel.cls.get_by_access_id(gistid)
327 gist = GistModel.cls.get_by_access_id(gistid)
328 if gist is None:
328 if gist is None:
329 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
329 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
330 return gist
330 return gist
331
331
332
332
333 def get_pull_request_or_error(pullrequestid):
333 def get_pull_request_or_error(pullrequestid):
334 """
334 """
335 Get pull request by id or return JsonRPCError if not found
335 Get pull request by id or return JsonRPCError if not found
336
336
337 :param pullrequestid:
337 :param pullrequestid:
338 """
338 """
339 from rhodecode.model.pull_request import PullRequestModel
339 from rhodecode.model.pull_request import PullRequestModel
340
340
341 try:
341 try:
342 pull_request = PullRequestModel().get(int(pullrequestid))
342 pull_request = PullRequestModel().get(int(pullrequestid))
343 except ValueError:
343 except ValueError:
344 raise JSONRPCError('pullrequestid must be an integer')
344 raise JSONRPCError('pullrequestid must be an integer')
345 if not pull_request:
345 if not pull_request:
346 raise JSONRPCError('pull request `%s` does not exist' % (
346 raise JSONRPCError('pull request `%s` does not exist' % (
347 pullrequestid,))
347 pullrequestid,))
348 return pull_request
348 return pull_request
349
349
350
350
351 def build_commit_data(commit, detail_level):
351 def build_commit_data(commit, detail_level):
352 parsed_diff = []
352 parsed_diff = []
353 if detail_level == 'extended':
353 if detail_level == 'extended':
354 for f in commit.added:
354 for f in commit.added:
355 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
355 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
356 for f in commit.changed:
356 for f in commit.changed:
357 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
357 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
358 for f in commit.removed:
358 for f in commit.removed:
359 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
359 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
360
360
361 elif detail_level == 'full':
361 elif detail_level == 'full':
362 from rhodecode.lib.diffs import DiffProcessor
362 from rhodecode.lib.diffs import DiffProcessor
363 diff_processor = DiffProcessor(commit.diff())
363 diff_processor = DiffProcessor(commit.diff())
364 for dp in diff_processor.prepare():
364 for dp in diff_processor.prepare():
365 del dp['stats']['ops']
365 del dp['stats']['ops']
366 _stats = dp['stats']
366 _stats = dp['stats']
367 parsed_diff.append(_get_commit_dict(
367 parsed_diff.append(_get_commit_dict(
368 filename=dp['filename'], op=dp['operation'],
368 filename=dp['filename'], op=dp['operation'],
369 new_revision=dp['new_revision'],
369 new_revision=dp['new_revision'],
370 old_revision=dp['old_revision'],
370 old_revision=dp['old_revision'],
371 raw_diff=dp['raw_diff'], stats=_stats))
371 raw_diff=dp['raw_diff'], stats=_stats))
372
372
373 return parsed_diff
373 return parsed_diff
374
374
375
375
376 def get_commit_or_error(ref, repo):
376 def get_commit_or_error(ref, repo):
377 try:
377 try:
378 ref_type, _, ref_hash = ref.split(':')
378 ref_type, _, ref_hash = ref.split(':')
379 except ValueError:
379 except ValueError:
380 raise JSONRPCError(
380 raise JSONRPCError(
381 'Ref `{ref}` given in a wrong format. Please check the API'
381 'Ref `{ref}` given in a wrong format. Please check the API'
382 ' documentation for more details'.format(ref=ref))
382 ' documentation for more details'.format(ref=ref))
383 try:
383 try:
384 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
384 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
385 # once get_commit supports ref_types
385 # once get_commit supports ref_types
386 return get_commit_from_ref_name(repo, ref_hash)
386 return get_commit_from_ref_name(repo, ref_hash)
387 except RepositoryError:
387 except RepositoryError:
388 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
388 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
389
389
390
390
391 def resolve_ref_or_error(ref, repo):
391 def resolve_ref_or_error(ref, repo):
392 def _parse_ref(type_, name, hash_=None):
392 def _parse_ref(type_, name, hash_=None):
393 return type_, name, hash_
393 return type_, name, hash_
394
394
395 try:
395 try:
396 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
396 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
397 except TypeError:
397 except TypeError:
398 raise JSONRPCError(
398 raise JSONRPCError(
399 'Ref `{ref}` given in a wrong format. Please check the API'
399 'Ref `{ref}` given in a wrong format. Please check the API'
400 ' documentation for more details'.format(ref=ref))
400 ' documentation for more details'.format(ref=ref))
401
401
402 try:
402 try:
403 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
403 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
404 except (KeyError, ValueError):
404 except (KeyError, ValueError):
405 raise JSONRPCError(
405 raise JSONRPCError(
406 'The specified {type} `{name}` does not exist'.format(
406 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
407 type=ref_type, name=ref_name))
407 type=ref_type, name=ref_name))
408
408
409 return ':'.join([ref_type, ref_name, ref_hash])
409 return ':'.join([ref_type, ref_name, ref_hash])
410
410
411
411
412 def _get_commit_dict(
412 def _get_commit_dict(
413 filename, op, new_revision=None, old_revision=None,
413 filename, op, new_revision=None, old_revision=None,
414 raw_diff=None, stats=None):
414 raw_diff=None, stats=None):
415 if stats is None:
415 if stats is None:
416 stats = {
416 stats = {
417 "added": None,
417 "added": None,
418 "binary": None,
418 "binary": None,
419 "deleted": None
419 "deleted": None
420 }
420 }
421 return {
421 return {
422 "filename": safe_unicode(filename),
422 "filename": safe_unicode(filename),
423 "op": op,
423 "op": op,
424
424
425 # extra details
425 # extra details
426 "new_revision": new_revision,
426 "new_revision": new_revision,
427 "old_revision": old_revision,
427 "old_revision": old_revision,
428
428
429 "raw_diff": raw_diff,
429 "raw_diff": raw_diff,
430 "stats": stats
430 "stats": stats
431 }
431 }
432
432
433
433
434 # TODO: mikhail: Think about moving this function to some library
435 def _get_ref_hash(repo, type_, name):
434 def _get_ref_hash(repo, type_, name):
436 vcs_repo = repo.scm_instance()
435 vcs_repo = repo.scm_instance()
437 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
436 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
438 return vcs_repo.branches[name]
437 return vcs_repo.branches[name]
439 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
438 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
440 return vcs_repo.bookmarks[name]
439 return vcs_repo.bookmarks[name]
441 else:
440 else:
442 raise ValueError()
441 raise ValueError()
@@ -1,937 +1,937 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124
124
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if Optional.extract(repoid):
126 if Optional.extract(repoid):
127 repo = get_repo_or_error(repoid)
127 repo = get_repo_or_error(repoid)
128 else:
128 else:
129 repo = pull_request.target_repo
129 repo = pull_request.target_repo
130
130
131 if not PullRequestModel().check_user_read(
131 if not PullRequestModel().check_user_read(
132 pull_request, apiuser, api=True):
132 pull_request, apiuser, api=True):
133 raise JSONRPCError('repository `%s` or pull request `%s` '
133 raise JSONRPCError('repository `%s` or pull request `%s` '
134 'does not exist' % (repoid, pullrequestid))
134 'does not exist' % (repoid, pullrequestid))
135 data = pull_request.get_api_data()
135 data = pull_request.get_api_data()
136 return data
136 return data
137
137
138
138
139 @jsonrpc_method()
139 @jsonrpc_method()
140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
141 """
141 """
142 Get all pull requests from the repository specified in `repoid`.
142 Get all pull requests from the repository specified in `repoid`.
143
143
144 :param apiuser: This is filled automatically from the |authtoken|.
144 :param apiuser: This is filled automatically from the |authtoken|.
145 :type apiuser: AuthUser
145 :type apiuser: AuthUser
146 :param repoid: Optional repository name or repository ID.
146 :param repoid: Optional repository name or repository ID.
147 :type repoid: str or int
147 :type repoid: str or int
148 :param status: Only return pull requests with the specified status.
148 :param status: Only return pull requests with the specified status.
149 Valid options are.
149 Valid options are.
150 * ``new`` (default)
150 * ``new`` (default)
151 * ``open``
151 * ``open``
152 * ``closed``
152 * ``closed``
153 :type status: str
153 :type status: str
154
154
155 Example output:
155 Example output:
156
156
157 .. code-block:: bash
157 .. code-block:: bash
158
158
159 "id": <id_given_in_input>,
159 "id": <id_given_in_input>,
160 "result":
160 "result":
161 [
161 [
162 ...
162 ...
163 {
163 {
164 "pull_request_id": "<pull_request_id>",
164 "pull_request_id": "<pull_request_id>",
165 "url": "<url>",
165 "url": "<url>",
166 "title" : "<title>",
166 "title" : "<title>",
167 "description": "<description>",
167 "description": "<description>",
168 "status": "<status>",
168 "status": "<status>",
169 "created_on": "<date_time_created>",
169 "created_on": "<date_time_created>",
170 "updated_on": "<date_time_updated>",
170 "updated_on": "<date_time_updated>",
171 "commit_ids": [
171 "commit_ids": [
172 ...
172 ...
173 "<commit_id>",
173 "<commit_id>",
174 "<commit_id>",
174 "<commit_id>",
175 ...
175 ...
176 ],
176 ],
177 "review_status": "<review_status>",
177 "review_status": "<review_status>",
178 "mergeable": {
178 "mergeable": {
179 "status": "<bool>",
179 "status": "<bool>",
180 "message: "<message>",
180 "message: "<message>",
181 },
181 },
182 "source": {
182 "source": {
183 "clone_url": "<clone_url>",
183 "clone_url": "<clone_url>",
184 "reference":
184 "reference":
185 {
185 {
186 "name": "<name>",
186 "name": "<name>",
187 "type": "<type>",
187 "type": "<type>",
188 "commit_id": "<commit_id>",
188 "commit_id": "<commit_id>",
189 }
189 }
190 },
190 },
191 "target": {
191 "target": {
192 "clone_url": "<clone_url>",
192 "clone_url": "<clone_url>",
193 "reference":
193 "reference":
194 {
194 {
195 "name": "<name>",
195 "name": "<name>",
196 "type": "<type>",
196 "type": "<type>",
197 "commit_id": "<commit_id>",
197 "commit_id": "<commit_id>",
198 }
198 }
199 },
199 },
200 "merge": {
200 "merge": {
201 "clone_url": "<clone_url>",
201 "clone_url": "<clone_url>",
202 "reference":
202 "reference":
203 {
203 {
204 "name": "<name>",
204 "name": "<name>",
205 "type": "<type>",
205 "type": "<type>",
206 "commit_id": "<commit_id>",
206 "commit_id": "<commit_id>",
207 }
207 }
208 },
208 },
209 "author": <user_obj>,
209 "author": <user_obj>,
210 "reviewers": [
210 "reviewers": [
211 ...
211 ...
212 {
212 {
213 "user": "<user_obj>",
213 "user": "<user_obj>",
214 "review_status": "<review_status>",
214 "review_status": "<review_status>",
215 }
215 }
216 ...
216 ...
217 ]
217 ]
218 }
218 }
219 ...
219 ...
220 ],
220 ],
221 "error": null
221 "error": null
222
222
223 """
223 """
224 repo = get_repo_or_error(repoid)
224 repo = get_repo_or_error(repoid)
225 if not has_superadmin_permission(apiuser):
225 if not has_superadmin_permission(apiuser):
226 _perms = (
226 _perms = (
227 'repository.admin', 'repository.write', 'repository.read',)
227 'repository.admin', 'repository.write', 'repository.read',)
228 validate_repo_permissions(apiuser, repoid, repo, _perms)
228 validate_repo_permissions(apiuser, repoid, repo, _perms)
229
229
230 status = Optional.extract(status)
230 status = Optional.extract(status)
231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
232 data = [pr.get_api_data() for pr in pull_requests]
232 data = [pr.get_api_data() for pr in pull_requests]
233 return data
233 return data
234
234
235
235
236 @jsonrpc_method()
236 @jsonrpc_method()
237 def merge_pull_request(
237 def merge_pull_request(
238 request, apiuser, pullrequestid, repoid=Optional(None),
238 request, apiuser, pullrequestid, repoid=Optional(None),
239 userid=Optional(OAttr('apiuser'))):
239 userid=Optional(OAttr('apiuser'))):
240 """
240 """
241 Merge the pull request specified by `pullrequestid` into its target
241 Merge the pull request specified by `pullrequestid` into its target
242 repository.
242 repository.
243
243
244 :param apiuser: This is filled automatically from the |authtoken|.
244 :param apiuser: This is filled automatically from the |authtoken|.
245 :type apiuser: AuthUser
245 :type apiuser: AuthUser
246 :param repoid: Optional, repository name or repository ID of the
246 :param repoid: Optional, repository name or repository ID of the
247 target repository to which the |pr| is to be merged.
247 target repository to which the |pr| is to be merged.
248 :type repoid: str or int
248 :type repoid: str or int
249 :param pullrequestid: ID of the pull request which shall be merged.
249 :param pullrequestid: ID of the pull request which shall be merged.
250 :type pullrequestid: int
250 :type pullrequestid: int
251 :param userid: Merge the pull request as this user.
251 :param userid: Merge the pull request as this user.
252 :type userid: Optional(str or int)
252 :type userid: Optional(str or int)
253
253
254 Example output:
254 Example output:
255
255
256 .. code-block:: bash
256 .. code-block:: bash
257
257
258 "id": <id_given_in_input>,
258 "id": <id_given_in_input>,
259 "result": {
259 "result": {
260 "executed": "<bool>",
260 "executed": "<bool>",
261 "failure_reason": "<int>",
261 "failure_reason": "<int>",
262 "merge_commit_id": "<merge_commit_id>",
262 "merge_commit_id": "<merge_commit_id>",
263 "possible": "<bool>",
263 "possible": "<bool>",
264 "merge_ref": {
264 "merge_ref": {
265 "commit_id": "<commit_id>",
265 "commit_id": "<commit_id>",
266 "type": "<type>",
266 "type": "<type>",
267 "name": "<name>"
267 "name": "<name>"
268 }
268 }
269 },
269 },
270 "error": null
270 "error": null
271 """
271 """
272 pull_request = get_pull_request_or_error(pullrequestid)
272 pull_request = get_pull_request_or_error(pullrequestid)
273 if Optional.extract(repoid):
273 if Optional.extract(repoid):
274 repo = get_repo_or_error(repoid)
274 repo = get_repo_or_error(repoid)
275 else:
275 else:
276 repo = pull_request.target_repo
276 repo = pull_request.target_repo
277
277
278 if not isinstance(userid, Optional):
278 if not isinstance(userid, Optional):
279 if (has_superadmin_permission(apiuser) or
279 if (has_superadmin_permission(apiuser) or
280 HasRepoPermissionAnyApi('repository.admin')(
280 HasRepoPermissionAnyApi('repository.admin')(
281 user=apiuser, repo_name=repo.repo_name)):
281 user=apiuser, repo_name=repo.repo_name)):
282 apiuser = get_user_or_error(userid)
282 apiuser = get_user_or_error(userid)
283 else:
283 else:
284 raise JSONRPCError('userid is not the same as your user')
284 raise JSONRPCError('userid is not the same as your user')
285
285
286 check = MergeCheck.validate(
286 check = MergeCheck.validate(
287 pull_request, user=apiuser, translator=request.translate)
287 pull_request, auth_user=apiuser, translator=request.translate)
288 merge_possible = not check.failed
288 merge_possible = not check.failed
289
289
290 if not merge_possible:
290 if not merge_possible:
291 error_messages = []
291 error_messages = []
292 for err_type, error_msg in check.errors:
292 for err_type, error_msg in check.errors:
293 error_msg = request.translate(error_msg)
293 error_msg = request.translate(error_msg)
294 error_messages.append(error_msg)
294 error_messages.append(error_msg)
295
295
296 reasons = ','.join(error_messages)
296 reasons = ','.join(error_messages)
297 raise JSONRPCError(
297 raise JSONRPCError(
298 'merge not possible for following reasons: {}'.format(reasons))
298 'merge not possible for following reasons: {}'.format(reasons))
299
299
300 target_repo = pull_request.target_repo
300 target_repo = pull_request.target_repo
301 extras = vcs_operation_context(
301 extras = vcs_operation_context(
302 request.environ, repo_name=target_repo.repo_name,
302 request.environ, repo_name=target_repo.repo_name,
303 username=apiuser.username, action='push',
303 username=apiuser.username, action='push',
304 scm=target_repo.repo_type)
304 scm=target_repo.repo_type)
305 merge_response = PullRequestModel().merge_repo(
305 merge_response = PullRequestModel().merge_repo(
306 pull_request, apiuser, extras=extras)
306 pull_request, apiuser, extras=extras)
307 if merge_response.executed:
307 if merge_response.executed:
308 PullRequestModel().close_pull_request(
308 PullRequestModel().close_pull_request(
309 pull_request.pull_request_id, apiuser)
309 pull_request.pull_request_id, apiuser)
310
310
311 Session().commit()
311 Session().commit()
312
312
313 # In previous versions the merge response directly contained the merge
313 # In previous versions the merge response directly contained the merge
314 # commit id. It is now contained in the merge reference object. To be
314 # commit id. It is now contained in the merge reference object. To be
315 # backwards compatible we have to extract it again.
315 # backwards compatible we have to extract it again.
316 merge_response = merge_response._asdict()
316 merge_response = merge_response._asdict()
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318
318
319 return merge_response
319 return merge_response
320
320
321
321
322 @jsonrpc_method()
322 @jsonrpc_method()
323 def get_pull_request_comments(
323 def get_pull_request_comments(
324 request, apiuser, pullrequestid, repoid=Optional(None)):
324 request, apiuser, pullrequestid, repoid=Optional(None)):
325 """
325 """
326 Get all comments of pull request specified with the `pullrequestid`
326 Get all comments of pull request specified with the `pullrequestid`
327
327
328 :param apiuser: This is filled automatically from the |authtoken|.
328 :param apiuser: This is filled automatically from the |authtoken|.
329 :type apiuser: AuthUser
329 :type apiuser: AuthUser
330 :param repoid: Optional repository name or repository ID.
330 :param repoid: Optional repository name or repository ID.
331 :type repoid: str or int
331 :type repoid: str or int
332 :param pullrequestid: The pull request ID.
332 :param pullrequestid: The pull request ID.
333 :type pullrequestid: int
333 :type pullrequestid: int
334
334
335 Example output:
335 Example output:
336
336
337 .. code-block:: bash
337 .. code-block:: bash
338
338
339 id : <id_given_in_input>
339 id : <id_given_in_input>
340 result : [
340 result : [
341 {
341 {
342 "comment_author": {
342 "comment_author": {
343 "active": true,
343 "active": true,
344 "full_name_or_username": "Tom Gore",
344 "full_name_or_username": "Tom Gore",
345 "username": "admin"
345 "username": "admin"
346 },
346 },
347 "comment_created_on": "2017-01-02T18:43:45.533",
347 "comment_created_on": "2017-01-02T18:43:45.533",
348 "comment_f_path": null,
348 "comment_f_path": null,
349 "comment_id": 25,
349 "comment_id": 25,
350 "comment_lineno": null,
350 "comment_lineno": null,
351 "comment_status": {
351 "comment_status": {
352 "status": "under_review",
352 "status": "under_review",
353 "status_lbl": "Under Review"
353 "status_lbl": "Under Review"
354 },
354 },
355 "comment_text": "Example text",
355 "comment_text": "Example text",
356 "comment_type": null,
356 "comment_type": null,
357 "pull_request_version": null
357 "pull_request_version": null
358 }
358 }
359 ],
359 ],
360 error : null
360 error : null
361 """
361 """
362
362
363 pull_request = get_pull_request_or_error(pullrequestid)
363 pull_request = get_pull_request_or_error(pullrequestid)
364 if Optional.extract(repoid):
364 if Optional.extract(repoid):
365 repo = get_repo_or_error(repoid)
365 repo = get_repo_or_error(repoid)
366 else:
366 else:
367 repo = pull_request.target_repo
367 repo = pull_request.target_repo
368
368
369 if not PullRequestModel().check_user_read(
369 if not PullRequestModel().check_user_read(
370 pull_request, apiuser, api=True):
370 pull_request, apiuser, api=True):
371 raise JSONRPCError('repository `%s` or pull request `%s` '
371 raise JSONRPCError('repository `%s` or pull request `%s` '
372 'does not exist' % (repoid, pullrequestid))
372 'does not exist' % (repoid, pullrequestid))
373
373
374 (pull_request_latest,
374 (pull_request_latest,
375 pull_request_at_ver,
375 pull_request_at_ver,
376 pull_request_display_obj,
376 pull_request_display_obj,
377 at_version) = PullRequestModel().get_pr_version(
377 at_version) = PullRequestModel().get_pr_version(
378 pull_request.pull_request_id, version=None)
378 pull_request.pull_request_id, version=None)
379
379
380 versions = pull_request_display_obj.versions()
380 versions = pull_request_display_obj.versions()
381 ver_map = {
381 ver_map = {
382 ver.pull_request_version_id: cnt
382 ver.pull_request_version_id: cnt
383 for cnt, ver in enumerate(versions, 1)
383 for cnt, ver in enumerate(versions, 1)
384 }
384 }
385
385
386 # GENERAL COMMENTS with versions #
386 # GENERAL COMMENTS with versions #
387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
388 q = q.order_by(ChangesetComment.comment_id.asc())
388 q = q.order_by(ChangesetComment.comment_id.asc())
389 general_comments = q.all()
389 general_comments = q.all()
390
390
391 # INLINE COMMENTS with versions #
391 # INLINE COMMENTS with versions #
392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
393 q = q.order_by(ChangesetComment.comment_id.asc())
393 q = q.order_by(ChangesetComment.comment_id.asc())
394 inline_comments = q.all()
394 inline_comments = q.all()
395
395
396 data = []
396 data = []
397 for comment in inline_comments + general_comments:
397 for comment in inline_comments + general_comments:
398 full_data = comment.get_api_data()
398 full_data = comment.get_api_data()
399 pr_version_id = None
399 pr_version_id = None
400 if comment.pull_request_version_id:
400 if comment.pull_request_version_id:
401 pr_version_id = 'v{}'.format(
401 pr_version_id = 'v{}'.format(
402 ver_map[comment.pull_request_version_id])
402 ver_map[comment.pull_request_version_id])
403
403
404 # sanitize some entries
404 # sanitize some entries
405
405
406 full_data['pull_request_version'] = pr_version_id
406 full_data['pull_request_version'] = pr_version_id
407 full_data['comment_author'] = {
407 full_data['comment_author'] = {
408 'username': full_data['comment_author'].username,
408 'username': full_data['comment_author'].username,
409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
410 'active': full_data['comment_author'].active,
410 'active': full_data['comment_author'].active,
411 }
411 }
412
412
413 if full_data['comment_status']:
413 if full_data['comment_status']:
414 full_data['comment_status'] = {
414 full_data['comment_status'] = {
415 'status': full_data['comment_status'][0].status,
415 'status': full_data['comment_status'][0].status,
416 'status_lbl': full_data['comment_status'][0].status_lbl,
416 'status_lbl': full_data['comment_status'][0].status_lbl,
417 }
417 }
418 else:
418 else:
419 full_data['comment_status'] = {}
419 full_data['comment_status'] = {}
420
420
421 data.append(full_data)
421 data.append(full_data)
422 return data
422 return data
423
423
424
424
425 @jsonrpc_method()
425 @jsonrpc_method()
426 def comment_pull_request(
426 def comment_pull_request(
427 request, apiuser, pullrequestid, repoid=Optional(None),
427 request, apiuser, pullrequestid, repoid=Optional(None),
428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
430 resolves_comment_id=Optional(None),
430 resolves_comment_id=Optional(None),
431 userid=Optional(OAttr('apiuser'))):
431 userid=Optional(OAttr('apiuser'))):
432 """
432 """
433 Comment on the pull request specified with the `pullrequestid`,
433 Comment on the pull request specified with the `pullrequestid`,
434 in the |repo| specified by the `repoid`, and optionally change the
434 in the |repo| specified by the `repoid`, and optionally change the
435 review status.
435 review status.
436
436
437 :param apiuser: This is filled automatically from the |authtoken|.
437 :param apiuser: This is filled automatically from the |authtoken|.
438 :type apiuser: AuthUser
438 :type apiuser: AuthUser
439 :param repoid: Optional repository name or repository ID.
439 :param repoid: Optional repository name or repository ID.
440 :type repoid: str or int
440 :type repoid: str or int
441 :param pullrequestid: The pull request ID.
441 :param pullrequestid: The pull request ID.
442 :type pullrequestid: int
442 :type pullrequestid: int
443 :param commit_id: Specify the commit_id for which to set a comment. If
443 :param commit_id: Specify the commit_id for which to set a comment. If
444 given commit_id is different than latest in the PR status
444 given commit_id is different than latest in the PR status
445 change won't be performed.
445 change won't be performed.
446 :type commit_id: str
446 :type commit_id: str
447 :param message: The text content of the comment.
447 :param message: The text content of the comment.
448 :type message: str
448 :type message: str
449 :param status: (**Optional**) Set the approval status of the pull
449 :param status: (**Optional**) Set the approval status of the pull
450 request. One of: 'not_reviewed', 'approved', 'rejected',
450 request. One of: 'not_reviewed', 'approved', 'rejected',
451 'under_review'
451 'under_review'
452 :type status: str
452 :type status: str
453 :param comment_type: Comment type, one of: 'note', 'todo'
453 :param comment_type: Comment type, one of: 'note', 'todo'
454 :type comment_type: Optional(str), default: 'note'
454 :type comment_type: Optional(str), default: 'note'
455 :param userid: Comment on the pull request as this user
455 :param userid: Comment on the pull request as this user
456 :type userid: Optional(str or int)
456 :type userid: Optional(str or int)
457
457
458 Example output:
458 Example output:
459
459
460 .. code-block:: bash
460 .. code-block:: bash
461
461
462 id : <id_given_in_input>
462 id : <id_given_in_input>
463 result : {
463 result : {
464 "pull_request_id": "<Integer>",
464 "pull_request_id": "<Integer>",
465 "comment_id": "<Integer>",
465 "comment_id": "<Integer>",
466 "status": {"given": <given_status>,
466 "status": {"given": <given_status>,
467 "was_changed": <bool status_was_actually_changed> },
467 "was_changed": <bool status_was_actually_changed> },
468 },
468 },
469 error : null
469 error : null
470 """
470 """
471 pull_request = get_pull_request_or_error(pullrequestid)
471 pull_request = get_pull_request_or_error(pullrequestid)
472 if Optional.extract(repoid):
472 if Optional.extract(repoid):
473 repo = get_repo_or_error(repoid)
473 repo = get_repo_or_error(repoid)
474 else:
474 else:
475 repo = pull_request.target_repo
475 repo = pull_request.target_repo
476
476
477 if not isinstance(userid, Optional):
477 if not isinstance(userid, Optional):
478 if (has_superadmin_permission(apiuser) or
478 if (has_superadmin_permission(apiuser) or
479 HasRepoPermissionAnyApi('repository.admin')(
479 HasRepoPermissionAnyApi('repository.admin')(
480 user=apiuser, repo_name=repo.repo_name)):
480 user=apiuser, repo_name=repo.repo_name)):
481 apiuser = get_user_or_error(userid)
481 apiuser = get_user_or_error(userid)
482 else:
482 else:
483 raise JSONRPCError('userid is not the same as your user')
483 raise JSONRPCError('userid is not the same as your user')
484
484
485 if not PullRequestModel().check_user_read(
485 if not PullRequestModel().check_user_read(
486 pull_request, apiuser, api=True):
486 pull_request, apiuser, api=True):
487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
488 message = Optional.extract(message)
488 message = Optional.extract(message)
489 status = Optional.extract(status)
489 status = Optional.extract(status)
490 commit_id = Optional.extract(commit_id)
490 commit_id = Optional.extract(commit_id)
491 comment_type = Optional.extract(comment_type)
491 comment_type = Optional.extract(comment_type)
492 resolves_comment_id = Optional.extract(resolves_comment_id)
492 resolves_comment_id = Optional.extract(resolves_comment_id)
493
493
494 if not message and not status:
494 if not message and not status:
495 raise JSONRPCError(
495 raise JSONRPCError(
496 'Both message and status parameters are missing. '
496 'Both message and status parameters are missing. '
497 'At least one is required.')
497 'At least one is required.')
498
498
499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
500 status is not None):
500 status is not None):
501 raise JSONRPCError('Unknown comment status: `%s`' % status)
501 raise JSONRPCError('Unknown comment status: `%s`' % status)
502
502
503 if commit_id and commit_id not in pull_request.revisions:
503 if commit_id and commit_id not in pull_request.revisions:
504 raise JSONRPCError(
504 raise JSONRPCError(
505 'Invalid commit_id `%s` for this pull request.' % commit_id)
505 'Invalid commit_id `%s` for this pull request.' % commit_id)
506
506
507 allowed_to_change_status = PullRequestModel().check_user_change_status(
507 allowed_to_change_status = PullRequestModel().check_user_change_status(
508 pull_request, apiuser)
508 pull_request, apiuser)
509
509
510 # if commit_id is passed re-validated if user is allowed to change status
510 # if commit_id is passed re-validated if user is allowed to change status
511 # based on latest commit_id from the PR
511 # based on latest commit_id from the PR
512 if commit_id:
512 if commit_id:
513 commit_idx = pull_request.revisions.index(commit_id)
513 commit_idx = pull_request.revisions.index(commit_id)
514 if commit_idx != 0:
514 if commit_idx != 0:
515 allowed_to_change_status = False
515 allowed_to_change_status = False
516
516
517 if resolves_comment_id:
517 if resolves_comment_id:
518 comment = ChangesetComment.get(resolves_comment_id)
518 comment = ChangesetComment.get(resolves_comment_id)
519 if not comment:
519 if not comment:
520 raise JSONRPCError(
520 raise JSONRPCError(
521 'Invalid resolves_comment_id `%s` for this pull request.'
521 'Invalid resolves_comment_id `%s` for this pull request.'
522 % resolves_comment_id)
522 % resolves_comment_id)
523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
524 raise JSONRPCError(
524 raise JSONRPCError(
525 'Comment `%s` is wrong type for setting status to resolved.'
525 'Comment `%s` is wrong type for setting status to resolved.'
526 % resolves_comment_id)
526 % resolves_comment_id)
527
527
528 text = message
528 text = message
529 status_label = ChangesetStatus.get_status_lbl(status)
529 status_label = ChangesetStatus.get_status_lbl(status)
530 if status and allowed_to_change_status:
530 if status and allowed_to_change_status:
531 st_message = ('Status change %(transition_icon)s %(status)s'
531 st_message = ('Status change %(transition_icon)s %(status)s'
532 % {'transition_icon': '>', 'status': status_label})
532 % {'transition_icon': '>', 'status': status_label})
533 text = message or st_message
533 text = message or st_message
534
534
535 rc_config = SettingsModel().get_all_settings()
535 rc_config = SettingsModel().get_all_settings()
536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
537
537
538 status_change = status and allowed_to_change_status
538 status_change = status and allowed_to_change_status
539 comment = CommentsModel().create(
539 comment = CommentsModel().create(
540 text=text,
540 text=text,
541 repo=pull_request.target_repo.repo_id,
541 repo=pull_request.target_repo.repo_id,
542 user=apiuser.user_id,
542 user=apiuser.user_id,
543 pull_request=pull_request.pull_request_id,
543 pull_request=pull_request.pull_request_id,
544 f_path=None,
544 f_path=None,
545 line_no=None,
545 line_no=None,
546 status_change=(status_label if status_change else None),
546 status_change=(status_label if status_change else None),
547 status_change_type=(status if status_change else None),
547 status_change_type=(status if status_change else None),
548 closing_pr=False,
548 closing_pr=False,
549 renderer=renderer,
549 renderer=renderer,
550 comment_type=comment_type,
550 comment_type=comment_type,
551 resolves_comment_id=resolves_comment_id,
551 resolves_comment_id=resolves_comment_id,
552 auth_user=apiuser
552 auth_user=apiuser
553 )
553 )
554
554
555 if allowed_to_change_status and status:
555 if allowed_to_change_status and status:
556 ChangesetStatusModel().set_status(
556 ChangesetStatusModel().set_status(
557 pull_request.target_repo.repo_id,
557 pull_request.target_repo.repo_id,
558 status,
558 status,
559 apiuser.user_id,
559 apiuser.user_id,
560 comment,
560 comment,
561 pull_request=pull_request.pull_request_id
561 pull_request=pull_request.pull_request_id
562 )
562 )
563 Session().flush()
563 Session().flush()
564
564
565 Session().commit()
565 Session().commit()
566 data = {
566 data = {
567 'pull_request_id': pull_request.pull_request_id,
567 'pull_request_id': pull_request.pull_request_id,
568 'comment_id': comment.comment_id if comment else None,
568 'comment_id': comment.comment_id if comment else None,
569 'status': {'given': status, 'was_changed': status_change},
569 'status': {'given': status, 'was_changed': status_change},
570 }
570 }
571 return data
571 return data
572
572
573
573
574 @jsonrpc_method()
574 @jsonrpc_method()
575 def create_pull_request(
575 def create_pull_request(
576 request, apiuser, source_repo, target_repo, source_ref, target_ref,
576 request, apiuser, source_repo, target_repo, source_ref, target_ref,
577 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
577 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
578 reviewers=Optional(None)):
578 reviewers=Optional(None)):
579 """
579 """
580 Creates a new pull request.
580 Creates a new pull request.
581
581
582 Accepts refs in the following formats:
582 Accepts refs in the following formats:
583
583
584 * branch:<branch_name>:<sha>
584 * branch:<branch_name>:<sha>
585 * branch:<branch_name>
585 * branch:<branch_name>
586 * bookmark:<bookmark_name>:<sha> (Mercurial only)
586 * bookmark:<bookmark_name>:<sha> (Mercurial only)
587 * bookmark:<bookmark_name> (Mercurial only)
587 * bookmark:<bookmark_name> (Mercurial only)
588
588
589 :param apiuser: This is filled automatically from the |authtoken|.
589 :param apiuser: This is filled automatically from the |authtoken|.
590 :type apiuser: AuthUser
590 :type apiuser: AuthUser
591 :param source_repo: Set the source repository name.
591 :param source_repo: Set the source repository name.
592 :type source_repo: str
592 :type source_repo: str
593 :param target_repo: Set the target repository name.
593 :param target_repo: Set the target repository name.
594 :type target_repo: str
594 :type target_repo: str
595 :param source_ref: Set the source ref name.
595 :param source_ref: Set the source ref name.
596 :type source_ref: str
596 :type source_ref: str
597 :param target_ref: Set the target ref name.
597 :param target_ref: Set the target ref name.
598 :type target_ref: str
598 :type target_ref: str
599 :param title: Optionally Set the pull request title, it's generated otherwise
599 :param title: Optionally Set the pull request title, it's generated otherwise
600 :type title: str
600 :type title: str
601 :param description: Set the pull request description.
601 :param description: Set the pull request description.
602 :type description: Optional(str)
602 :type description: Optional(str)
603 :type description_renderer: Optional(str)
603 :type description_renderer: Optional(str)
604 :param description_renderer: Set pull request renderer for the description.
604 :param description_renderer: Set pull request renderer for the description.
605 It should be 'rst', 'markdown' or 'plain'. If not give default
605 It should be 'rst', 'markdown' or 'plain'. If not give default
606 system renderer will be used
606 system renderer will be used
607 :param reviewers: Set the new pull request reviewers list.
607 :param reviewers: Set the new pull request reviewers list.
608 Reviewer defined by review rules will be added automatically to the
608 Reviewer defined by review rules will be added automatically to the
609 defined list.
609 defined list.
610 :type reviewers: Optional(list)
610 :type reviewers: Optional(list)
611 Accepts username strings or objects of the format:
611 Accepts username strings or objects of the format:
612
612
613 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
613 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
614 """
614 """
615
615
616 source_db_repo = get_repo_or_error(source_repo)
616 source_db_repo = get_repo_or_error(source_repo)
617 target_db_repo = get_repo_or_error(target_repo)
617 target_db_repo = get_repo_or_error(target_repo)
618 if not has_superadmin_permission(apiuser):
618 if not has_superadmin_permission(apiuser):
619 _perms = ('repository.admin', 'repository.write', 'repository.read',)
619 _perms = ('repository.admin', 'repository.write', 'repository.read',)
620 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
620 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
621
621
622 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
622 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
623 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
623 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
624
624
625 source_scm = source_db_repo.scm_instance()
625 source_scm = source_db_repo.scm_instance()
626 target_scm = target_db_repo.scm_instance()
626 target_scm = target_db_repo.scm_instance()
627
627
628 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
628 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
629 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
629 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
630
630
631 ancestor = source_scm.get_common_ancestor(
631 ancestor = source_scm.get_common_ancestor(
632 source_commit.raw_id, target_commit.raw_id, target_scm)
632 source_commit.raw_id, target_commit.raw_id, target_scm)
633 if not ancestor:
633 if not ancestor:
634 raise JSONRPCError('no common ancestor found')
634 raise JSONRPCError('no common ancestor found')
635
635
636 # recalculate target ref based on ancestor
636 # recalculate target ref based on ancestor
637 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
637 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
638 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
638 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
639
639
640 commit_ranges = target_scm.compare(
640 commit_ranges = target_scm.compare(
641 target_commit.raw_id, source_commit.raw_id, source_scm,
641 target_commit.raw_id, source_commit.raw_id, source_scm,
642 merge=True, pre_load=[])
642 merge=True, pre_load=[])
643
643
644 if not commit_ranges:
644 if not commit_ranges:
645 raise JSONRPCError('no commits found')
645 raise JSONRPCError('no commits found')
646
646
647 reviewer_objects = Optional.extract(reviewers) or []
647 reviewer_objects = Optional.extract(reviewers) or []
648
648
649 # serialize and validate passed in given reviewers
649 # serialize and validate passed in given reviewers
650 if reviewer_objects:
650 if reviewer_objects:
651 schema = ReviewerListSchema()
651 schema = ReviewerListSchema()
652 try:
652 try:
653 reviewer_objects = schema.deserialize(reviewer_objects)
653 reviewer_objects = schema.deserialize(reviewer_objects)
654 except Invalid as err:
654 except Invalid as err:
655 raise JSONRPCValidationError(colander_exc=err)
655 raise JSONRPCValidationError(colander_exc=err)
656
656
657 # validate users
657 # validate users
658 for reviewer_object in reviewer_objects:
658 for reviewer_object in reviewer_objects:
659 user = get_user_or_error(reviewer_object['username'])
659 user = get_user_or_error(reviewer_object['username'])
660 reviewer_object['user_id'] = user.user_id
660 reviewer_object['user_id'] = user.user_id
661
661
662 get_default_reviewers_data, validate_default_reviewers = \
662 get_default_reviewers_data, validate_default_reviewers = \
663 PullRequestModel().get_reviewer_functions()
663 PullRequestModel().get_reviewer_functions()
664
664
665 # recalculate reviewers logic, to make sure we can validate this
665 # recalculate reviewers logic, to make sure we can validate this
666 reviewer_rules = get_default_reviewers_data(
666 reviewer_rules = get_default_reviewers_data(
667 apiuser.get_instance(), source_db_repo,
667 apiuser.get_instance(), source_db_repo,
668 source_commit, target_db_repo, target_commit)
668 source_commit, target_db_repo, target_commit)
669
669
670 # now MERGE our given with the calculated
670 # now MERGE our given with the calculated
671 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
671 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
672
672
673 try:
673 try:
674 reviewers = validate_default_reviewers(
674 reviewers = validate_default_reviewers(
675 reviewer_objects, reviewer_rules)
675 reviewer_objects, reviewer_rules)
676 except ValueError as e:
676 except ValueError as e:
677 raise JSONRPCError('Reviewers Validation: {}'.format(e))
677 raise JSONRPCError('Reviewers Validation: {}'.format(e))
678
678
679 title = Optional.extract(title)
679 title = Optional.extract(title)
680 if not title:
680 if not title:
681 title_source_ref = source_ref.split(':', 2)[1]
681 title_source_ref = source_ref.split(':', 2)[1]
682 title = PullRequestModel().generate_pullrequest_title(
682 title = PullRequestModel().generate_pullrequest_title(
683 source=source_repo,
683 source=source_repo,
684 source_ref=title_source_ref,
684 source_ref=title_source_ref,
685 target=target_repo
685 target=target_repo
686 )
686 )
687 # fetch renderer, if set fallback to plain in case of PR
687 # fetch renderer, if set fallback to plain in case of PR
688 rc_config = SettingsModel().get_all_settings()
688 rc_config = SettingsModel().get_all_settings()
689 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
689 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
690 description = Optional.extract(description)
690 description = Optional.extract(description)
691 description_renderer = Optional.extract(description_renderer) or default_system_renderer
691 description_renderer = Optional.extract(description_renderer) or default_system_renderer
692
692
693 pull_request = PullRequestModel().create(
693 pull_request = PullRequestModel().create(
694 created_by=apiuser.user_id,
694 created_by=apiuser.user_id,
695 source_repo=source_repo,
695 source_repo=source_repo,
696 source_ref=full_source_ref,
696 source_ref=full_source_ref,
697 target_repo=target_repo,
697 target_repo=target_repo,
698 target_ref=full_target_ref,
698 target_ref=full_target_ref,
699 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
699 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
700 reviewers=reviewers,
700 reviewers=reviewers,
701 title=title,
701 title=title,
702 description=description,
702 description=description,
703 description_renderer=description_renderer,
703 description_renderer=description_renderer,
704 reviewer_data=reviewer_rules,
704 reviewer_data=reviewer_rules,
705 auth_user=apiuser
705 auth_user=apiuser
706 )
706 )
707
707
708 Session().commit()
708 Session().commit()
709 data = {
709 data = {
710 'msg': 'Created new pull request `{}`'.format(title),
710 'msg': 'Created new pull request `{}`'.format(title),
711 'pull_request_id': pull_request.pull_request_id,
711 'pull_request_id': pull_request.pull_request_id,
712 }
712 }
713 return data
713 return data
714
714
715
715
716 @jsonrpc_method()
716 @jsonrpc_method()
717 def update_pull_request(
717 def update_pull_request(
718 request, apiuser, pullrequestid, repoid=Optional(None),
718 request, apiuser, pullrequestid, repoid=Optional(None),
719 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
719 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
720 reviewers=Optional(None), update_commits=Optional(None)):
720 reviewers=Optional(None), update_commits=Optional(None)):
721 """
721 """
722 Updates a pull request.
722 Updates a pull request.
723
723
724 :param apiuser: This is filled automatically from the |authtoken|.
724 :param apiuser: This is filled automatically from the |authtoken|.
725 :type apiuser: AuthUser
725 :type apiuser: AuthUser
726 :param repoid: Optional repository name or repository ID.
726 :param repoid: Optional repository name or repository ID.
727 :type repoid: str or int
727 :type repoid: str or int
728 :param pullrequestid: The pull request ID.
728 :param pullrequestid: The pull request ID.
729 :type pullrequestid: int
729 :type pullrequestid: int
730 :param title: Set the pull request title.
730 :param title: Set the pull request title.
731 :type title: str
731 :type title: str
732 :param description: Update pull request description.
732 :param description: Update pull request description.
733 :type description: Optional(str)
733 :type description: Optional(str)
734 :type description_renderer: Optional(str)
734 :type description_renderer: Optional(str)
735 :param description_renderer: Update pull request renderer for the description.
735 :param description_renderer: Update pull request renderer for the description.
736 It should be 'rst', 'markdown' or 'plain'
736 It should be 'rst', 'markdown' or 'plain'
737 :param reviewers: Update pull request reviewers list with new value.
737 :param reviewers: Update pull request reviewers list with new value.
738 :type reviewers: Optional(list)
738 :type reviewers: Optional(list)
739 Accepts username strings or objects of the format:
739 Accepts username strings or objects of the format:
740
740
741 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
741 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
742
742
743 :param update_commits: Trigger update of commits for this pull request
743 :param update_commits: Trigger update of commits for this pull request
744 :type: update_commits: Optional(bool)
744 :type: update_commits: Optional(bool)
745
745
746 Example output:
746 Example output:
747
747
748 .. code-block:: bash
748 .. code-block:: bash
749
749
750 id : <id_given_in_input>
750 id : <id_given_in_input>
751 result : {
751 result : {
752 "msg": "Updated pull request `63`",
752 "msg": "Updated pull request `63`",
753 "pull_request": <pull_request_object>,
753 "pull_request": <pull_request_object>,
754 "updated_reviewers": {
754 "updated_reviewers": {
755 "added": [
755 "added": [
756 "username"
756 "username"
757 ],
757 ],
758 "removed": []
758 "removed": []
759 },
759 },
760 "updated_commits": {
760 "updated_commits": {
761 "added": [
761 "added": [
762 "<sha1_hash>"
762 "<sha1_hash>"
763 ],
763 ],
764 "common": [
764 "common": [
765 "<sha1_hash>",
765 "<sha1_hash>",
766 "<sha1_hash>",
766 "<sha1_hash>",
767 ],
767 ],
768 "removed": []
768 "removed": []
769 }
769 }
770 }
770 }
771 error : null
771 error : null
772 """
772 """
773
773
774 pull_request = get_pull_request_or_error(pullrequestid)
774 pull_request = get_pull_request_or_error(pullrequestid)
775 if Optional.extract(repoid):
775 if Optional.extract(repoid):
776 repo = get_repo_or_error(repoid)
776 repo = get_repo_or_error(repoid)
777 else:
777 else:
778 repo = pull_request.target_repo
778 repo = pull_request.target_repo
779
779
780 if not PullRequestModel().check_user_update(
780 if not PullRequestModel().check_user_update(
781 pull_request, apiuser, api=True):
781 pull_request, apiuser, api=True):
782 raise JSONRPCError(
782 raise JSONRPCError(
783 'pull request `%s` update failed, no permission to update.' % (
783 'pull request `%s` update failed, no permission to update.' % (
784 pullrequestid,))
784 pullrequestid,))
785 if pull_request.is_closed():
785 if pull_request.is_closed():
786 raise JSONRPCError(
786 raise JSONRPCError(
787 'pull request `%s` update failed, pull request is closed' % (
787 'pull request `%s` update failed, pull request is closed' % (
788 pullrequestid,))
788 pullrequestid,))
789
789
790 reviewer_objects = Optional.extract(reviewers) or []
790 reviewer_objects = Optional.extract(reviewers) or []
791
791
792 if reviewer_objects:
792 if reviewer_objects:
793 schema = ReviewerListSchema()
793 schema = ReviewerListSchema()
794 try:
794 try:
795 reviewer_objects = schema.deserialize(reviewer_objects)
795 reviewer_objects = schema.deserialize(reviewer_objects)
796 except Invalid as err:
796 except Invalid as err:
797 raise JSONRPCValidationError(colander_exc=err)
797 raise JSONRPCValidationError(colander_exc=err)
798
798
799 # validate users
799 # validate users
800 for reviewer_object in reviewer_objects:
800 for reviewer_object in reviewer_objects:
801 user = get_user_or_error(reviewer_object['username'])
801 user = get_user_or_error(reviewer_object['username'])
802 reviewer_object['user_id'] = user.user_id
802 reviewer_object['user_id'] = user.user_id
803
803
804 get_default_reviewers_data, get_validated_reviewers = \
804 get_default_reviewers_data, get_validated_reviewers = \
805 PullRequestModel().get_reviewer_functions()
805 PullRequestModel().get_reviewer_functions()
806
806
807 # re-use stored rules
807 # re-use stored rules
808 reviewer_rules = pull_request.reviewer_data
808 reviewer_rules = pull_request.reviewer_data
809 try:
809 try:
810 reviewers = get_validated_reviewers(
810 reviewers = get_validated_reviewers(
811 reviewer_objects, reviewer_rules)
811 reviewer_objects, reviewer_rules)
812 except ValueError as e:
812 except ValueError as e:
813 raise JSONRPCError('Reviewers Validation: {}'.format(e))
813 raise JSONRPCError('Reviewers Validation: {}'.format(e))
814 else:
814 else:
815 reviewers = []
815 reviewers = []
816
816
817 title = Optional.extract(title)
817 title = Optional.extract(title)
818 description = Optional.extract(description)
818 description = Optional.extract(description)
819 description_renderer = Optional.extract(description_renderer)
819 description_renderer = Optional.extract(description_renderer)
820
820
821 if title or description:
821 if title or description:
822 PullRequestModel().edit(
822 PullRequestModel().edit(
823 pull_request,
823 pull_request,
824 title or pull_request.title,
824 title or pull_request.title,
825 description or pull_request.description,
825 description or pull_request.description,
826 description_renderer or pull_request.description_renderer,
826 description_renderer or pull_request.description_renderer,
827 apiuser)
827 apiuser)
828 Session().commit()
828 Session().commit()
829
829
830 commit_changes = {"added": [], "common": [], "removed": []}
830 commit_changes = {"added": [], "common": [], "removed": []}
831 if str2bool(Optional.extract(update_commits)):
831 if str2bool(Optional.extract(update_commits)):
832 if PullRequestModel().has_valid_update_type(pull_request):
832 if PullRequestModel().has_valid_update_type(pull_request):
833 update_response = PullRequestModel().update_commits(
833 update_response = PullRequestModel().update_commits(
834 pull_request)
834 pull_request)
835 commit_changes = update_response.changes or commit_changes
835 commit_changes = update_response.changes or commit_changes
836 Session().commit()
836 Session().commit()
837
837
838 reviewers_changes = {"added": [], "removed": []}
838 reviewers_changes = {"added": [], "removed": []}
839 if reviewers:
839 if reviewers:
840 added_reviewers, removed_reviewers = \
840 added_reviewers, removed_reviewers = \
841 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
841 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
842
842
843 reviewers_changes['added'] = sorted(
843 reviewers_changes['added'] = sorted(
844 [get_user_or_error(n).username for n in added_reviewers])
844 [get_user_or_error(n).username for n in added_reviewers])
845 reviewers_changes['removed'] = sorted(
845 reviewers_changes['removed'] = sorted(
846 [get_user_or_error(n).username for n in removed_reviewers])
846 [get_user_or_error(n).username for n in removed_reviewers])
847 Session().commit()
847 Session().commit()
848
848
849 data = {
849 data = {
850 'msg': 'Updated pull request `{}`'.format(
850 'msg': 'Updated pull request `{}`'.format(
851 pull_request.pull_request_id),
851 pull_request.pull_request_id),
852 'pull_request': pull_request.get_api_data(),
852 'pull_request': pull_request.get_api_data(),
853 'updated_commits': commit_changes,
853 'updated_commits': commit_changes,
854 'updated_reviewers': reviewers_changes
854 'updated_reviewers': reviewers_changes
855 }
855 }
856
856
857 return data
857 return data
858
858
859
859
860 @jsonrpc_method()
860 @jsonrpc_method()
861 def close_pull_request(
861 def close_pull_request(
862 request, apiuser, pullrequestid, repoid=Optional(None),
862 request, apiuser, pullrequestid, repoid=Optional(None),
863 userid=Optional(OAttr('apiuser')), message=Optional('')):
863 userid=Optional(OAttr('apiuser')), message=Optional('')):
864 """
864 """
865 Close the pull request specified by `pullrequestid`.
865 Close the pull request specified by `pullrequestid`.
866
866
867 :param apiuser: This is filled automatically from the |authtoken|.
867 :param apiuser: This is filled automatically from the |authtoken|.
868 :type apiuser: AuthUser
868 :type apiuser: AuthUser
869 :param repoid: Repository name or repository ID to which the pull
869 :param repoid: Repository name or repository ID to which the pull
870 request belongs.
870 request belongs.
871 :type repoid: str or int
871 :type repoid: str or int
872 :param pullrequestid: ID of the pull request to be closed.
872 :param pullrequestid: ID of the pull request to be closed.
873 :type pullrequestid: int
873 :type pullrequestid: int
874 :param userid: Close the pull request as this user.
874 :param userid: Close the pull request as this user.
875 :type userid: Optional(str or int)
875 :type userid: Optional(str or int)
876 :param message: Optional message to close the Pull Request with. If not
876 :param message: Optional message to close the Pull Request with. If not
877 specified it will be generated automatically.
877 specified it will be generated automatically.
878 :type message: Optional(str)
878 :type message: Optional(str)
879
879
880 Example output:
880 Example output:
881
881
882 .. code-block:: bash
882 .. code-block:: bash
883
883
884 "id": <id_given_in_input>,
884 "id": <id_given_in_input>,
885 "result": {
885 "result": {
886 "pull_request_id": "<int>",
886 "pull_request_id": "<int>",
887 "close_status": "<str:status_lbl>,
887 "close_status": "<str:status_lbl>,
888 "closed": "<bool>"
888 "closed": "<bool>"
889 },
889 },
890 "error": null
890 "error": null
891
891
892 """
892 """
893 _ = request.translate
893 _ = request.translate
894
894
895 pull_request = get_pull_request_or_error(pullrequestid)
895 pull_request = get_pull_request_or_error(pullrequestid)
896 if Optional.extract(repoid):
896 if Optional.extract(repoid):
897 repo = get_repo_or_error(repoid)
897 repo = get_repo_or_error(repoid)
898 else:
898 else:
899 repo = pull_request.target_repo
899 repo = pull_request.target_repo
900
900
901 if not isinstance(userid, Optional):
901 if not isinstance(userid, Optional):
902 if (has_superadmin_permission(apiuser) or
902 if (has_superadmin_permission(apiuser) or
903 HasRepoPermissionAnyApi('repository.admin')(
903 HasRepoPermissionAnyApi('repository.admin')(
904 user=apiuser, repo_name=repo.repo_name)):
904 user=apiuser, repo_name=repo.repo_name)):
905 apiuser = get_user_or_error(userid)
905 apiuser = get_user_or_error(userid)
906 else:
906 else:
907 raise JSONRPCError('userid is not the same as your user')
907 raise JSONRPCError('userid is not the same as your user')
908
908
909 if pull_request.is_closed():
909 if pull_request.is_closed():
910 raise JSONRPCError(
910 raise JSONRPCError(
911 'pull request `%s` is already closed' % (pullrequestid,))
911 'pull request `%s` is already closed' % (pullrequestid,))
912
912
913 # only owner or admin or person with write permissions
913 # only owner or admin or person with write permissions
914 allowed_to_close = PullRequestModel().check_user_update(
914 allowed_to_close = PullRequestModel().check_user_update(
915 pull_request, apiuser, api=True)
915 pull_request, apiuser, api=True)
916
916
917 if not allowed_to_close:
917 if not allowed_to_close:
918 raise JSONRPCError(
918 raise JSONRPCError(
919 'pull request `%s` close failed, no permission to close.' % (
919 'pull request `%s` close failed, no permission to close.' % (
920 pullrequestid,))
920 pullrequestid,))
921
921
922 # message we're using to close the PR, else it's automatically generated
922 # message we're using to close the PR, else it's automatically generated
923 message = Optional.extract(message)
923 message = Optional.extract(message)
924
924
925 # finally close the PR, with proper message comment
925 # finally close the PR, with proper message comment
926 comment, status = PullRequestModel().close_pull_request_with_comment(
926 comment, status = PullRequestModel().close_pull_request_with_comment(
927 pull_request, apiuser, repo, message=message)
927 pull_request, apiuser, repo, message=message)
928 status_lbl = ChangesetStatus.get_status_lbl(status)
928 status_lbl = ChangesetStatus.get_status_lbl(status)
929
929
930 Session().commit()
930 Session().commit()
931
931
932 data = {
932 data = {
933 'pull_request_id': pull_request.pull_request_id,
933 'pull_request_id': pull_request.pull_request_id,
934 'close_status': status_lbl,
934 'close_status': status_lbl,
935 'closed': True,
935 'closed': True,
936 }
936 }
937 return data
937 return data
@@ -1,1324 +1,1325 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 RepositoryRequirementError, EmptyRepositoryError)
45 RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 ChangesetComment, ChangesetStatus, Repository)
49 ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
65 c.renderer = 'plain'
66 return c
66 return c
67
67
68 def _get_pull_requests_list(
68 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
70
70
71 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
75
75
76 # pagination
76 # pagination
77
77
78 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name, source=source, opened_by=opened_by,
80 repo_name, source=source, opened_by=opened_by,
81 statuses=statuses, offset=start, length=limit,
81 statuses=statuses, offset=start, length=limit,
82 order_by=order_by, order_dir=order_dir)
82 order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name, source=source, statuses=statuses,
84 repo_name, source=source, statuses=statuses,
85 opened_by=opened_by)
85 opened_by=opened_by)
86 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, source=source, opened_by=opened_by,
88 repo_name, source=source, opened_by=opened_by,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 offset=start, length=limit, order_by=order_by,
90 offset=start, length=limit, order_by=order_by,
91 order_dir=order_dir)
91 order_dir=order_dir)
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 repo_name, source=source, user_id=self._rhodecode_user.user_id,
94 statuses=statuses, opened_by=opened_by)
94 statuses=statuses, opened_by=opened_by)
95 else:
95 else:
96 pull_requests = PullRequestModel().get_all(
96 pull_requests = PullRequestModel().get_all(
97 repo_name, source=source, opened_by=opened_by,
97 repo_name, source=source, opened_by=opened_by,
98 statuses=statuses, offset=start, length=limit,
98 statuses=statuses, offset=start, length=limit,
99 order_by=order_by, order_dir=order_dir)
99 order_by=order_by, order_dir=order_dir)
100 pull_requests_total_count = PullRequestModel().count_all(
100 pull_requests_total_count = PullRequestModel().count_all(
101 repo_name, source=source, statuses=statuses,
101 repo_name, source=source, statuses=statuses,
102 opened_by=opened_by)
102 opened_by=opened_by)
103
103
104 data = []
104 data = []
105 comments_model = CommentsModel()
105 comments_model = CommentsModel()
106 for pr in pull_requests:
106 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
107 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
108 self.db_repo.repo_id, pull_request=pr)
109
109
110 data.append({
110 data.append({
111 'name': _render('pullrequest_name',
111 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.target_repo.repo_name),
112 pr.pull_request_id, pr.target_repo.repo_name),
113 'name_raw': pr.pull_request_id,
113 'name_raw': pr.pull_request_id,
114 'status': _render('pullrequest_status',
114 'status': _render('pullrequest_status',
115 pr.calculated_review_status()),
115 pr.calculated_review_status()),
116 'title': _render(
116 'title': _render(
117 'pullrequest_title', pr.title, pr.description),
117 'pullrequest_title', pr.title, pr.description),
118 'description': h.escape(pr.description),
118 'description': h.escape(pr.description),
119 'updated_on': _render('pullrequest_updated_on',
119 'updated_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.updated_on)),
120 h.datetime_to_time(pr.updated_on)),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'created_on': _render('pullrequest_updated_on',
122 'created_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.created_on)),
123 h.datetime_to_time(pr.created_on)),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'author': _render('pullrequest_author',
125 'author': _render('pullrequest_author',
126 pr.author.full_contact, ),
126 pr.author.full_contact, ),
127 'author_raw': pr.author.full_name,
127 'author_raw': pr.author.full_name,
128 'comments': _render('pullrequest_comments', len(comments)),
128 'comments': _render('pullrequest_comments', len(comments)),
129 'comments_raw': len(comments),
129 'comments_raw': len(comments),
130 'closed': pr.is_closed(),
130 'closed': pr.is_closed(),
131 })
131 })
132
132
133 data = ({
133 data = ({
134 'draw': draw,
134 'draw': draw,
135 'data': data,
135 'data': data,
136 'recordsTotal': pull_requests_total_count,
136 'recordsTotal': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
138 })
138 })
139 return data
139 return data
140
140
141 @LoginRequired()
141 @LoginRequired()
142 @HasRepoPermissionAnyDecorator(
142 @HasRepoPermissionAnyDecorator(
143 'repository.read', 'repository.write', 'repository.admin')
143 'repository.read', 'repository.write', 'repository.admin')
144 @view_config(
144 @view_config(
145 route_name='pullrequest_show_all', request_method='GET',
145 route_name='pullrequest_show_all', request_method='GET',
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 def pull_request_list(self):
147 def pull_request_list(self):
148 c = self.load_default_context()
148 c = self.load_default_context()
149
149
150 req_get = self.request.GET
150 req_get = self.request.GET
151 c.source = str2bool(req_get.get('source'))
151 c.source = str2bool(req_get.get('source'))
152 c.closed = str2bool(req_get.get('closed'))
152 c.closed = str2bool(req_get.get('closed'))
153 c.my = str2bool(req_get.get('my'))
153 c.my = str2bool(req_get.get('my'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156
156
157 c.active = 'open'
157 c.active = 'open'
158 if c.my:
158 if c.my:
159 c.active = 'my'
159 c.active = 'my'
160 if c.closed:
160 if c.closed:
161 c.active = 'closed'
161 c.active = 'closed'
162 if c.awaiting_review and not c.source:
162 if c.awaiting_review and not c.source:
163 c.active = 'awaiting'
163 c.active = 'awaiting'
164 if c.source and not c.awaiting_review:
164 if c.source and not c.awaiting_review:
165 c.active = 'source'
165 c.active = 'source'
166 if c.awaiting_my_review:
166 if c.awaiting_my_review:
167 c.active = 'awaiting_my'
167 c.active = 'awaiting_my'
168
168
169 return self._get_template_context(c)
169 return self._get_template_context(c)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
172 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
173 'repository.read', 'repository.write', 'repository.admin')
174 @view_config(
174 @view_config(
175 route_name='pullrequest_show_all_data', request_method='GET',
175 route_name='pullrequest_show_all_data', request_method='GET',
176 renderer='json_ext', xhr=True)
176 renderer='json_ext', xhr=True)
177 def pull_request_list_data(self):
177 def pull_request_list_data(self):
178 self.load_default_context()
178 self.load_default_context()
179
179
180 # additional filters
180 # additional filters
181 req_get = self.request.GET
181 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
182 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
183 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
184 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
187
188 filter_type = 'awaiting_review' if awaiting_review \
188 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else None
190 else None
191
191
192 opened_by = None
192 opened_by = None
193 if my:
193 if my:
194 opened_by = [self._rhodecode_user.user_id]
194 opened_by = [self._rhodecode_user.user_id]
195
195
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
197 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
198 statuses = [PullRequest.STATUS_CLOSED]
199
199
200 data = self._get_pull_requests_list(
200 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
201 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
203
204 return data
204 return data
205
205
206 def _is_diff_cache_enabled(self, target_repo):
206 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
207 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
208 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
210 return caching_enabled
211
211
212 def _get_diffset(self, source_repo_name, source_repo,
212 def _get_diffset(self, source_repo_name, source_repo,
213 source_ref_id, target_ref_id,
213 source_ref_id, target_ref_id,
214 target_commit, source_commit, diff_limit, file_limit,
214 target_commit, source_commit, diff_limit, file_limit,
215 fulldiff):
215 fulldiff):
216
216
217 vcs_diff = PullRequestModel().get_diff(
217 vcs_diff = PullRequestModel().get_diff(
218 source_repo, source_ref_id, target_ref_id)
218 source_repo, source_ref_id, target_ref_id)
219
219
220 diff_processor = diffs.DiffProcessor(
220 diff_processor = diffs.DiffProcessor(
221 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 file_limit=file_limit, show_full_diff=fulldiff)
222 file_limit=file_limit, show_full_diff=fulldiff)
223
223
224 _parsed = diff_processor.prepare()
224 _parsed = diff_processor.prepare()
225
225
226 diffset = codeblocks.DiffSet(
226 diffset = codeblocks.DiffSet(
227 repo_name=self.db_repo_name,
227 repo_name=self.db_repo_name,
228 source_repo_name=source_repo_name,
228 source_repo_name=source_repo_name,
229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 )
231 )
232 diffset = self.path_filter.render_patchset_filtered(
232 diffset = self.path_filter.render_patchset_filtered(
233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234
234
235 return diffset
235 return diffset
236
236
237 @LoginRequired()
237 @LoginRequired()
238 @HasRepoPermissionAnyDecorator(
238 @HasRepoPermissionAnyDecorator(
239 'repository.read', 'repository.write', 'repository.admin')
239 'repository.read', 'repository.write', 'repository.admin')
240 @view_config(
240 @view_config(
241 route_name='pullrequest_show', request_method='GET',
241 route_name='pullrequest_show', request_method='GET',
242 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
242 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
243 def pull_request_show(self):
243 def pull_request_show(self):
244 pull_request_id = self.request.matchdict['pull_request_id']
244 pull_request_id = self.request.matchdict['pull_request_id']
245
245
246 c = self.load_default_context()
246 c = self.load_default_context()
247
247
248 version = self.request.GET.get('version')
248 version = self.request.GET.get('version')
249 from_version = self.request.GET.get('from_version') or version
249 from_version = self.request.GET.get('from_version') or version
250 merge_checks = self.request.GET.get('merge_checks')
250 merge_checks = self.request.GET.get('merge_checks')
251 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
251 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
252 force_refresh = str2bool(self.request.GET.get('force_refresh'))
252 force_refresh = str2bool(self.request.GET.get('force_refresh'))
253
253
254 (pull_request_latest,
254 (pull_request_latest,
255 pull_request_at_ver,
255 pull_request_at_ver,
256 pull_request_display_obj,
256 pull_request_display_obj,
257 at_version) = PullRequestModel().get_pr_version(
257 at_version) = PullRequestModel().get_pr_version(
258 pull_request_id, version=version)
258 pull_request_id, version=version)
259 pr_closed = pull_request_latest.is_closed()
259 pr_closed = pull_request_latest.is_closed()
260
260
261 if pr_closed and (version or from_version):
261 if pr_closed and (version or from_version):
262 # not allow to browse versions
262 # not allow to browse versions
263 raise HTTPFound(h.route_path(
263 raise HTTPFound(h.route_path(
264 'pullrequest_show', repo_name=self.db_repo_name,
264 'pullrequest_show', repo_name=self.db_repo_name,
265 pull_request_id=pull_request_id))
265 pull_request_id=pull_request_id))
266
266
267 versions = pull_request_display_obj.versions()
267 versions = pull_request_display_obj.versions()
268
268
269 c.at_version = at_version
269 c.at_version = at_version
270 c.at_version_num = (at_version
270 c.at_version_num = (at_version
271 if at_version and at_version != 'latest'
271 if at_version and at_version != 'latest'
272 else None)
272 else None)
273 c.at_version_pos = ChangesetComment.get_index_from_version(
273 c.at_version_pos = ChangesetComment.get_index_from_version(
274 c.at_version_num, versions)
274 c.at_version_num, versions)
275
275
276 (prev_pull_request_latest,
276 (prev_pull_request_latest,
277 prev_pull_request_at_ver,
277 prev_pull_request_at_ver,
278 prev_pull_request_display_obj,
278 prev_pull_request_display_obj,
279 prev_at_version) = PullRequestModel().get_pr_version(
279 prev_at_version) = PullRequestModel().get_pr_version(
280 pull_request_id, version=from_version)
280 pull_request_id, version=from_version)
281
281
282 c.from_version = prev_at_version
282 c.from_version = prev_at_version
283 c.from_version_num = (prev_at_version
283 c.from_version_num = (prev_at_version
284 if prev_at_version and prev_at_version != 'latest'
284 if prev_at_version and prev_at_version != 'latest'
285 else None)
285 else None)
286 c.from_version_pos = ChangesetComment.get_index_from_version(
286 c.from_version_pos = ChangesetComment.get_index_from_version(
287 c.from_version_num, versions)
287 c.from_version_num, versions)
288
288
289 # define if we're in COMPARE mode or VIEW at version mode
289 # define if we're in COMPARE mode or VIEW at version mode
290 compare = at_version != prev_at_version
290 compare = at_version != prev_at_version
291
291
292 # pull_requests repo_name we opened it against
292 # pull_requests repo_name we opened it against
293 # ie. target_repo must match
293 # ie. target_repo must match
294 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
294 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
295 raise HTTPNotFound()
295 raise HTTPNotFound()
296
296
297 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
297 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
298 pull_request_at_ver)
298 pull_request_at_ver)
299
299
300 c.pull_request = pull_request_display_obj
300 c.pull_request = pull_request_display_obj
301 c.renderer = pull_request_at_ver.description_renderer or c.renderer
301 c.renderer = pull_request_at_ver.description_renderer or c.renderer
302 c.pull_request_latest = pull_request_latest
302 c.pull_request_latest = pull_request_latest
303
303
304 if compare or (at_version and not at_version == 'latest'):
304 if compare or (at_version and not at_version == 'latest'):
305 c.allowed_to_change_status = False
305 c.allowed_to_change_status = False
306 c.allowed_to_update = False
306 c.allowed_to_update = False
307 c.allowed_to_merge = False
307 c.allowed_to_merge = False
308 c.allowed_to_delete = False
308 c.allowed_to_delete = False
309 c.allowed_to_comment = False
309 c.allowed_to_comment = False
310 c.allowed_to_close = False
310 c.allowed_to_close = False
311 else:
311 else:
312 can_change_status = PullRequestModel().check_user_change_status(
312 can_change_status = PullRequestModel().check_user_change_status(
313 pull_request_at_ver, self._rhodecode_user)
313 pull_request_at_ver, self._rhodecode_user)
314 c.allowed_to_change_status = can_change_status and not pr_closed
314 c.allowed_to_change_status = can_change_status and not pr_closed
315
315
316 c.allowed_to_update = PullRequestModel().check_user_update(
316 c.allowed_to_update = PullRequestModel().check_user_update(
317 pull_request_latest, self._rhodecode_user) and not pr_closed
317 pull_request_latest, self._rhodecode_user) and not pr_closed
318 c.allowed_to_merge = PullRequestModel().check_user_merge(
318 c.allowed_to_merge = PullRequestModel().check_user_merge(
319 pull_request_latest, self._rhodecode_user) and not pr_closed
319 pull_request_latest, self._rhodecode_user) and not pr_closed
320 c.allowed_to_delete = PullRequestModel().check_user_delete(
320 c.allowed_to_delete = PullRequestModel().check_user_delete(
321 pull_request_latest, self._rhodecode_user) and not pr_closed
321 pull_request_latest, self._rhodecode_user) and not pr_closed
322 c.allowed_to_comment = not pr_closed
322 c.allowed_to_comment = not pr_closed
323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
324
324
325 c.forbid_adding_reviewers = False
325 c.forbid_adding_reviewers = False
326 c.forbid_author_to_review = False
326 c.forbid_author_to_review = False
327 c.forbid_commit_author_to_review = False
327 c.forbid_commit_author_to_review = False
328
328
329 if pull_request_latest.reviewer_data and \
329 if pull_request_latest.reviewer_data and \
330 'rules' in pull_request_latest.reviewer_data:
330 'rules' in pull_request_latest.reviewer_data:
331 rules = pull_request_latest.reviewer_data['rules'] or {}
331 rules = pull_request_latest.reviewer_data['rules'] or {}
332 try:
332 try:
333 c.forbid_adding_reviewers = rules.get(
333 c.forbid_adding_reviewers = rules.get(
334 'forbid_adding_reviewers')
334 'forbid_adding_reviewers')
335 c.forbid_author_to_review = rules.get(
335 c.forbid_author_to_review = rules.get(
336 'forbid_author_to_review')
336 'forbid_author_to_review')
337 c.forbid_commit_author_to_review = rules.get(
337 c.forbid_commit_author_to_review = rules.get(
338 'forbid_commit_author_to_review')
338 'forbid_commit_author_to_review')
339 except Exception:
339 except Exception:
340 pass
340 pass
341
341
342 # check merge capabilities
342 # check merge capabilities
343 _merge_check = MergeCheck.validate(
343 _merge_check = MergeCheck.validate(
344 pull_request_latest, user=self._rhodecode_user,
344 pull_request_latest, auth_user=self._rhodecode_user,
345 translator=self.request.translate,
345 translator=self.request.translate,
346 force_shadow_repo_refresh=force_refresh)
346 force_shadow_repo_refresh=force_refresh)
347 c.pr_merge_errors = _merge_check.error_details
347 c.pr_merge_errors = _merge_check.error_details
348 c.pr_merge_possible = not _merge_check.failed
348 c.pr_merge_possible = not _merge_check.failed
349 c.pr_merge_message = _merge_check.merge_msg
349 c.pr_merge_message = _merge_check.merge_msg
350
350
351 c.pr_merge_info = MergeCheck.get_merge_conditions(
351 c.pr_merge_info = MergeCheck.get_merge_conditions(
352 pull_request_latest, translator=self.request.translate)
352 pull_request_latest, translator=self.request.translate)
353
353
354 c.pull_request_review_status = _merge_check.review_status
354 c.pull_request_review_status = _merge_check.review_status
355 if merge_checks:
355 if merge_checks:
356 self.request.override_renderer = \
356 self.request.override_renderer = \
357 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
357 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
358 return self._get_template_context(c)
358 return self._get_template_context(c)
359
359
360 comments_model = CommentsModel()
360 comments_model = CommentsModel()
361
361
362 # reviewers and statuses
362 # reviewers and statuses
363 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
363 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
364 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
364 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
365
365
366 # GENERAL COMMENTS with versions #
366 # GENERAL COMMENTS with versions #
367 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
367 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
368 q = q.order_by(ChangesetComment.comment_id.asc())
368 q = q.order_by(ChangesetComment.comment_id.asc())
369 general_comments = q
369 general_comments = q
370
370
371 # pick comments we want to render at current version
371 # pick comments we want to render at current version
372 c.comment_versions = comments_model.aggregate_comments(
372 c.comment_versions = comments_model.aggregate_comments(
373 general_comments, versions, c.at_version_num)
373 general_comments, versions, c.at_version_num)
374 c.comments = c.comment_versions[c.at_version_num]['until']
374 c.comments = c.comment_versions[c.at_version_num]['until']
375
375
376 # INLINE COMMENTS with versions #
376 # INLINE COMMENTS with versions #
377 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
377 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
378 q = q.order_by(ChangesetComment.comment_id.asc())
378 q = q.order_by(ChangesetComment.comment_id.asc())
379 inline_comments = q
379 inline_comments = q
380
380
381 c.inline_versions = comments_model.aggregate_comments(
381 c.inline_versions = comments_model.aggregate_comments(
382 inline_comments, versions, c.at_version_num, inline=True)
382 inline_comments, versions, c.at_version_num, inline=True)
383
383
384 # inject latest version
384 # inject latest version
385 latest_ver = PullRequest.get_pr_display_object(
385 latest_ver = PullRequest.get_pr_display_object(
386 pull_request_latest, pull_request_latest)
386 pull_request_latest, pull_request_latest)
387
387
388 c.versions = versions + [latest_ver]
388 c.versions = versions + [latest_ver]
389
389
390 # if we use version, then do not show later comments
390 # if we use version, then do not show later comments
391 # than current version
391 # than current version
392 display_inline_comments = collections.defaultdict(
392 display_inline_comments = collections.defaultdict(
393 lambda: collections.defaultdict(list))
393 lambda: collections.defaultdict(list))
394 for co in inline_comments:
394 for co in inline_comments:
395 if c.at_version_num:
395 if c.at_version_num:
396 # pick comments that are at least UPTO given version, so we
396 # pick comments that are at least UPTO given version, so we
397 # don't render comments for higher version
397 # don't render comments for higher version
398 should_render = co.pull_request_version_id and \
398 should_render = co.pull_request_version_id and \
399 co.pull_request_version_id <= c.at_version_num
399 co.pull_request_version_id <= c.at_version_num
400 else:
400 else:
401 # showing all, for 'latest'
401 # showing all, for 'latest'
402 should_render = True
402 should_render = True
403
403
404 if should_render:
404 if should_render:
405 display_inline_comments[co.f_path][co.line_no].append(co)
405 display_inline_comments[co.f_path][co.line_no].append(co)
406
406
407 # load diff data into template context, if we use compare mode then
407 # load diff data into template context, if we use compare mode then
408 # diff is calculated based on changes between versions of PR
408 # diff is calculated based on changes between versions of PR
409
409
410 source_repo = pull_request_at_ver.source_repo
410 source_repo = pull_request_at_ver.source_repo
411 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
411 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
412
412
413 target_repo = pull_request_at_ver.target_repo
413 target_repo = pull_request_at_ver.target_repo
414 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
414 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
415
415
416 if compare:
416 if compare:
417 # in compare switch the diff base to latest commit from prev version
417 # in compare switch the diff base to latest commit from prev version
418 target_ref_id = prev_pull_request_display_obj.revisions[0]
418 target_ref_id = prev_pull_request_display_obj.revisions[0]
419
419
420 # despite opening commits for bookmarks/branches/tags, we always
420 # despite opening commits for bookmarks/branches/tags, we always
421 # convert this to rev to prevent changes after bookmark or branch change
421 # convert this to rev to prevent changes after bookmark or branch change
422 c.source_ref_type = 'rev'
422 c.source_ref_type = 'rev'
423 c.source_ref = source_ref_id
423 c.source_ref = source_ref_id
424
424
425 c.target_ref_type = 'rev'
425 c.target_ref_type = 'rev'
426 c.target_ref = target_ref_id
426 c.target_ref = target_ref_id
427
427
428 c.source_repo = source_repo
428 c.source_repo = source_repo
429 c.target_repo = target_repo
429 c.target_repo = target_repo
430
430
431 c.commit_ranges = []
431 c.commit_ranges = []
432 source_commit = EmptyCommit()
432 source_commit = EmptyCommit()
433 target_commit = EmptyCommit()
433 target_commit = EmptyCommit()
434 c.missing_requirements = False
434 c.missing_requirements = False
435
435
436 source_scm = source_repo.scm_instance()
436 source_scm = source_repo.scm_instance()
437 target_scm = target_repo.scm_instance()
437 target_scm = target_repo.scm_instance()
438
438
439 shadow_scm = None
439 shadow_scm = None
440 try:
440 try:
441 shadow_scm = pull_request_latest.get_shadow_repo()
441 shadow_scm = pull_request_latest.get_shadow_repo()
442 except Exception:
442 except Exception:
443 log.debug('Failed to get shadow repo', exc_info=True)
443 log.debug('Failed to get shadow repo', exc_info=True)
444 # try first the existing source_repo, and then shadow
444 # try first the existing source_repo, and then shadow
445 # repo if we can obtain one
445 # repo if we can obtain one
446 commits_source_repo = source_scm or shadow_scm
446 commits_source_repo = source_scm or shadow_scm
447
447
448 c.commits_source_repo = commits_source_repo
448 c.commits_source_repo = commits_source_repo
449 c.ancestor = None # set it to None, to hide it from PR view
449 c.ancestor = None # set it to None, to hide it from PR view
450
450
451 # empty version means latest, so we keep this to prevent
451 # empty version means latest, so we keep this to prevent
452 # double caching
452 # double caching
453 version_normalized = version or 'latest'
453 version_normalized = version or 'latest'
454 from_version_normalized = from_version or 'latest'
454 from_version_normalized = from_version or 'latest'
455
455
456 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
456 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
457 target_repo)
457 target_repo)
458 cache_file_path = diff_cache_exist(
458 cache_file_path = diff_cache_exist(
459 cache_path, 'pull_request', pull_request_id, version_normalized,
459 cache_path, 'pull_request', pull_request_id, version_normalized,
460 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
460 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
461
461
462 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
462 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
463 force_recache = str2bool(self.request.GET.get('force_recache'))
463 force_recache = str2bool(self.request.GET.get('force_recache'))
464
464
465 cached_diff = None
465 cached_diff = None
466 if caching_enabled:
466 if caching_enabled:
467 cached_diff = load_cached_diff(cache_file_path)
467 cached_diff = load_cached_diff(cache_file_path)
468
468
469 has_proper_commit_cache = (
469 has_proper_commit_cache = (
470 cached_diff and cached_diff.get('commits')
470 cached_diff and cached_diff.get('commits')
471 and len(cached_diff.get('commits', [])) == 5
471 and len(cached_diff.get('commits', [])) == 5
472 and cached_diff.get('commits')[0]
472 and cached_diff.get('commits')[0]
473 and cached_diff.get('commits')[3])
473 and cached_diff.get('commits')[3])
474 if not force_recache and has_proper_commit_cache:
474 if not force_recache and has_proper_commit_cache:
475 diff_commit_cache = \
475 diff_commit_cache = \
476 (ancestor_commit, commit_cache, missing_requirements,
476 (ancestor_commit, commit_cache, missing_requirements,
477 source_commit, target_commit) = cached_diff['commits']
477 source_commit, target_commit) = cached_diff['commits']
478 else:
478 else:
479 diff_commit_cache = \
479 diff_commit_cache = \
480 (ancestor_commit, commit_cache, missing_requirements,
480 (ancestor_commit, commit_cache, missing_requirements,
481 source_commit, target_commit) = self.get_commits(
481 source_commit, target_commit) = self.get_commits(
482 commits_source_repo,
482 commits_source_repo,
483 pull_request_at_ver,
483 pull_request_at_ver,
484 source_commit,
484 source_commit,
485 source_ref_id,
485 source_ref_id,
486 source_scm,
486 source_scm,
487 target_commit,
487 target_commit,
488 target_ref_id,
488 target_ref_id,
489 target_scm)
489 target_scm)
490
490
491 # register our commit range
491 # register our commit range
492 for comm in commit_cache.values():
492 for comm in commit_cache.values():
493 c.commit_ranges.append(comm)
493 c.commit_ranges.append(comm)
494
494
495 c.missing_requirements = missing_requirements
495 c.missing_requirements = missing_requirements
496 c.ancestor_commit = ancestor_commit
496 c.ancestor_commit = ancestor_commit
497 c.statuses = source_repo.statuses(
497 c.statuses = source_repo.statuses(
498 [x.raw_id for x in c.commit_ranges])
498 [x.raw_id for x in c.commit_ranges])
499
499
500 # auto collapse if we have more than limit
500 # auto collapse if we have more than limit
501 collapse_limit = diffs.DiffProcessor._collapse_commits_over
501 collapse_limit = diffs.DiffProcessor._collapse_commits_over
502 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
502 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
503 c.compare_mode = compare
503 c.compare_mode = compare
504
504
505 # diff_limit is the old behavior, will cut off the whole diff
505 # diff_limit is the old behavior, will cut off the whole diff
506 # if the limit is applied otherwise will just hide the
506 # if the limit is applied otherwise will just hide the
507 # big files from the front-end
507 # big files from the front-end
508 diff_limit = c.visual.cut_off_limit_diff
508 diff_limit = c.visual.cut_off_limit_diff
509 file_limit = c.visual.cut_off_limit_file
509 file_limit = c.visual.cut_off_limit_file
510
510
511 c.missing_commits = False
511 c.missing_commits = False
512 if (c.missing_requirements
512 if (c.missing_requirements
513 or isinstance(source_commit, EmptyCommit)
513 or isinstance(source_commit, EmptyCommit)
514 or source_commit == target_commit):
514 or source_commit == target_commit):
515
515
516 c.missing_commits = True
516 c.missing_commits = True
517 else:
517 else:
518 c.inline_comments = display_inline_comments
518 c.inline_comments = display_inline_comments
519
519
520 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
520 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
521 if not force_recache and has_proper_diff_cache:
521 if not force_recache and has_proper_diff_cache:
522 c.diffset = cached_diff['diff']
522 c.diffset = cached_diff['diff']
523 (ancestor_commit, commit_cache, missing_requirements,
523 (ancestor_commit, commit_cache, missing_requirements,
524 source_commit, target_commit) = cached_diff['commits']
524 source_commit, target_commit) = cached_diff['commits']
525 else:
525 else:
526 c.diffset = self._get_diffset(
526 c.diffset = self._get_diffset(
527 c.source_repo.repo_name, commits_source_repo,
527 c.source_repo.repo_name, commits_source_repo,
528 source_ref_id, target_ref_id,
528 source_ref_id, target_ref_id,
529 target_commit, source_commit,
529 target_commit, source_commit,
530 diff_limit, file_limit, c.fulldiff)
530 diff_limit, file_limit, c.fulldiff)
531
531
532 # save cached diff
532 # save cached diff
533 if caching_enabled:
533 if caching_enabled:
534 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
534 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
535
535
536 c.limited_diff = c.diffset.limited_diff
536 c.limited_diff = c.diffset.limited_diff
537
537
538 # calculate removed files that are bound to comments
538 # calculate removed files that are bound to comments
539 comment_deleted_files = [
539 comment_deleted_files = [
540 fname for fname in display_inline_comments
540 fname for fname in display_inline_comments
541 if fname not in c.diffset.file_stats]
541 if fname not in c.diffset.file_stats]
542
542
543 c.deleted_files_comments = collections.defaultdict(dict)
543 c.deleted_files_comments = collections.defaultdict(dict)
544 for fname, per_line_comments in display_inline_comments.items():
544 for fname, per_line_comments in display_inline_comments.items():
545 if fname in comment_deleted_files:
545 if fname in comment_deleted_files:
546 c.deleted_files_comments[fname]['stats'] = 0
546 c.deleted_files_comments[fname]['stats'] = 0
547 c.deleted_files_comments[fname]['comments'] = list()
547 c.deleted_files_comments[fname]['comments'] = list()
548 for lno, comments in per_line_comments.items():
548 for lno, comments in per_line_comments.items():
549 c.deleted_files_comments[fname]['comments'].extend(
549 c.deleted_files_comments[fname]['comments'].extend(
550 comments)
550 comments)
551
551
552 # this is a hack to properly display links, when creating PR, the
552 # this is a hack to properly display links, when creating PR, the
553 # compare view and others uses different notation, and
553 # compare view and others uses different notation, and
554 # compare_commits.mako renders links based on the target_repo.
554 # compare_commits.mako renders links based on the target_repo.
555 # We need to swap that here to generate it properly on the html side
555 # We need to swap that here to generate it properly on the html side
556 c.target_repo = c.source_repo
556 c.target_repo = c.source_repo
557
557
558 c.commit_statuses = ChangesetStatus.STATUSES
558 c.commit_statuses = ChangesetStatus.STATUSES
559
559
560 c.show_version_changes = not pr_closed
560 c.show_version_changes = not pr_closed
561 if c.show_version_changes:
561 if c.show_version_changes:
562 cur_obj = pull_request_at_ver
562 cur_obj = pull_request_at_ver
563 prev_obj = prev_pull_request_at_ver
563 prev_obj = prev_pull_request_at_ver
564
564
565 old_commit_ids = prev_obj.revisions
565 old_commit_ids = prev_obj.revisions
566 new_commit_ids = cur_obj.revisions
566 new_commit_ids = cur_obj.revisions
567 commit_changes = PullRequestModel()._calculate_commit_id_changes(
567 commit_changes = PullRequestModel()._calculate_commit_id_changes(
568 old_commit_ids, new_commit_ids)
568 old_commit_ids, new_commit_ids)
569 c.commit_changes_summary = commit_changes
569 c.commit_changes_summary = commit_changes
570
570
571 # calculate the diff for commits between versions
571 # calculate the diff for commits between versions
572 c.commit_changes = []
572 c.commit_changes = []
573 mark = lambda cs, fw: list(
573 mark = lambda cs, fw: list(
574 h.itertools.izip_longest([], cs, fillvalue=fw))
574 h.itertools.izip_longest([], cs, fillvalue=fw))
575 for c_type, raw_id in mark(commit_changes.added, 'a') \
575 for c_type, raw_id in mark(commit_changes.added, 'a') \
576 + mark(commit_changes.removed, 'r') \
576 + mark(commit_changes.removed, 'r') \
577 + mark(commit_changes.common, 'c'):
577 + mark(commit_changes.common, 'c'):
578
578
579 if raw_id in commit_cache:
579 if raw_id in commit_cache:
580 commit = commit_cache[raw_id]
580 commit = commit_cache[raw_id]
581 else:
581 else:
582 try:
582 try:
583 commit = commits_source_repo.get_commit(raw_id)
583 commit = commits_source_repo.get_commit(raw_id)
584 except CommitDoesNotExistError:
584 except CommitDoesNotExistError:
585 # in case we fail extracting still use "dummy" commit
585 # in case we fail extracting still use "dummy" commit
586 # for display in commit diff
586 # for display in commit diff
587 commit = h.AttributeDict(
587 commit = h.AttributeDict(
588 {'raw_id': raw_id,
588 {'raw_id': raw_id,
589 'message': 'EMPTY or MISSING COMMIT'})
589 'message': 'EMPTY or MISSING COMMIT'})
590 c.commit_changes.append([c_type, commit])
590 c.commit_changes.append([c_type, commit])
591
591
592 # current user review statuses for each version
592 # current user review statuses for each version
593 c.review_versions = {}
593 c.review_versions = {}
594 if self._rhodecode_user.user_id in allowed_reviewers:
594 if self._rhodecode_user.user_id in allowed_reviewers:
595 for co in general_comments:
595 for co in general_comments:
596 if co.author.user_id == self._rhodecode_user.user_id:
596 if co.author.user_id == self._rhodecode_user.user_id:
597 status = co.status_change
597 status = co.status_change
598 if status:
598 if status:
599 _ver_pr = status[0].comment.pull_request_version_id
599 _ver_pr = status[0].comment.pull_request_version_id
600 c.review_versions[_ver_pr] = status[0]
600 c.review_versions[_ver_pr] = status[0]
601
601
602 return self._get_template_context(c)
602 return self._get_template_context(c)
603
603
604 def get_commits(
604 def get_commits(
605 self, commits_source_repo, pull_request_at_ver, source_commit,
605 self, commits_source_repo, pull_request_at_ver, source_commit,
606 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
606 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
607 commit_cache = collections.OrderedDict()
607 commit_cache = collections.OrderedDict()
608 missing_requirements = False
608 missing_requirements = False
609 try:
609 try:
610 pre_load = ["author", "branch", "date", "message"]
610 pre_load = ["author", "branch", "date", "message"]
611 show_revs = pull_request_at_ver.revisions
611 show_revs = pull_request_at_ver.revisions
612 for rev in show_revs:
612 for rev in show_revs:
613 comm = commits_source_repo.get_commit(
613 comm = commits_source_repo.get_commit(
614 commit_id=rev, pre_load=pre_load)
614 commit_id=rev, pre_load=pre_load)
615 commit_cache[comm.raw_id] = comm
615 commit_cache[comm.raw_id] = comm
616
616
617 # Order here matters, we first need to get target, and then
617 # Order here matters, we first need to get target, and then
618 # the source
618 # the source
619 target_commit = commits_source_repo.get_commit(
619 target_commit = commits_source_repo.get_commit(
620 commit_id=safe_str(target_ref_id))
620 commit_id=safe_str(target_ref_id))
621
621
622 source_commit = commits_source_repo.get_commit(
622 source_commit = commits_source_repo.get_commit(
623 commit_id=safe_str(source_ref_id))
623 commit_id=safe_str(source_ref_id))
624 except CommitDoesNotExistError:
624 except CommitDoesNotExistError:
625 log.warning(
625 log.warning(
626 'Failed to get commit from `{}` repo'.format(
626 'Failed to get commit from `{}` repo'.format(
627 commits_source_repo), exc_info=True)
627 commits_source_repo), exc_info=True)
628 except RepositoryRequirementError:
628 except RepositoryRequirementError:
629 log.warning(
629 log.warning(
630 'Failed to get all required data from repo', exc_info=True)
630 'Failed to get all required data from repo', exc_info=True)
631 missing_requirements = True
631 missing_requirements = True
632 ancestor_commit = None
632 ancestor_commit = None
633 try:
633 try:
634 ancestor_id = source_scm.get_common_ancestor(
634 ancestor_id = source_scm.get_common_ancestor(
635 source_commit.raw_id, target_commit.raw_id, target_scm)
635 source_commit.raw_id, target_commit.raw_id, target_scm)
636 ancestor_commit = source_scm.get_commit(ancestor_id)
636 ancestor_commit = source_scm.get_commit(ancestor_id)
637 except Exception:
637 except Exception:
638 ancestor_commit = None
638 ancestor_commit = None
639 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
639 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
640
640
641 def assure_not_empty_repo(self):
641 def assure_not_empty_repo(self):
642 _ = self.request.translate
642 _ = self.request.translate
643
643
644 try:
644 try:
645 self.db_repo.scm_instance().get_commit()
645 self.db_repo.scm_instance().get_commit()
646 except EmptyRepositoryError:
646 except EmptyRepositoryError:
647 h.flash(h.literal(_('There are no commits yet')),
647 h.flash(h.literal(_('There are no commits yet')),
648 category='warning')
648 category='warning')
649 raise HTTPFound(
649 raise HTTPFound(
650 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
650 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
651
651
652 @LoginRequired()
652 @LoginRequired()
653 @NotAnonymous()
653 @NotAnonymous()
654 @HasRepoPermissionAnyDecorator(
654 @HasRepoPermissionAnyDecorator(
655 'repository.read', 'repository.write', 'repository.admin')
655 'repository.read', 'repository.write', 'repository.admin')
656 @view_config(
656 @view_config(
657 route_name='pullrequest_new', request_method='GET',
657 route_name='pullrequest_new', request_method='GET',
658 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
658 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
659 def pull_request_new(self):
659 def pull_request_new(self):
660 _ = self.request.translate
660 _ = self.request.translate
661 c = self.load_default_context()
661 c = self.load_default_context()
662
662
663 self.assure_not_empty_repo()
663 self.assure_not_empty_repo()
664 source_repo = self.db_repo
664 source_repo = self.db_repo
665
665
666 commit_id = self.request.GET.get('commit')
666 commit_id = self.request.GET.get('commit')
667 branch_ref = self.request.GET.get('branch')
667 branch_ref = self.request.GET.get('branch')
668 bookmark_ref = self.request.GET.get('bookmark')
668 bookmark_ref = self.request.GET.get('bookmark')
669
669
670 try:
670 try:
671 source_repo_data = PullRequestModel().generate_repo_data(
671 source_repo_data = PullRequestModel().generate_repo_data(
672 source_repo, commit_id=commit_id,
672 source_repo, commit_id=commit_id,
673 branch=branch_ref, bookmark=bookmark_ref,
673 branch=branch_ref, bookmark=bookmark_ref,
674 translator=self.request.translate)
674 translator=self.request.translate)
675 except CommitDoesNotExistError as e:
675 except CommitDoesNotExistError as e:
676 log.exception(e)
676 log.exception(e)
677 h.flash(_('Commit does not exist'), 'error')
677 h.flash(_('Commit does not exist'), 'error')
678 raise HTTPFound(
678 raise HTTPFound(
679 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
679 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
680
680
681 default_target_repo = source_repo
681 default_target_repo = source_repo
682
682
683 if source_repo.parent:
683 if source_repo.parent:
684 parent_vcs_obj = source_repo.parent.scm_instance()
684 parent_vcs_obj = source_repo.parent.scm_instance()
685 if parent_vcs_obj and not parent_vcs_obj.is_empty():
685 if parent_vcs_obj and not parent_vcs_obj.is_empty():
686 # change default if we have a parent repo
686 # change default if we have a parent repo
687 default_target_repo = source_repo.parent
687 default_target_repo = source_repo.parent
688
688
689 target_repo_data = PullRequestModel().generate_repo_data(
689 target_repo_data = PullRequestModel().generate_repo_data(
690 default_target_repo, translator=self.request.translate)
690 default_target_repo, translator=self.request.translate)
691
691
692 selected_source_ref = source_repo_data['refs']['selected_ref']
692 selected_source_ref = source_repo_data['refs']['selected_ref']
693 title_source_ref = ''
693 title_source_ref = ''
694 if selected_source_ref:
694 if selected_source_ref:
695 title_source_ref = selected_source_ref.split(':', 2)[1]
695 title_source_ref = selected_source_ref.split(':', 2)[1]
696 c.default_title = PullRequestModel().generate_pullrequest_title(
696 c.default_title = PullRequestModel().generate_pullrequest_title(
697 source=source_repo.repo_name,
697 source=source_repo.repo_name,
698 source_ref=title_source_ref,
698 source_ref=title_source_ref,
699 target=default_target_repo.repo_name
699 target=default_target_repo.repo_name
700 )
700 )
701
701
702 c.default_repo_data = {
702 c.default_repo_data = {
703 'source_repo_name': source_repo.repo_name,
703 'source_repo_name': source_repo.repo_name,
704 'source_refs_json': json.dumps(source_repo_data),
704 'source_refs_json': json.dumps(source_repo_data),
705 'target_repo_name': default_target_repo.repo_name,
705 'target_repo_name': default_target_repo.repo_name,
706 'target_refs_json': json.dumps(target_repo_data),
706 'target_refs_json': json.dumps(target_repo_data),
707 }
707 }
708 c.default_source_ref = selected_source_ref
708 c.default_source_ref = selected_source_ref
709
709
710 return self._get_template_context(c)
710 return self._get_template_context(c)
711
711
712 @LoginRequired()
712 @LoginRequired()
713 @NotAnonymous()
713 @NotAnonymous()
714 @HasRepoPermissionAnyDecorator(
714 @HasRepoPermissionAnyDecorator(
715 'repository.read', 'repository.write', 'repository.admin')
715 'repository.read', 'repository.write', 'repository.admin')
716 @view_config(
716 @view_config(
717 route_name='pullrequest_repo_refs', request_method='GET',
717 route_name='pullrequest_repo_refs', request_method='GET',
718 renderer='json_ext', xhr=True)
718 renderer='json_ext', xhr=True)
719 def pull_request_repo_refs(self):
719 def pull_request_repo_refs(self):
720 self.load_default_context()
720 self.load_default_context()
721 target_repo_name = self.request.matchdict['target_repo_name']
721 target_repo_name = self.request.matchdict['target_repo_name']
722 repo = Repository.get_by_repo_name(target_repo_name)
722 repo = Repository.get_by_repo_name(target_repo_name)
723 if not repo:
723 if not repo:
724 raise HTTPNotFound()
724 raise HTTPNotFound()
725
725
726 target_perm = HasRepoPermissionAny(
726 target_perm = HasRepoPermissionAny(
727 'repository.read', 'repository.write', 'repository.admin')(
727 'repository.read', 'repository.write', 'repository.admin')(
728 target_repo_name)
728 target_repo_name)
729 if not target_perm:
729 if not target_perm:
730 raise HTTPNotFound()
730 raise HTTPNotFound()
731
731
732 return PullRequestModel().generate_repo_data(
732 return PullRequestModel().generate_repo_data(
733 repo, translator=self.request.translate)
733 repo, translator=self.request.translate)
734
734
735 @LoginRequired()
735 @LoginRequired()
736 @NotAnonymous()
736 @NotAnonymous()
737 @HasRepoPermissionAnyDecorator(
737 @HasRepoPermissionAnyDecorator(
738 'repository.read', 'repository.write', 'repository.admin')
738 'repository.read', 'repository.write', 'repository.admin')
739 @view_config(
739 @view_config(
740 route_name='pullrequest_repo_destinations', request_method='GET',
740 route_name='pullrequest_repo_destinations', request_method='GET',
741 renderer='json_ext', xhr=True)
741 renderer='json_ext', xhr=True)
742 def pull_request_repo_destinations(self):
742 def pull_request_repo_destinations(self):
743 _ = self.request.translate
743 _ = self.request.translate
744 filter_query = self.request.GET.get('query')
744 filter_query = self.request.GET.get('query')
745
745
746 query = Repository.query() \
746 query = Repository.query() \
747 .order_by(func.length(Repository.repo_name)) \
747 .order_by(func.length(Repository.repo_name)) \
748 .filter(
748 .filter(
749 or_(Repository.repo_name == self.db_repo.repo_name,
749 or_(Repository.repo_name == self.db_repo.repo_name,
750 Repository.fork_id == self.db_repo.repo_id))
750 Repository.fork_id == self.db_repo.repo_id))
751
751
752 if filter_query:
752 if filter_query:
753 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
753 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
754 query = query.filter(
754 query = query.filter(
755 Repository.repo_name.ilike(ilike_expression))
755 Repository.repo_name.ilike(ilike_expression))
756
756
757 add_parent = False
757 add_parent = False
758 if self.db_repo.parent:
758 if self.db_repo.parent:
759 if filter_query in self.db_repo.parent.repo_name:
759 if filter_query in self.db_repo.parent.repo_name:
760 parent_vcs_obj = self.db_repo.parent.scm_instance()
760 parent_vcs_obj = self.db_repo.parent.scm_instance()
761 if parent_vcs_obj and not parent_vcs_obj.is_empty():
761 if parent_vcs_obj and not parent_vcs_obj.is_empty():
762 add_parent = True
762 add_parent = True
763
763
764 limit = 20 - 1 if add_parent else 20
764 limit = 20 - 1 if add_parent else 20
765 all_repos = query.limit(limit).all()
765 all_repos = query.limit(limit).all()
766 if add_parent:
766 if add_parent:
767 all_repos += [self.db_repo.parent]
767 all_repos += [self.db_repo.parent]
768
768
769 repos = []
769 repos = []
770 for obj in ScmModel().get_repos(all_repos):
770 for obj in ScmModel().get_repos(all_repos):
771 repos.append({
771 repos.append({
772 'id': obj['name'],
772 'id': obj['name'],
773 'text': obj['name'],
773 'text': obj['name'],
774 'type': 'repo',
774 'type': 'repo',
775 'repo_id': obj['dbrepo']['repo_id'],
775 'repo_id': obj['dbrepo']['repo_id'],
776 'repo_type': obj['dbrepo']['repo_type'],
776 'repo_type': obj['dbrepo']['repo_type'],
777 'private': obj['dbrepo']['private'],
777 'private': obj['dbrepo']['private'],
778
778
779 })
779 })
780
780
781 data = {
781 data = {
782 'more': False,
782 'more': False,
783 'results': [{
783 'results': [{
784 'text': _('Repositories'),
784 'text': _('Repositories'),
785 'children': repos
785 'children': repos
786 }] if repos else []
786 }] if repos else []
787 }
787 }
788 return data
788 return data
789
789
790 @LoginRequired()
790 @LoginRequired()
791 @NotAnonymous()
791 @NotAnonymous()
792 @HasRepoPermissionAnyDecorator(
792 @HasRepoPermissionAnyDecorator(
793 'repository.read', 'repository.write', 'repository.admin')
793 'repository.read', 'repository.write', 'repository.admin')
794 @CSRFRequired()
794 @CSRFRequired()
795 @view_config(
795 @view_config(
796 route_name='pullrequest_create', request_method='POST',
796 route_name='pullrequest_create', request_method='POST',
797 renderer=None)
797 renderer=None)
798 def pull_request_create(self):
798 def pull_request_create(self):
799 _ = self.request.translate
799 _ = self.request.translate
800 self.assure_not_empty_repo()
800 self.assure_not_empty_repo()
801 self.load_default_context()
801 self.load_default_context()
802
802
803 controls = peppercorn.parse(self.request.POST.items())
803 controls = peppercorn.parse(self.request.POST.items())
804
804
805 try:
805 try:
806 form = PullRequestForm(
806 form = PullRequestForm(
807 self.request.translate, self.db_repo.repo_id)()
807 self.request.translate, self.db_repo.repo_id)()
808 _form = form.to_python(controls)
808 _form = form.to_python(controls)
809 except formencode.Invalid as errors:
809 except formencode.Invalid as errors:
810 if errors.error_dict.get('revisions'):
810 if errors.error_dict.get('revisions'):
811 msg = 'Revisions: %s' % errors.error_dict['revisions']
811 msg = 'Revisions: %s' % errors.error_dict['revisions']
812 elif errors.error_dict.get('pullrequest_title'):
812 elif errors.error_dict.get('pullrequest_title'):
813 msg = errors.error_dict.get('pullrequest_title')
813 msg = errors.error_dict.get('pullrequest_title')
814 else:
814 else:
815 msg = _('Error creating pull request: {}').format(errors)
815 msg = _('Error creating pull request: {}').format(errors)
816 log.exception(msg)
816 log.exception(msg)
817 h.flash(msg, 'error')
817 h.flash(msg, 'error')
818
818
819 # would rather just go back to form ...
819 # would rather just go back to form ...
820 raise HTTPFound(
820 raise HTTPFound(
821 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
821 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
822
822
823 source_repo = _form['source_repo']
823 source_repo = _form['source_repo']
824 source_ref = _form['source_ref']
824 source_ref = _form['source_ref']
825 target_repo = _form['target_repo']
825 target_repo = _form['target_repo']
826 target_ref = _form['target_ref']
826 target_ref = _form['target_ref']
827 commit_ids = _form['revisions'][::-1]
827 commit_ids = _form['revisions'][::-1]
828
828
829 # find the ancestor for this pr
829 # find the ancestor for this pr
830 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
830 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
831 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
831 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
832
832
833 # re-check permissions again here
833 # re-check permissions again here
834 # source_repo we must have read permissions
834 # source_repo we must have read permissions
835
835
836 source_perm = HasRepoPermissionAny(
836 source_perm = HasRepoPermissionAny(
837 'repository.read',
837 'repository.read',
838 'repository.write', 'repository.admin')(source_db_repo.repo_name)
838 'repository.write', 'repository.admin')(source_db_repo.repo_name)
839 if not source_perm:
839 if not source_perm:
840 msg = _('Not Enough permissions to source repo `{}`.'.format(
840 msg = _('Not Enough permissions to source repo `{}`.'.format(
841 source_db_repo.repo_name))
841 source_db_repo.repo_name))
842 h.flash(msg, category='error')
842 h.flash(msg, category='error')
843 # copy the args back to redirect
843 # copy the args back to redirect
844 org_query = self.request.GET.mixed()
844 org_query = self.request.GET.mixed()
845 raise HTTPFound(
845 raise HTTPFound(
846 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
846 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
847 _query=org_query))
847 _query=org_query))
848
848
849 # target repo we must have read permissions, and also later on
849 # target repo we must have read permissions, and also later on
850 # we want to check branch permissions here
850 # we want to check branch permissions here
851 target_perm = HasRepoPermissionAny(
851 target_perm = HasRepoPermissionAny(
852 'repository.read',
852 'repository.read',
853 'repository.write', 'repository.admin')(target_db_repo.repo_name)
853 'repository.write', 'repository.admin')(target_db_repo.repo_name)
854 if not target_perm:
854 if not target_perm:
855 msg = _('Not Enough permissions to target repo `{}`.'.format(
855 msg = _('Not Enough permissions to target repo `{}`.'.format(
856 target_db_repo.repo_name))
856 target_db_repo.repo_name))
857 h.flash(msg, category='error')
857 h.flash(msg, category='error')
858 # copy the args back to redirect
858 # copy the args back to redirect
859 org_query = self.request.GET.mixed()
859 org_query = self.request.GET.mixed()
860 raise HTTPFound(
860 raise HTTPFound(
861 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
861 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
862 _query=org_query))
862 _query=org_query))
863
863
864 source_scm = source_db_repo.scm_instance()
864 source_scm = source_db_repo.scm_instance()
865 target_scm = target_db_repo.scm_instance()
865 target_scm = target_db_repo.scm_instance()
866
866
867 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
867 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
868 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
868 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
869
869
870 ancestor = source_scm.get_common_ancestor(
870 ancestor = source_scm.get_common_ancestor(
871 source_commit.raw_id, target_commit.raw_id, target_scm)
871 source_commit.raw_id, target_commit.raw_id, target_scm)
872
872
873 # recalculate target ref based on ancestor
873 # recalculate target ref based on ancestor
874 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
874 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
875 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
875 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
876
876
877 get_default_reviewers_data, validate_default_reviewers = \
877 get_default_reviewers_data, validate_default_reviewers = \
878 PullRequestModel().get_reviewer_functions()
878 PullRequestModel().get_reviewer_functions()
879
879
880 # recalculate reviewers logic, to make sure we can validate this
880 # recalculate reviewers logic, to make sure we can validate this
881 reviewer_rules = get_default_reviewers_data(
881 reviewer_rules = get_default_reviewers_data(
882 self._rhodecode_db_user, source_db_repo,
882 self._rhodecode_db_user, source_db_repo,
883 source_commit, target_db_repo, target_commit)
883 source_commit, target_db_repo, target_commit)
884
884
885 given_reviewers = _form['review_members']
885 given_reviewers = _form['review_members']
886 reviewers = validate_default_reviewers(
886 reviewers = validate_default_reviewers(
887 given_reviewers, reviewer_rules)
887 given_reviewers, reviewer_rules)
888
888
889 pullrequest_title = _form['pullrequest_title']
889 pullrequest_title = _form['pullrequest_title']
890 title_source_ref = source_ref.split(':', 2)[1]
890 title_source_ref = source_ref.split(':', 2)[1]
891 if not pullrequest_title:
891 if not pullrequest_title:
892 pullrequest_title = PullRequestModel().generate_pullrequest_title(
892 pullrequest_title = PullRequestModel().generate_pullrequest_title(
893 source=source_repo,
893 source=source_repo,
894 source_ref=title_source_ref,
894 source_ref=title_source_ref,
895 target=target_repo
895 target=target_repo
896 )
896 )
897
897
898 description = _form['pullrequest_desc']
898 description = _form['pullrequest_desc']
899 description_renderer = _form['description_renderer']
899 description_renderer = _form['description_renderer']
900
900
901 try:
901 try:
902 pull_request = PullRequestModel().create(
902 pull_request = PullRequestModel().create(
903 created_by=self._rhodecode_user.user_id,
903 created_by=self._rhodecode_user.user_id,
904 source_repo=source_repo,
904 source_repo=source_repo,
905 source_ref=source_ref,
905 source_ref=source_ref,
906 target_repo=target_repo,
906 target_repo=target_repo,
907 target_ref=target_ref,
907 target_ref=target_ref,
908 revisions=commit_ids,
908 revisions=commit_ids,
909 reviewers=reviewers,
909 reviewers=reviewers,
910 title=pullrequest_title,
910 title=pullrequest_title,
911 description=description,
911 description=description,
912 description_renderer=description_renderer,
912 description_renderer=description_renderer,
913 reviewer_data=reviewer_rules,
913 reviewer_data=reviewer_rules,
914 auth_user=self._rhodecode_user
914 auth_user=self._rhodecode_user
915 )
915 )
916 Session().commit()
916 Session().commit()
917
917
918 h.flash(_('Successfully opened new pull request'),
918 h.flash(_('Successfully opened new pull request'),
919 category='success')
919 category='success')
920 except Exception:
920 except Exception:
921 msg = _('Error occurred during creation of this pull request.')
921 msg = _('Error occurred during creation of this pull request.')
922 log.exception(msg)
922 log.exception(msg)
923 h.flash(msg, category='error')
923 h.flash(msg, category='error')
924
924
925 # copy the args back to redirect
925 # copy the args back to redirect
926 org_query = self.request.GET.mixed()
926 org_query = self.request.GET.mixed()
927 raise HTTPFound(
927 raise HTTPFound(
928 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
928 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
929 _query=org_query))
929 _query=org_query))
930
930
931 raise HTTPFound(
931 raise HTTPFound(
932 h.route_path('pullrequest_show', repo_name=target_repo,
932 h.route_path('pullrequest_show', repo_name=target_repo,
933 pull_request_id=pull_request.pull_request_id))
933 pull_request_id=pull_request.pull_request_id))
934
934
935 @LoginRequired()
935 @LoginRequired()
936 @NotAnonymous()
936 @NotAnonymous()
937 @HasRepoPermissionAnyDecorator(
937 @HasRepoPermissionAnyDecorator(
938 'repository.read', 'repository.write', 'repository.admin')
938 'repository.read', 'repository.write', 'repository.admin')
939 @CSRFRequired()
939 @CSRFRequired()
940 @view_config(
940 @view_config(
941 route_name='pullrequest_update', request_method='POST',
941 route_name='pullrequest_update', request_method='POST',
942 renderer='json_ext')
942 renderer='json_ext')
943 def pull_request_update(self):
943 def pull_request_update(self):
944 pull_request = PullRequest.get_or_404(
944 pull_request = PullRequest.get_or_404(
945 self.request.matchdict['pull_request_id'])
945 self.request.matchdict['pull_request_id'])
946 _ = self.request.translate
946 _ = self.request.translate
947
947
948 self.load_default_context()
948 self.load_default_context()
949
949
950 if pull_request.is_closed():
950 if pull_request.is_closed():
951 log.debug('update: forbidden because pull request is closed')
951 log.debug('update: forbidden because pull request is closed')
952 msg = _(u'Cannot update closed pull requests.')
952 msg = _(u'Cannot update closed pull requests.')
953 h.flash(msg, category='error')
953 h.flash(msg, category='error')
954 return True
954 return True
955
955
956 # only owner or admin can update it
956 # only owner or admin can update it
957 allowed_to_update = PullRequestModel().check_user_update(
957 allowed_to_update = PullRequestModel().check_user_update(
958 pull_request, self._rhodecode_user)
958 pull_request, self._rhodecode_user)
959 if allowed_to_update:
959 if allowed_to_update:
960 controls = peppercorn.parse(self.request.POST.items())
960 controls = peppercorn.parse(self.request.POST.items())
961
961
962 if 'review_members' in controls:
962 if 'review_members' in controls:
963 self._update_reviewers(
963 self._update_reviewers(
964 pull_request, controls['review_members'],
964 pull_request, controls['review_members'],
965 pull_request.reviewer_data)
965 pull_request.reviewer_data)
966 elif str2bool(self.request.POST.get('update_commits', 'false')):
966 elif str2bool(self.request.POST.get('update_commits', 'false')):
967 self._update_commits(pull_request)
967 self._update_commits(pull_request)
968 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
968 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
969 self._edit_pull_request(pull_request)
969 self._edit_pull_request(pull_request)
970 else:
970 else:
971 raise HTTPBadRequest()
971 raise HTTPBadRequest()
972 return True
972 return True
973 raise HTTPForbidden()
973 raise HTTPForbidden()
974
974
975 def _edit_pull_request(self, pull_request):
975 def _edit_pull_request(self, pull_request):
976 _ = self.request.translate
976 _ = self.request.translate
977
977
978 try:
978 try:
979 PullRequestModel().edit(
979 PullRequestModel().edit(
980 pull_request,
980 pull_request,
981 self.request.POST.get('title'),
981 self.request.POST.get('title'),
982 self.request.POST.get('description'),
982 self.request.POST.get('description'),
983 self.request.POST.get('description_renderer'),
983 self.request.POST.get('description_renderer'),
984 self._rhodecode_user)
984 self._rhodecode_user)
985 except ValueError:
985 except ValueError:
986 msg = _(u'Cannot update closed pull requests.')
986 msg = _(u'Cannot update closed pull requests.')
987 h.flash(msg, category='error')
987 h.flash(msg, category='error')
988 return
988 return
989 else:
989 else:
990 Session().commit()
990 Session().commit()
991
991
992 msg = _(u'Pull request title & description updated.')
992 msg = _(u'Pull request title & description updated.')
993 h.flash(msg, category='success')
993 h.flash(msg, category='success')
994 return
994 return
995
995
996 def _update_commits(self, pull_request):
996 def _update_commits(self, pull_request):
997 _ = self.request.translate
997 _ = self.request.translate
998 resp = PullRequestModel().update_commits(pull_request)
998 resp = PullRequestModel().update_commits(pull_request)
999
999
1000 if resp.executed:
1000 if resp.executed:
1001
1001
1002 if resp.target_changed and resp.source_changed:
1002 if resp.target_changed and resp.source_changed:
1003 changed = 'target and source repositories'
1003 changed = 'target and source repositories'
1004 elif resp.target_changed and not resp.source_changed:
1004 elif resp.target_changed and not resp.source_changed:
1005 changed = 'target repository'
1005 changed = 'target repository'
1006 elif not resp.target_changed and resp.source_changed:
1006 elif not resp.target_changed and resp.source_changed:
1007 changed = 'source repository'
1007 changed = 'source repository'
1008 else:
1008 else:
1009 changed = 'nothing'
1009 changed = 'nothing'
1010
1010
1011 msg = _(
1011 msg = _(
1012 u'Pull request updated to "{source_commit_id}" with '
1012 u'Pull request updated to "{source_commit_id}" with '
1013 u'{count_added} added, {count_removed} removed commits. '
1013 u'{count_added} added, {count_removed} removed commits. '
1014 u'Source of changes: {change_source}')
1014 u'Source of changes: {change_source}')
1015 msg = msg.format(
1015 msg = msg.format(
1016 source_commit_id=pull_request.source_ref_parts.commit_id,
1016 source_commit_id=pull_request.source_ref_parts.commit_id,
1017 count_added=len(resp.changes.added),
1017 count_added=len(resp.changes.added),
1018 count_removed=len(resp.changes.removed),
1018 count_removed=len(resp.changes.removed),
1019 change_source=changed)
1019 change_source=changed)
1020 h.flash(msg, category='success')
1020 h.flash(msg, category='success')
1021
1021
1022 channel = '/repo${}$/pr/{}'.format(
1022 channel = '/repo${}$/pr/{}'.format(
1023 pull_request.target_repo.repo_name,
1023 pull_request.target_repo.repo_name,
1024 pull_request.pull_request_id)
1024 pull_request.pull_request_id)
1025 message = msg + (
1025 message = msg + (
1026 ' - <a onclick="window.location.reload()">'
1026 ' - <a onclick="window.location.reload()">'
1027 '<strong>{}</strong></a>'.format(_('Reload page')))
1027 '<strong>{}</strong></a>'.format(_('Reload page')))
1028 channelstream.post_message(
1028 channelstream.post_message(
1029 channel, message, self._rhodecode_user.username,
1029 channel, message, self._rhodecode_user.username,
1030 registry=self.request.registry)
1030 registry=self.request.registry)
1031 else:
1031 else:
1032 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1032 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1033 warning_reasons = [
1033 warning_reasons = [
1034 UpdateFailureReason.NO_CHANGE,
1034 UpdateFailureReason.NO_CHANGE,
1035 UpdateFailureReason.WRONG_REF_TYPE,
1035 UpdateFailureReason.WRONG_REF_TYPE,
1036 ]
1036 ]
1037 category = 'warning' if resp.reason in warning_reasons else 'error'
1037 category = 'warning' if resp.reason in warning_reasons else 'error'
1038 h.flash(msg, category=category)
1038 h.flash(msg, category=category)
1039
1039
1040 @LoginRequired()
1040 @LoginRequired()
1041 @NotAnonymous()
1041 @NotAnonymous()
1042 @HasRepoPermissionAnyDecorator(
1042 @HasRepoPermissionAnyDecorator(
1043 'repository.read', 'repository.write', 'repository.admin')
1043 'repository.read', 'repository.write', 'repository.admin')
1044 @CSRFRequired()
1044 @CSRFRequired()
1045 @view_config(
1045 @view_config(
1046 route_name='pullrequest_merge', request_method='POST',
1046 route_name='pullrequest_merge', request_method='POST',
1047 renderer='json_ext')
1047 renderer='json_ext')
1048 def pull_request_merge(self):
1048 def pull_request_merge(self):
1049 """
1049 """
1050 Merge will perform a server-side merge of the specified
1050 Merge will perform a server-side merge of the specified
1051 pull request, if the pull request is approved and mergeable.
1051 pull request, if the pull request is approved and mergeable.
1052 After successful merging, the pull request is automatically
1052 After successful merging, the pull request is automatically
1053 closed, with a relevant comment.
1053 closed, with a relevant comment.
1054 """
1054 """
1055 pull_request = PullRequest.get_or_404(
1055 pull_request = PullRequest.get_or_404(
1056 self.request.matchdict['pull_request_id'])
1056 self.request.matchdict['pull_request_id'])
1057
1057
1058 self.load_default_context()
1058 self.load_default_context()
1059 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1059 check = MergeCheck.validate(
1060 translator=self.request.translate)
1060 pull_request, auth_user=self._rhodecode_user,
1061 translator=self.request.translate)
1061 merge_possible = not check.failed
1062 merge_possible = not check.failed
1062
1063
1063 for err_type, error_msg in check.errors:
1064 for err_type, error_msg in check.errors:
1064 h.flash(error_msg, category=err_type)
1065 h.flash(error_msg, category=err_type)
1065
1066
1066 if merge_possible:
1067 if merge_possible:
1067 log.debug("Pre-conditions checked, trying to merge.")
1068 log.debug("Pre-conditions checked, trying to merge.")
1068 extras = vcs_operation_context(
1069 extras = vcs_operation_context(
1069 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1070 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1070 username=self._rhodecode_db_user.username, action='push',
1071 username=self._rhodecode_db_user.username, action='push',
1071 scm=pull_request.target_repo.repo_type)
1072 scm=pull_request.target_repo.repo_type)
1072 self._merge_pull_request(
1073 self._merge_pull_request(
1073 pull_request, self._rhodecode_db_user, extras)
1074 pull_request, self._rhodecode_db_user, extras)
1074 else:
1075 else:
1075 log.debug("Pre-conditions failed, NOT merging.")
1076 log.debug("Pre-conditions failed, NOT merging.")
1076
1077
1077 raise HTTPFound(
1078 raise HTTPFound(
1078 h.route_path('pullrequest_show',
1079 h.route_path('pullrequest_show',
1079 repo_name=pull_request.target_repo.repo_name,
1080 repo_name=pull_request.target_repo.repo_name,
1080 pull_request_id=pull_request.pull_request_id))
1081 pull_request_id=pull_request.pull_request_id))
1081
1082
1082 def _merge_pull_request(self, pull_request, user, extras):
1083 def _merge_pull_request(self, pull_request, user, extras):
1083 _ = self.request.translate
1084 _ = self.request.translate
1084 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1085 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1085
1086
1086 if merge_resp.executed:
1087 if merge_resp.executed:
1087 log.debug("The merge was successful, closing the pull request.")
1088 log.debug("The merge was successful, closing the pull request.")
1088 PullRequestModel().close_pull_request(
1089 PullRequestModel().close_pull_request(
1089 pull_request.pull_request_id, user)
1090 pull_request.pull_request_id, user)
1090 Session().commit()
1091 Session().commit()
1091 msg = _('Pull request was successfully merged and closed.')
1092 msg = _('Pull request was successfully merged and closed.')
1092 h.flash(msg, category='success')
1093 h.flash(msg, category='success')
1093 else:
1094 else:
1094 log.debug(
1095 log.debug(
1095 "The merge was not successful. Merge response: %s",
1096 "The merge was not successful. Merge response: %s",
1096 merge_resp)
1097 merge_resp)
1097 msg = PullRequestModel().merge_status_message(
1098 msg = PullRequestModel().merge_status_message(
1098 merge_resp.failure_reason)
1099 merge_resp.failure_reason)
1099 h.flash(msg, category='error')
1100 h.flash(msg, category='error')
1100
1101
1101 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1102 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1102 _ = self.request.translate
1103 _ = self.request.translate
1103 get_default_reviewers_data, validate_default_reviewers = \
1104 get_default_reviewers_data, validate_default_reviewers = \
1104 PullRequestModel().get_reviewer_functions()
1105 PullRequestModel().get_reviewer_functions()
1105
1106
1106 try:
1107 try:
1107 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1108 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1108 except ValueError as e:
1109 except ValueError as e:
1109 log.error('Reviewers Validation: {}'.format(e))
1110 log.error('Reviewers Validation: {}'.format(e))
1110 h.flash(e, category='error')
1111 h.flash(e, category='error')
1111 return
1112 return
1112
1113
1113 PullRequestModel().update_reviewers(
1114 PullRequestModel().update_reviewers(
1114 pull_request, reviewers, self._rhodecode_user)
1115 pull_request, reviewers, self._rhodecode_user)
1115 h.flash(_('Pull request reviewers updated.'), category='success')
1116 h.flash(_('Pull request reviewers updated.'), category='success')
1116 Session().commit()
1117 Session().commit()
1117
1118
1118 @LoginRequired()
1119 @LoginRequired()
1119 @NotAnonymous()
1120 @NotAnonymous()
1120 @HasRepoPermissionAnyDecorator(
1121 @HasRepoPermissionAnyDecorator(
1121 'repository.read', 'repository.write', 'repository.admin')
1122 'repository.read', 'repository.write', 'repository.admin')
1122 @CSRFRequired()
1123 @CSRFRequired()
1123 @view_config(
1124 @view_config(
1124 route_name='pullrequest_delete', request_method='POST',
1125 route_name='pullrequest_delete', request_method='POST',
1125 renderer='json_ext')
1126 renderer='json_ext')
1126 def pull_request_delete(self):
1127 def pull_request_delete(self):
1127 _ = self.request.translate
1128 _ = self.request.translate
1128
1129
1129 pull_request = PullRequest.get_or_404(
1130 pull_request = PullRequest.get_or_404(
1130 self.request.matchdict['pull_request_id'])
1131 self.request.matchdict['pull_request_id'])
1131 self.load_default_context()
1132 self.load_default_context()
1132
1133
1133 pr_closed = pull_request.is_closed()
1134 pr_closed = pull_request.is_closed()
1134 allowed_to_delete = PullRequestModel().check_user_delete(
1135 allowed_to_delete = PullRequestModel().check_user_delete(
1135 pull_request, self._rhodecode_user) and not pr_closed
1136 pull_request, self._rhodecode_user) and not pr_closed
1136
1137
1137 # only owner can delete it !
1138 # only owner can delete it !
1138 if allowed_to_delete:
1139 if allowed_to_delete:
1139 PullRequestModel().delete(pull_request, self._rhodecode_user)
1140 PullRequestModel().delete(pull_request, self._rhodecode_user)
1140 Session().commit()
1141 Session().commit()
1141 h.flash(_('Successfully deleted pull request'),
1142 h.flash(_('Successfully deleted pull request'),
1142 category='success')
1143 category='success')
1143 raise HTTPFound(h.route_path('pullrequest_show_all',
1144 raise HTTPFound(h.route_path('pullrequest_show_all',
1144 repo_name=self.db_repo_name))
1145 repo_name=self.db_repo_name))
1145
1146
1146 log.warning('user %s tried to delete pull request without access',
1147 log.warning('user %s tried to delete pull request without access',
1147 self._rhodecode_user)
1148 self._rhodecode_user)
1148 raise HTTPNotFound()
1149 raise HTTPNotFound()
1149
1150
1150 @LoginRequired()
1151 @LoginRequired()
1151 @NotAnonymous()
1152 @NotAnonymous()
1152 @HasRepoPermissionAnyDecorator(
1153 @HasRepoPermissionAnyDecorator(
1153 'repository.read', 'repository.write', 'repository.admin')
1154 'repository.read', 'repository.write', 'repository.admin')
1154 @CSRFRequired()
1155 @CSRFRequired()
1155 @view_config(
1156 @view_config(
1156 route_name='pullrequest_comment_create', request_method='POST',
1157 route_name='pullrequest_comment_create', request_method='POST',
1157 renderer='json_ext')
1158 renderer='json_ext')
1158 def pull_request_comment_create(self):
1159 def pull_request_comment_create(self):
1159 _ = self.request.translate
1160 _ = self.request.translate
1160
1161
1161 pull_request = PullRequest.get_or_404(
1162 pull_request = PullRequest.get_or_404(
1162 self.request.matchdict['pull_request_id'])
1163 self.request.matchdict['pull_request_id'])
1163 pull_request_id = pull_request.pull_request_id
1164 pull_request_id = pull_request.pull_request_id
1164
1165
1165 if pull_request.is_closed():
1166 if pull_request.is_closed():
1166 log.debug('comment: forbidden because pull request is closed')
1167 log.debug('comment: forbidden because pull request is closed')
1167 raise HTTPForbidden()
1168 raise HTTPForbidden()
1168
1169
1169 allowed_to_comment = PullRequestModel().check_user_comment(
1170 allowed_to_comment = PullRequestModel().check_user_comment(
1170 pull_request, self._rhodecode_user)
1171 pull_request, self._rhodecode_user)
1171 if not allowed_to_comment:
1172 if not allowed_to_comment:
1172 log.debug(
1173 log.debug(
1173 'comment: forbidden because pull request is from forbidden repo')
1174 'comment: forbidden because pull request is from forbidden repo')
1174 raise HTTPForbidden()
1175 raise HTTPForbidden()
1175
1176
1176 c = self.load_default_context()
1177 c = self.load_default_context()
1177
1178
1178 status = self.request.POST.get('changeset_status', None)
1179 status = self.request.POST.get('changeset_status', None)
1179 text = self.request.POST.get('text')
1180 text = self.request.POST.get('text')
1180 comment_type = self.request.POST.get('comment_type')
1181 comment_type = self.request.POST.get('comment_type')
1181 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1182 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1182 close_pull_request = self.request.POST.get('close_pull_request')
1183 close_pull_request = self.request.POST.get('close_pull_request')
1183
1184
1184 # the logic here should work like following, if we submit close
1185 # the logic here should work like following, if we submit close
1185 # pr comment, use `close_pull_request_with_comment` function
1186 # pr comment, use `close_pull_request_with_comment` function
1186 # else handle regular comment logic
1187 # else handle regular comment logic
1187
1188
1188 if close_pull_request:
1189 if close_pull_request:
1189 # only owner or admin or person with write permissions
1190 # only owner or admin or person with write permissions
1190 allowed_to_close = PullRequestModel().check_user_update(
1191 allowed_to_close = PullRequestModel().check_user_update(
1191 pull_request, self._rhodecode_user)
1192 pull_request, self._rhodecode_user)
1192 if not allowed_to_close:
1193 if not allowed_to_close:
1193 log.debug('comment: forbidden because not allowed to close '
1194 log.debug('comment: forbidden because not allowed to close '
1194 'pull request %s', pull_request_id)
1195 'pull request %s', pull_request_id)
1195 raise HTTPForbidden()
1196 raise HTTPForbidden()
1196 comment, status = PullRequestModel().close_pull_request_with_comment(
1197 comment, status = PullRequestModel().close_pull_request_with_comment(
1197 pull_request, self._rhodecode_user, self.db_repo, message=text)
1198 pull_request, self._rhodecode_user, self.db_repo, message=text)
1198 Session().flush()
1199 Session().flush()
1199 events.trigger(
1200 events.trigger(
1200 events.PullRequestCommentEvent(pull_request, comment))
1201 events.PullRequestCommentEvent(pull_request, comment))
1201
1202
1202 else:
1203 else:
1203 # regular comment case, could be inline, or one with status.
1204 # regular comment case, could be inline, or one with status.
1204 # for that one we check also permissions
1205 # for that one we check also permissions
1205
1206
1206 allowed_to_change_status = PullRequestModel().check_user_change_status(
1207 allowed_to_change_status = PullRequestModel().check_user_change_status(
1207 pull_request, self._rhodecode_user)
1208 pull_request, self._rhodecode_user)
1208
1209
1209 if status and allowed_to_change_status:
1210 if status and allowed_to_change_status:
1210 message = (_('Status change %(transition_icon)s %(status)s')
1211 message = (_('Status change %(transition_icon)s %(status)s')
1211 % {'transition_icon': '>',
1212 % {'transition_icon': '>',
1212 'status': ChangesetStatus.get_status_lbl(status)})
1213 'status': ChangesetStatus.get_status_lbl(status)})
1213 text = text or message
1214 text = text or message
1214
1215
1215 comment = CommentsModel().create(
1216 comment = CommentsModel().create(
1216 text=text,
1217 text=text,
1217 repo=self.db_repo.repo_id,
1218 repo=self.db_repo.repo_id,
1218 user=self._rhodecode_user.user_id,
1219 user=self._rhodecode_user.user_id,
1219 pull_request=pull_request,
1220 pull_request=pull_request,
1220 f_path=self.request.POST.get('f_path'),
1221 f_path=self.request.POST.get('f_path'),
1221 line_no=self.request.POST.get('line'),
1222 line_no=self.request.POST.get('line'),
1222 status_change=(ChangesetStatus.get_status_lbl(status)
1223 status_change=(ChangesetStatus.get_status_lbl(status)
1223 if status and allowed_to_change_status else None),
1224 if status and allowed_to_change_status else None),
1224 status_change_type=(status
1225 status_change_type=(status
1225 if status and allowed_to_change_status else None),
1226 if status and allowed_to_change_status else None),
1226 comment_type=comment_type,
1227 comment_type=comment_type,
1227 resolves_comment_id=resolves_comment_id,
1228 resolves_comment_id=resolves_comment_id,
1228 auth_user=self._rhodecode_user
1229 auth_user=self._rhodecode_user
1229 )
1230 )
1230
1231
1231 if allowed_to_change_status:
1232 if allowed_to_change_status:
1232 # calculate old status before we change it
1233 # calculate old status before we change it
1233 old_calculated_status = pull_request.calculated_review_status()
1234 old_calculated_status = pull_request.calculated_review_status()
1234
1235
1235 # get status if set !
1236 # get status if set !
1236 if status:
1237 if status:
1237 ChangesetStatusModel().set_status(
1238 ChangesetStatusModel().set_status(
1238 self.db_repo.repo_id,
1239 self.db_repo.repo_id,
1239 status,
1240 status,
1240 self._rhodecode_user.user_id,
1241 self._rhodecode_user.user_id,
1241 comment,
1242 comment,
1242 pull_request=pull_request
1243 pull_request=pull_request
1243 )
1244 )
1244
1245
1245 Session().flush()
1246 Session().flush()
1246 # this is somehow required to get access to some relationship
1247 # this is somehow required to get access to some relationship
1247 # loaded on comment
1248 # loaded on comment
1248 Session().refresh(comment)
1249 Session().refresh(comment)
1249
1250
1250 events.trigger(
1251 events.trigger(
1251 events.PullRequestCommentEvent(pull_request, comment))
1252 events.PullRequestCommentEvent(pull_request, comment))
1252
1253
1253 # we now calculate the status of pull request, and based on that
1254 # we now calculate the status of pull request, and based on that
1254 # calculation we set the commits status
1255 # calculation we set the commits status
1255 calculated_status = pull_request.calculated_review_status()
1256 calculated_status = pull_request.calculated_review_status()
1256 if old_calculated_status != calculated_status:
1257 if old_calculated_status != calculated_status:
1257 PullRequestModel()._trigger_pull_request_hook(
1258 PullRequestModel()._trigger_pull_request_hook(
1258 pull_request, self._rhodecode_user, 'review_status_change')
1259 pull_request, self._rhodecode_user, 'review_status_change')
1259
1260
1260 Session().commit()
1261 Session().commit()
1261
1262
1262 data = {
1263 data = {
1263 'target_id': h.safeid(h.safe_unicode(
1264 'target_id': h.safeid(h.safe_unicode(
1264 self.request.POST.get('f_path'))),
1265 self.request.POST.get('f_path'))),
1265 }
1266 }
1266 if comment:
1267 if comment:
1267 c.co = comment
1268 c.co = comment
1268 rendered_comment = render(
1269 rendered_comment = render(
1269 'rhodecode:templates/changeset/changeset_comment_block.mako',
1270 'rhodecode:templates/changeset/changeset_comment_block.mako',
1270 self._get_template_context(c), self.request)
1271 self._get_template_context(c), self.request)
1271
1272
1272 data.update(comment.get_dict())
1273 data.update(comment.get_dict())
1273 data.update({'rendered_text': rendered_comment})
1274 data.update({'rendered_text': rendered_comment})
1274
1275
1275 return data
1276 return data
1276
1277
1277 @LoginRequired()
1278 @LoginRequired()
1278 @NotAnonymous()
1279 @NotAnonymous()
1279 @HasRepoPermissionAnyDecorator(
1280 @HasRepoPermissionAnyDecorator(
1280 'repository.read', 'repository.write', 'repository.admin')
1281 'repository.read', 'repository.write', 'repository.admin')
1281 @CSRFRequired()
1282 @CSRFRequired()
1282 @view_config(
1283 @view_config(
1283 route_name='pullrequest_comment_delete', request_method='POST',
1284 route_name='pullrequest_comment_delete', request_method='POST',
1284 renderer='json_ext')
1285 renderer='json_ext')
1285 def pull_request_comment_delete(self):
1286 def pull_request_comment_delete(self):
1286 pull_request = PullRequest.get_or_404(
1287 pull_request = PullRequest.get_or_404(
1287 self.request.matchdict['pull_request_id'])
1288 self.request.matchdict['pull_request_id'])
1288
1289
1289 comment = ChangesetComment.get_or_404(
1290 comment = ChangesetComment.get_or_404(
1290 self.request.matchdict['comment_id'])
1291 self.request.matchdict['comment_id'])
1291 comment_id = comment.comment_id
1292 comment_id = comment.comment_id
1292
1293
1293 if pull_request.is_closed():
1294 if pull_request.is_closed():
1294 log.debug('comment: forbidden because pull request is closed')
1295 log.debug('comment: forbidden because pull request is closed')
1295 raise HTTPForbidden()
1296 raise HTTPForbidden()
1296
1297
1297 if not comment:
1298 if not comment:
1298 log.debug('Comment with id:%s not found, skipping', comment_id)
1299 log.debug('Comment with id:%s not found, skipping', comment_id)
1299 # comment already deleted in another call probably
1300 # comment already deleted in another call probably
1300 return True
1301 return True
1301
1302
1302 if comment.pull_request.is_closed():
1303 if comment.pull_request.is_closed():
1303 # don't allow deleting comments on closed pull request
1304 # don't allow deleting comments on closed pull request
1304 raise HTTPForbidden()
1305 raise HTTPForbidden()
1305
1306
1306 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1307 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1307 super_admin = h.HasPermissionAny('hg.admin')()
1308 super_admin = h.HasPermissionAny('hg.admin')()
1308 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1309 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1309 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1310 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1310 comment_repo_admin = is_repo_admin and is_repo_comment
1311 comment_repo_admin = is_repo_admin and is_repo_comment
1311
1312
1312 if super_admin or comment_owner or comment_repo_admin:
1313 if super_admin or comment_owner or comment_repo_admin:
1313 old_calculated_status = comment.pull_request.calculated_review_status()
1314 old_calculated_status = comment.pull_request.calculated_review_status()
1314 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1315 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1315 Session().commit()
1316 Session().commit()
1316 calculated_status = comment.pull_request.calculated_review_status()
1317 calculated_status = comment.pull_request.calculated_review_status()
1317 if old_calculated_status != calculated_status:
1318 if old_calculated_status != calculated_status:
1318 PullRequestModel()._trigger_pull_request_hook(
1319 PullRequestModel()._trigger_pull_request_hook(
1319 comment.pull_request, self._rhodecode_user, 'review_status_change')
1320 comment.pull_request, self._rhodecode_user, 'review_status_change')
1320 return True
1321 return True
1321 else:
1322 else:
1322 log.warning('No permissions for user %s to delete comment_id: %s',
1323 log.warning('No permissions for user %s to delete comment_id: %s',
1323 self._rhodecode_db_user, comment_id)
1324 self._rhodecode_db_user, comment_id)
1324 raise HTTPNotFound()
1325 raise HTTPNotFound()
@@ -1,1707 +1,1726 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext#, _
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
66 # request update.
66 # request update.
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
70
70
71
71
72 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
73
73
74 cls = PullRequest
74 cls = PullRequest
75
75
76 DIFF_CONTEXT = 3
76 DIFF_CONTEXT = 3
77
77
78 MERGE_STATUS_MESSAGES = {
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
87 'This pull request could not be merged because push to target'
88 ' failed.'),
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
112 'to sub repositories.'),
113 }
113 }
114
114
115 UPDATE_STATUS_MESSAGES = {
115 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
116 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
117 'Pull request update successful.'),
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 'Pull request update failed because of an unknown error.'),
119 'Pull request update failed because of an unknown error.'),
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 'No update needed because the source and target have not changed.'),
121 'No update needed because the source and target have not changed.'),
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 'Pull request cannot be updated because the reference type is '
123 'Pull request cannot be updated because the reference type is '
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 'This pull request cannot be updated because the target '
126 'This pull request cannot be updated because the target '
127 'reference is missing.'),
127 'reference is missing.'),
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 'This pull request cannot be updated because the source '
129 'This pull request cannot be updated because the source '
130 'reference is missing.'),
130 'reference is missing.'),
131 }
131 }
132
132
133 def __get_pull_request(self, pull_request):
133 def __get_pull_request(self, pull_request):
134 return self._get_instance((
134 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
135 PullRequest, PullRequestVersion), pull_request)
136
136
137 def _check_perms(self, perms, pull_request, user, api=False):
137 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
138 if not api:
139 return h.HasRepoPermissionAny(*perms)(
139 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
140 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
141 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
142 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
143 user=user, repo_name=pull_request.target_repo.repo_name)
144
144
145 def check_user_read(self, pull_request, user, api=False):
145 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
147 return self._check_perms(_perms, pull_request, user, api)
148
148
149 def check_user_merge(self, pull_request, user, api=False):
149 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
151 return self._check_perms(_perms, pull_request, user, api)
152
152
153 def check_user_update(self, pull_request, user, api=False):
153 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
154 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
155 return self.check_user_merge(pull_request, user, api) or owner
156
156
157 def check_user_delete(self, pull_request, user):
157 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
158 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
159 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
160 return self._check_perms(_perms, pull_request, user) or owner
161
161
162 def check_user_change_status(self, pull_request, user, api=False):
162 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
163 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
164 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
165 return self.check_user_update(pull_request, user, api) or reviewer
166
166
167 def check_user_comment(self, pull_request, user):
167 def check_user_comment(self, pull_request, user):
168 owner = user.user_id == pull_request.user_id
168 owner = user.user_id == pull_request.user_id
169 return self.check_user_read(pull_request, user) or owner
169 return self.check_user_read(pull_request, user) or owner
170
170
171 def get(self, pull_request):
171 def get(self, pull_request):
172 return self.__get_pull_request(pull_request)
172 return self.__get_pull_request(pull_request)
173
173
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 opened_by=None, order_by=None,
175 opened_by=None, order_by=None,
176 order_dir='desc'):
176 order_dir='desc'):
177 repo = None
177 repo = None
178 if repo_name:
178 if repo_name:
179 repo = self._get_repo(repo_name)
179 repo = self._get_repo(repo_name)
180
180
181 q = PullRequest.query()
181 q = PullRequest.query()
182
182
183 # source or target
183 # source or target
184 if repo and source:
184 if repo and source:
185 q = q.filter(PullRequest.source_repo == repo)
185 q = q.filter(PullRequest.source_repo == repo)
186 elif repo:
186 elif repo:
187 q = q.filter(PullRequest.target_repo == repo)
187 q = q.filter(PullRequest.target_repo == repo)
188
188
189 # closed,opened
189 # closed,opened
190 if statuses:
190 if statuses:
191 q = q.filter(PullRequest.status.in_(statuses))
191 q = q.filter(PullRequest.status.in_(statuses))
192
192
193 # opened by filter
193 # opened by filter
194 if opened_by:
194 if opened_by:
195 q = q.filter(PullRequest.user_id.in_(opened_by))
195 q = q.filter(PullRequest.user_id.in_(opened_by))
196
196
197 if order_by:
197 if order_by:
198 order_map = {
198 order_map = {
199 'name_raw': PullRequest.pull_request_id,
199 'name_raw': PullRequest.pull_request_id,
200 'title': PullRequest.title,
200 'title': PullRequest.title,
201 'updated_on_raw': PullRequest.updated_on,
201 'updated_on_raw': PullRequest.updated_on,
202 'target_repo': PullRequest.target_repo_id
202 'target_repo': PullRequest.target_repo_id
203 }
203 }
204 if order_dir == 'asc':
204 if order_dir == 'asc':
205 q = q.order_by(order_map[order_by].asc())
205 q = q.order_by(order_map[order_by].asc())
206 else:
206 else:
207 q = q.order_by(order_map[order_by].desc())
207 q = q.order_by(order_map[order_by].desc())
208
208
209 return q
209 return q
210
210
211 def count_all(self, repo_name, source=False, statuses=None,
211 def count_all(self, repo_name, source=False, statuses=None,
212 opened_by=None):
212 opened_by=None):
213 """
213 """
214 Count the number of pull requests for a specific repository.
214 Count the number of pull requests for a specific repository.
215
215
216 :param repo_name: target or source repo
216 :param repo_name: target or source repo
217 :param source: boolean flag to specify if repo_name refers to source
217 :param source: boolean flag to specify if repo_name refers to source
218 :param statuses: list of pull request statuses
218 :param statuses: list of pull request statuses
219 :param opened_by: author user of the pull request
219 :param opened_by: author user of the pull request
220 :returns: int number of pull requests
220 :returns: int number of pull requests
221 """
221 """
222 q = self._prepare_get_all_query(
222 q = self._prepare_get_all_query(
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224
224
225 return q.count()
225 return q.count()
226
226
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 offset=0, length=None, order_by=None, order_dir='desc'):
228 offset=0, length=None, order_by=None, order_dir='desc'):
229 """
229 """
230 Get all pull requests for a specific repository.
230 Get all pull requests for a specific repository.
231
231
232 :param repo_name: target or source repo
232 :param repo_name: target or source repo
233 :param source: boolean flag to specify if repo_name refers to source
233 :param source: boolean flag to specify if repo_name refers to source
234 :param statuses: list of pull request statuses
234 :param statuses: list of pull request statuses
235 :param opened_by: author user of the pull request
235 :param opened_by: author user of the pull request
236 :param offset: pagination offset
236 :param offset: pagination offset
237 :param length: length of returned list
237 :param length: length of returned list
238 :param order_by: order of the returned list
238 :param order_by: order of the returned list
239 :param order_dir: 'asc' or 'desc' ordering direction
239 :param order_dir: 'asc' or 'desc' ordering direction
240 :returns: list of pull requests
240 :returns: list of pull requests
241 """
241 """
242 q = self._prepare_get_all_query(
242 q = self._prepare_get_all_query(
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 order_by=order_by, order_dir=order_dir)
244 order_by=order_by, order_dir=order_dir)
245
245
246 if length:
246 if length:
247 pull_requests = q.limit(length).offset(offset).all()
247 pull_requests = q.limit(length).offset(offset).all()
248 else:
248 else:
249 pull_requests = q.all()
249 pull_requests = q.all()
250
250
251 return pull_requests
251 return pull_requests
252
252
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 opened_by=None):
254 opened_by=None):
255 """
255 """
256 Count the number of pull requests for a specific repository that are
256 Count the number of pull requests for a specific repository that are
257 awaiting review.
257 awaiting review.
258
258
259 :param repo_name: target or source repo
259 :param repo_name: target or source repo
260 :param source: boolean flag to specify if repo_name refers to source
260 :param source: boolean flag to specify if repo_name refers to source
261 :param statuses: list of pull request statuses
261 :param statuses: list of pull request statuses
262 :param opened_by: author user of the pull request
262 :param opened_by: author user of the pull request
263 :returns: int number of pull requests
263 :returns: int number of pull requests
264 """
264 """
265 pull_requests = self.get_awaiting_review(
265 pull_requests = self.get_awaiting_review(
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267
267
268 return len(pull_requests)
268 return len(pull_requests)
269
269
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 opened_by=None, offset=0, length=None,
271 opened_by=None, offset=0, length=None,
272 order_by=None, order_dir='desc'):
272 order_by=None, order_dir='desc'):
273 """
273 """
274 Get all pull requests for a specific repository that are awaiting
274 Get all pull requests for a specific repository that are awaiting
275 review.
275 review.
276
276
277 :param repo_name: target or source repo
277 :param repo_name: target or source repo
278 :param source: boolean flag to specify if repo_name refers to source
278 :param source: boolean flag to specify if repo_name refers to source
279 :param statuses: list of pull request statuses
279 :param statuses: list of pull request statuses
280 :param opened_by: author user of the pull request
280 :param opened_by: author user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _filtered_pull_requests = []
291 _filtered_pull_requests = []
292 for pr in pull_requests:
292 for pr in pull_requests:
293 status = pr.calculated_review_status()
293 status = pr.calculated_review_status()
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 _filtered_pull_requests.append(pr)
296 _filtered_pull_requests.append(pr)
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 opened_by=None, user_id=None):
303 opened_by=None, user_id=None):
304 """
304 """
305 Count the number of pull requests for a specific repository that are
305 Count the number of pull requests for a specific repository that are
306 awaiting review from a specific user.
306 awaiting review from a specific user.
307
307
308 :param repo_name: target or source repo
308 :param repo_name: target or source repo
309 :param source: boolean flag to specify if repo_name refers to source
309 :param source: boolean flag to specify if repo_name refers to source
310 :param statuses: list of pull request statuses
310 :param statuses: list of pull request statuses
311 :param opened_by: author user of the pull request
311 :param opened_by: author user of the pull request
312 :param user_id: reviewer user of the pull request
312 :param user_id: reviewer user of the pull request
313 :returns: int number of pull requests
313 :returns: int number of pull requests
314 """
314 """
315 pull_requests = self.get_awaiting_my_review(
315 pull_requests = self.get_awaiting_my_review(
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 user_id=user_id)
317 user_id=user_id)
318
318
319 return len(pull_requests)
319 return len(pull_requests)
320
320
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 opened_by=None, user_id=None, offset=0,
322 opened_by=None, user_id=None, offset=0,
323 length=None, order_by=None, order_dir='desc'):
323 length=None, order_by=None, order_dir='desc'):
324 """
324 """
325 Get all pull requests for a specific repository that are awaiting
325 Get all pull requests for a specific repository that are awaiting
326 review from a specific user.
326 review from a specific user.
327
327
328 :param repo_name: target or source repo
328 :param repo_name: target or source repo
329 :param source: boolean flag to specify if repo_name refers to source
329 :param source: boolean flag to specify if repo_name refers to source
330 :param statuses: list of pull request statuses
330 :param statuses: list of pull request statuses
331 :param opened_by: author user of the pull request
331 :param opened_by: author user of the pull request
332 :param user_id: reviewer user of the pull request
332 :param user_id: reviewer user of the pull request
333 :param offset: pagination offset
333 :param offset: pagination offset
334 :param length: length of returned list
334 :param length: length of returned list
335 :param order_by: order of the returned list
335 :param order_by: order of the returned list
336 :param order_dir: 'asc' or 'desc' ordering direction
336 :param order_dir: 'asc' or 'desc' ordering direction
337 :returns: list of pull requests
337 :returns: list of pull requests
338 """
338 """
339 pull_requests = self.get_all(
339 pull_requests = self.get_all(
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 order_by=order_by, order_dir=order_dir)
341 order_by=order_by, order_dir=order_dir)
342
342
343 _my = PullRequestModel().get_not_reviewed(user_id)
343 _my = PullRequestModel().get_not_reviewed(user_id)
344 my_participation = []
344 my_participation = []
345 for pr in pull_requests:
345 for pr in pull_requests:
346 if pr in _my:
346 if pr in _my:
347 my_participation.append(pr)
347 my_participation.append(pr)
348 _filtered_pull_requests = my_participation
348 _filtered_pull_requests = my_participation
349 if length:
349 if length:
350 return _filtered_pull_requests[offset:offset+length]
350 return _filtered_pull_requests[offset:offset+length]
351 else:
351 else:
352 return _filtered_pull_requests
352 return _filtered_pull_requests
353
353
354 def get_not_reviewed(self, user_id):
354 def get_not_reviewed(self, user_id):
355 return [
355 return [
356 x.pull_request for x in PullRequestReviewers.query().filter(
356 x.pull_request for x in PullRequestReviewers.query().filter(
357 PullRequestReviewers.user_id == user_id).all()
357 PullRequestReviewers.user_id == user_id).all()
358 ]
358 ]
359
359
360 def _prepare_participating_query(self, user_id=None, statuses=None,
360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 order_by=None, order_dir='desc'):
361 order_by=None, order_dir='desc'):
362 q = PullRequest.query()
362 q = PullRequest.query()
363 if user_id:
363 if user_id:
364 reviewers_subquery = Session().query(
364 reviewers_subquery = Session().query(
365 PullRequestReviewers.pull_request_id).filter(
365 PullRequestReviewers.pull_request_id).filter(
366 PullRequestReviewers.user_id == user_id).subquery()
366 PullRequestReviewers.user_id == user_id).subquery()
367 user_filter = or_(
367 user_filter = or_(
368 PullRequest.user_id == user_id,
368 PullRequest.user_id == user_id,
369 PullRequest.pull_request_id.in_(reviewers_subquery)
369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 )
370 )
371 q = PullRequest.query().filter(user_filter)
371 q = PullRequest.query().filter(user_filter)
372
372
373 # closed,opened
373 # closed,opened
374 if statuses:
374 if statuses:
375 q = q.filter(PullRequest.status.in_(statuses))
375 q = q.filter(PullRequest.status.in_(statuses))
376
376
377 if order_by:
377 if order_by:
378 order_map = {
378 order_map = {
379 'name_raw': PullRequest.pull_request_id,
379 'name_raw': PullRequest.pull_request_id,
380 'title': PullRequest.title,
380 'title': PullRequest.title,
381 'updated_on_raw': PullRequest.updated_on,
381 'updated_on_raw': PullRequest.updated_on,
382 'target_repo': PullRequest.target_repo_id
382 'target_repo': PullRequest.target_repo_id
383 }
383 }
384 if order_dir == 'asc':
384 if order_dir == 'asc':
385 q = q.order_by(order_map[order_by].asc())
385 q = q.order_by(order_map[order_by].asc())
386 else:
386 else:
387 q = q.order_by(order_map[order_by].desc())
387 q = q.order_by(order_map[order_by].desc())
388
388
389 return q
389 return q
390
390
391 def count_im_participating_in(self, user_id=None, statuses=None):
391 def count_im_participating_in(self, user_id=None, statuses=None):
392 q = self._prepare_participating_query(user_id, statuses=statuses)
392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 return q.count()
393 return q.count()
394
394
395 def get_im_participating_in(
395 def get_im_participating_in(
396 self, user_id=None, statuses=None, offset=0,
396 self, user_id=None, statuses=None, offset=0,
397 length=None, order_by=None, order_dir='desc'):
397 length=None, order_by=None, order_dir='desc'):
398 """
398 """
399 Get all Pull requests that i'm participating in, or i have opened
399 Get all Pull requests that i'm participating in, or i have opened
400 """
400 """
401
401
402 q = self._prepare_participating_query(
402 q = self._prepare_participating_query(
403 user_id, statuses=statuses, order_by=order_by,
403 user_id, statuses=statuses, order_by=order_by,
404 order_dir=order_dir)
404 order_dir=order_dir)
405
405
406 if length:
406 if length:
407 pull_requests = q.limit(length).offset(offset).all()
407 pull_requests = q.limit(length).offset(offset).all()
408 else:
408 else:
409 pull_requests = q.all()
409 pull_requests = q.all()
410
410
411 return pull_requests
411 return pull_requests
412
412
413 def get_versions(self, pull_request):
413 def get_versions(self, pull_request):
414 """
414 """
415 returns version of pull request sorted by ID descending
415 returns version of pull request sorted by ID descending
416 """
416 """
417 return PullRequestVersion.query()\
417 return PullRequestVersion.query()\
418 .filter(PullRequestVersion.pull_request == pull_request)\
418 .filter(PullRequestVersion.pull_request == pull_request)\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 .all()
420 .all()
421
421
422 def get_pr_version(self, pull_request_id, version=None):
422 def get_pr_version(self, pull_request_id, version=None):
423 at_version = None
423 at_version = None
424
424
425 if version and version == 'latest':
425 if version and version == 'latest':
426 pull_request_ver = PullRequest.get(pull_request_id)
426 pull_request_ver = PullRequest.get(pull_request_id)
427 pull_request_obj = pull_request_ver
427 pull_request_obj = pull_request_ver
428 _org_pull_request_obj = pull_request_obj
428 _org_pull_request_obj = pull_request_obj
429 at_version = 'latest'
429 at_version = 'latest'
430 elif version:
430 elif version:
431 pull_request_ver = PullRequestVersion.get_or_404(version)
431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 pull_request_obj = pull_request_ver
432 pull_request_obj = pull_request_ver
433 _org_pull_request_obj = pull_request_ver.pull_request
433 _org_pull_request_obj = pull_request_ver.pull_request
434 at_version = pull_request_ver.pull_request_version_id
434 at_version = pull_request_ver.pull_request_version_id
435 else:
435 else:
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 pull_request_id)
437 pull_request_id)
438
438
439 pull_request_display_obj = PullRequest.get_pr_display_object(
439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 pull_request_obj, _org_pull_request_obj)
440 pull_request_obj, _org_pull_request_obj)
441
441
442 return _org_pull_request_obj, pull_request_obj, \
442 return _org_pull_request_obj, pull_request_obj, \
443 pull_request_display_obj, at_version
443 pull_request_display_obj, at_version
444
444
445 def create(self, created_by, source_repo, source_ref, target_repo,
445 def create(self, created_by, source_repo, source_ref, target_repo,
446 target_ref, revisions, reviewers, title, description=None,
446 target_ref, revisions, reviewers, title, description=None,
447 description_renderer=None,
447 description_renderer=None,
448 reviewer_data=None, translator=None, auth_user=None):
448 reviewer_data=None, translator=None, auth_user=None):
449 translator = translator or get_current_request().translate
449 translator = translator or get_current_request().translate
450
450
451 created_by_user = self._get_user(created_by)
451 created_by_user = self._get_user(created_by)
452 auth_user = auth_user or created_by_user
452 auth_user = auth_user or created_by_user.AuthUser()
453 source_repo = self._get_repo(source_repo)
453 source_repo = self._get_repo(source_repo)
454 target_repo = self._get_repo(target_repo)
454 target_repo = self._get_repo(target_repo)
455
455
456 pull_request = PullRequest()
456 pull_request = PullRequest()
457 pull_request.source_repo = source_repo
457 pull_request.source_repo = source_repo
458 pull_request.source_ref = source_ref
458 pull_request.source_ref = source_ref
459 pull_request.target_repo = target_repo
459 pull_request.target_repo = target_repo
460 pull_request.target_ref = target_ref
460 pull_request.target_ref = target_ref
461 pull_request.revisions = revisions
461 pull_request.revisions = revisions
462 pull_request.title = title
462 pull_request.title = title
463 pull_request.description = description
463 pull_request.description = description
464 pull_request.description_renderer = description_renderer
464 pull_request.description_renderer = description_renderer
465 pull_request.author = created_by_user
465 pull_request.author = created_by_user
466 pull_request.reviewer_data = reviewer_data
466 pull_request.reviewer_data = reviewer_data
467
467
468 Session().add(pull_request)
468 Session().add(pull_request)
469 Session().flush()
469 Session().flush()
470
470
471 reviewer_ids = set()
471 reviewer_ids = set()
472 # members / reviewers
472 # members / reviewers
473 for reviewer_object in reviewers:
473 for reviewer_object in reviewers:
474 user_id, reasons, mandatory, rules = reviewer_object
474 user_id, reasons, mandatory, rules = reviewer_object
475 user = self._get_user(user_id)
475 user = self._get_user(user_id)
476
476
477 # skip duplicates
477 # skip duplicates
478 if user.user_id in reviewer_ids:
478 if user.user_id in reviewer_ids:
479 continue
479 continue
480
480
481 reviewer_ids.add(user.user_id)
481 reviewer_ids.add(user.user_id)
482
482
483 reviewer = PullRequestReviewers()
483 reviewer = PullRequestReviewers()
484 reviewer.user = user
484 reviewer.user = user
485 reviewer.pull_request = pull_request
485 reviewer.pull_request = pull_request
486 reviewer.reasons = reasons
486 reviewer.reasons = reasons
487 reviewer.mandatory = mandatory
487 reviewer.mandatory = mandatory
488
488
489 # NOTE(marcink): pick only first rule for now
489 # NOTE(marcink): pick only first rule for now
490 rule_id = list(rules)[0] if rules else None
490 rule_id = list(rules)[0] if rules else None
491 rule = RepoReviewRule.get(rule_id) if rule_id else None
491 rule = RepoReviewRule.get(rule_id) if rule_id else None
492 if rule:
492 if rule:
493 review_group = rule.user_group_vote_rule(user_id)
493 review_group = rule.user_group_vote_rule(user_id)
494 # we check if this particular reviewer is member of a voting group
494 # we check if this particular reviewer is member of a voting group
495 if review_group:
495 if review_group:
496 # NOTE(marcink):
496 # NOTE(marcink):
497 # can be that user is member of more but we pick the first same,
497 # can be that user is member of more but we pick the first same,
498 # same as default reviewers algo
498 # same as default reviewers algo
499 review_group = review_group[0]
499 review_group = review_group[0]
500
500
501 rule_data = {
501 rule_data = {
502 'rule_name':
502 'rule_name':
503 rule.review_rule_name,
503 rule.review_rule_name,
504 'rule_user_group_entry_id':
504 'rule_user_group_entry_id':
505 review_group.repo_review_rule_users_group_id,
505 review_group.repo_review_rule_users_group_id,
506 'rule_user_group_name':
506 'rule_user_group_name':
507 review_group.users_group.users_group_name,
507 review_group.users_group.users_group_name,
508 'rule_user_group_members':
508 'rule_user_group_members':
509 [x.user.username for x in review_group.users_group.members],
509 [x.user.username for x in review_group.users_group.members],
510 'rule_user_group_members_id':
510 'rule_user_group_members_id':
511 [x.user.user_id for x in review_group.users_group.members],
511 [x.user.user_id for x in review_group.users_group.members],
512 }
512 }
513 # e.g {'vote_rule': -1, 'mandatory': True}
513 # e.g {'vote_rule': -1, 'mandatory': True}
514 rule_data.update(review_group.rule_data())
514 rule_data.update(review_group.rule_data())
515
515
516 reviewer.rule_data = rule_data
516 reviewer.rule_data = rule_data
517
517
518 Session().add(reviewer)
518 Session().add(reviewer)
519 Session().flush()
519 Session().flush()
520
520
521 # Set approval status to "Under Review" for all commits which are
521 # Set approval status to "Under Review" for all commits which are
522 # part of this pull request.
522 # part of this pull request.
523 ChangesetStatusModel().set_status(
523 ChangesetStatusModel().set_status(
524 repo=target_repo,
524 repo=target_repo,
525 status=ChangesetStatus.STATUS_UNDER_REVIEW,
525 status=ChangesetStatus.STATUS_UNDER_REVIEW,
526 user=created_by_user,
526 user=created_by_user,
527 pull_request=pull_request
527 pull_request=pull_request
528 )
528 )
529 # we commit early at this point. This has to do with a fact
529 # we commit early at this point. This has to do with a fact
530 # that before queries do some row-locking. And because of that
530 # that before queries do some row-locking. And because of that
531 # we need to commit and finish transation before below validate call
531 # we need to commit and finish transation before below validate call
532 # that for large repos could be long resulting in long row locks
532 # that for large repos could be long resulting in long row locks
533 Session().commit()
533 Session().commit()
534
534
535 # prepare workspace, and run initial merge simulation
535 # prepare workspace, and run initial merge simulation
536 MergeCheck.validate(
536 MergeCheck.validate(
537 pull_request, user=created_by_user, translator=translator)
537 pull_request, auth_user=auth_user, translator=translator)
538
538
539 self.notify_reviewers(pull_request, reviewer_ids)
539 self.notify_reviewers(pull_request, reviewer_ids)
540 self._trigger_pull_request_hook(
540 self._trigger_pull_request_hook(
541 pull_request, created_by_user, 'create')
541 pull_request, created_by_user, 'create')
542
542
543 creation_data = pull_request.get_api_data(with_merge_state=False)
543 creation_data = pull_request.get_api_data(with_merge_state=False)
544 self._log_audit_action(
544 self._log_audit_action(
545 'repo.pull_request.create', {'data': creation_data},
545 'repo.pull_request.create', {'data': creation_data},
546 auth_user, pull_request)
546 auth_user, pull_request)
547
547
548 return pull_request
548 return pull_request
549
549
550 def _trigger_pull_request_hook(self, pull_request, user, action):
550 def _trigger_pull_request_hook(self, pull_request, user, action):
551 pull_request = self.__get_pull_request(pull_request)
551 pull_request = self.__get_pull_request(pull_request)
552 target_scm = pull_request.target_repo.scm_instance()
552 target_scm = pull_request.target_repo.scm_instance()
553 if action == 'create':
553 if action == 'create':
554 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
554 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
555 elif action == 'merge':
555 elif action == 'merge':
556 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
556 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
557 elif action == 'close':
557 elif action == 'close':
558 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
558 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
559 elif action == 'review_status_change':
559 elif action == 'review_status_change':
560 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
560 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
561 elif action == 'update':
561 elif action == 'update':
562 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
562 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
563 else:
563 else:
564 return
564 return
565
565
566 trigger_hook(
566 trigger_hook(
567 username=user.username,
567 username=user.username,
568 repo_name=pull_request.target_repo.repo_name,
568 repo_name=pull_request.target_repo.repo_name,
569 repo_alias=target_scm.alias,
569 repo_alias=target_scm.alias,
570 pull_request=pull_request)
570 pull_request=pull_request)
571
571
572 def _get_commit_ids(self, pull_request):
572 def _get_commit_ids(self, pull_request):
573 """
573 """
574 Return the commit ids of the merged pull request.
574 Return the commit ids of the merged pull request.
575
575
576 This method is not dealing correctly yet with the lack of autoupdates
576 This method is not dealing correctly yet with the lack of autoupdates
577 nor with the implicit target updates.
577 nor with the implicit target updates.
578 For example: if a commit in the source repo is already in the target it
578 For example: if a commit in the source repo is already in the target it
579 will be reported anyways.
579 will be reported anyways.
580 """
580 """
581 merge_rev = pull_request.merge_rev
581 merge_rev = pull_request.merge_rev
582 if merge_rev is None:
582 if merge_rev is None:
583 raise ValueError('This pull request was not merged yet')
583 raise ValueError('This pull request was not merged yet')
584
584
585 commit_ids = list(pull_request.revisions)
585 commit_ids = list(pull_request.revisions)
586 if merge_rev not in commit_ids:
586 if merge_rev not in commit_ids:
587 commit_ids.append(merge_rev)
587 commit_ids.append(merge_rev)
588
588
589 return commit_ids
589 return commit_ids
590
590
591 def merge_repo(self, pull_request, user, extras):
591 def merge_repo(self, pull_request, user, extras):
592 log.debug("Merging pull request %s", pull_request.pull_request_id)
592 log.debug("Merging pull request %s", pull_request.pull_request_id)
593 merge_state = self._merge_pull_request(pull_request, user, extras)
593 merge_state = self._merge_pull_request(pull_request, user, extras)
594 if merge_state.executed:
594 if merge_state.executed:
595 log.debug(
595 log.debug(
596 "Merge was successful, updating the pull request comments.")
596 "Merge was successful, updating the pull request comments.")
597 self._comment_and_close_pr(pull_request, user, merge_state)
597 self._comment_and_close_pr(pull_request, user, merge_state)
598
598
599 self._log_audit_action(
599 self._log_audit_action(
600 'repo.pull_request.merge',
600 'repo.pull_request.merge',
601 {'merge_state': merge_state.__dict__},
601 {'merge_state': merge_state.__dict__},
602 user, pull_request)
602 user, pull_request)
603
603
604 else:
604 else:
605 log.warn("Merge failed, not updating the pull request.")
605 log.warn("Merge failed, not updating the pull request.")
606 return merge_state
606 return merge_state
607
607
608 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
608 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
609 target_vcs = pull_request.target_repo.scm_instance()
609 target_vcs = pull_request.target_repo.scm_instance()
610 source_vcs = pull_request.source_repo.scm_instance()
610 source_vcs = pull_request.source_repo.scm_instance()
611 target_ref = self._refresh_reference(
611 target_ref = self._refresh_reference(
612 pull_request.target_ref_parts, target_vcs)
612 pull_request.target_ref_parts, target_vcs)
613
613
614 message = merge_msg or (
614 message = merge_msg or (
615 'Merge pull request #%(pr_id)s from '
615 'Merge pull request #%(pr_id)s from '
616 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
616 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
617 'pr_id': pull_request.pull_request_id,
617 'pr_id': pull_request.pull_request_id,
618 'source_repo': source_vcs.name,
618 'source_repo': source_vcs.name,
619 'source_ref_name': pull_request.source_ref_parts.name,
619 'source_ref_name': pull_request.source_ref_parts.name,
620 'pr_title': pull_request.title
620 'pr_title': pull_request.title
621 }
621 }
622
622
623 workspace_id = self._workspace_id(pull_request)
623 workspace_id = self._workspace_id(pull_request)
624 repo_id = pull_request.target_repo.repo_id
624 repo_id = pull_request.target_repo.repo_id
625 use_rebase = self._use_rebase_for_merging(pull_request)
625 use_rebase = self._use_rebase_for_merging(pull_request)
626 close_branch = self._close_branch_before_merging(pull_request)
626 close_branch = self._close_branch_before_merging(pull_request)
627
627
628 callback_daemon, extras = prepare_callback_daemon(
628 callback_daemon, extras = prepare_callback_daemon(
629 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
629 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
630 host=vcs_settings.HOOKS_HOST,
630 host=vcs_settings.HOOKS_HOST,
631 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
631 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
632
632
633 with callback_daemon:
633 with callback_daemon:
634 # TODO: johbo: Implement a clean way to run a config_override
634 # TODO: johbo: Implement a clean way to run a config_override
635 # for a single call.
635 # for a single call.
636 target_vcs.config.set(
636 target_vcs.config.set(
637 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
637 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
638 merge_state = target_vcs.merge(
638 merge_state = target_vcs.merge(
639 repo_id, workspace_id, target_ref, source_vcs,
639 repo_id, workspace_id, target_ref, source_vcs,
640 pull_request.source_ref_parts,
640 pull_request.source_ref_parts,
641 user_name=user.username, user_email=user.email,
641 user_name=user.username, user_email=user.email,
642 message=message, use_rebase=use_rebase,
642 message=message, use_rebase=use_rebase,
643 close_branch=close_branch)
643 close_branch=close_branch)
644 return merge_state
644 return merge_state
645
645
646 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
646 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
647 pull_request.merge_rev = merge_state.merge_ref.commit_id
647 pull_request.merge_rev = merge_state.merge_ref.commit_id
648 pull_request.updated_on = datetime.datetime.now()
648 pull_request.updated_on = datetime.datetime.now()
649 close_msg = close_msg or 'Pull request merged and closed'
649 close_msg = close_msg or 'Pull request merged and closed'
650
650
651 CommentsModel().create(
651 CommentsModel().create(
652 text=safe_unicode(close_msg),
652 text=safe_unicode(close_msg),
653 repo=pull_request.target_repo.repo_id,
653 repo=pull_request.target_repo.repo_id,
654 user=user.user_id,
654 user=user.user_id,
655 pull_request=pull_request.pull_request_id,
655 pull_request=pull_request.pull_request_id,
656 f_path=None,
656 f_path=None,
657 line_no=None,
657 line_no=None,
658 closing_pr=True
658 closing_pr=True
659 )
659 )
660
660
661 Session().add(pull_request)
661 Session().add(pull_request)
662 Session().flush()
662 Session().flush()
663 # TODO: paris: replace invalidation with less radical solution
663 # TODO: paris: replace invalidation with less radical solution
664 ScmModel().mark_for_invalidation(
664 ScmModel().mark_for_invalidation(
665 pull_request.target_repo.repo_name)
665 pull_request.target_repo.repo_name)
666 self._trigger_pull_request_hook(pull_request, user, 'merge')
666 self._trigger_pull_request_hook(pull_request, user, 'merge')
667
667
668 def has_valid_update_type(self, pull_request):
668 def has_valid_update_type(self, pull_request):
669 source_ref_type = pull_request.source_ref_parts.type
669 source_ref_type = pull_request.source_ref_parts.type
670 return source_ref_type in ['book', 'branch', 'tag']
670 return source_ref_type in ['book', 'branch', 'tag']
671
671
672 def update_commits(self, pull_request):
672 def update_commits(self, pull_request):
673 """
673 """
674 Get the updated list of commits for the pull request
674 Get the updated list of commits for the pull request
675 and return the new pull request version and the list
675 and return the new pull request version and the list
676 of commits processed by this update action
676 of commits processed by this update action
677 """
677 """
678 pull_request = self.__get_pull_request(pull_request)
678 pull_request = self.__get_pull_request(pull_request)
679 source_ref_type = pull_request.source_ref_parts.type
679 source_ref_type = pull_request.source_ref_parts.type
680 source_ref_name = pull_request.source_ref_parts.name
680 source_ref_name = pull_request.source_ref_parts.name
681 source_ref_id = pull_request.source_ref_parts.commit_id
681 source_ref_id = pull_request.source_ref_parts.commit_id
682
682
683 target_ref_type = pull_request.target_ref_parts.type
683 target_ref_type = pull_request.target_ref_parts.type
684 target_ref_name = pull_request.target_ref_parts.name
684 target_ref_name = pull_request.target_ref_parts.name
685 target_ref_id = pull_request.target_ref_parts.commit_id
685 target_ref_id = pull_request.target_ref_parts.commit_id
686
686
687 if not self.has_valid_update_type(pull_request):
687 if not self.has_valid_update_type(pull_request):
688 log.debug(
688 log.debug(
689 "Skipping update of pull request %s due to ref type: %s",
689 "Skipping update of pull request %s due to ref type: %s",
690 pull_request, source_ref_type)
690 pull_request, source_ref_type)
691 return UpdateResponse(
691 return UpdateResponse(
692 executed=False,
692 executed=False,
693 reason=UpdateFailureReason.WRONG_REF_TYPE,
693 reason=UpdateFailureReason.WRONG_REF_TYPE,
694 old=pull_request, new=None, changes=None,
694 old=pull_request, new=None, changes=None,
695 source_changed=False, target_changed=False)
695 source_changed=False, target_changed=False)
696
696
697 # source repo
697 # source repo
698 source_repo = pull_request.source_repo.scm_instance()
698 source_repo = pull_request.source_repo.scm_instance()
699 try:
699 try:
700 source_commit = source_repo.get_commit(commit_id=source_ref_name)
700 source_commit = source_repo.get_commit(commit_id=source_ref_name)
701 except CommitDoesNotExistError:
701 except CommitDoesNotExistError:
702 return UpdateResponse(
702 return UpdateResponse(
703 executed=False,
703 executed=False,
704 reason=UpdateFailureReason.MISSING_SOURCE_REF,
704 reason=UpdateFailureReason.MISSING_SOURCE_REF,
705 old=pull_request, new=None, changes=None,
705 old=pull_request, new=None, changes=None,
706 source_changed=False, target_changed=False)
706 source_changed=False, target_changed=False)
707
707
708 source_changed = source_ref_id != source_commit.raw_id
708 source_changed = source_ref_id != source_commit.raw_id
709
709
710 # target repo
710 # target repo
711 target_repo = pull_request.target_repo.scm_instance()
711 target_repo = pull_request.target_repo.scm_instance()
712 try:
712 try:
713 target_commit = target_repo.get_commit(commit_id=target_ref_name)
713 target_commit = target_repo.get_commit(commit_id=target_ref_name)
714 except CommitDoesNotExistError:
714 except CommitDoesNotExistError:
715 return UpdateResponse(
715 return UpdateResponse(
716 executed=False,
716 executed=False,
717 reason=UpdateFailureReason.MISSING_TARGET_REF,
717 reason=UpdateFailureReason.MISSING_TARGET_REF,
718 old=pull_request, new=None, changes=None,
718 old=pull_request, new=None, changes=None,
719 source_changed=False, target_changed=False)
719 source_changed=False, target_changed=False)
720 target_changed = target_ref_id != target_commit.raw_id
720 target_changed = target_ref_id != target_commit.raw_id
721
721
722 if not (source_changed or target_changed):
722 if not (source_changed or target_changed):
723 log.debug("Nothing changed in pull request %s", pull_request)
723 log.debug("Nothing changed in pull request %s", pull_request)
724 return UpdateResponse(
724 return UpdateResponse(
725 executed=False,
725 executed=False,
726 reason=UpdateFailureReason.NO_CHANGE,
726 reason=UpdateFailureReason.NO_CHANGE,
727 old=pull_request, new=None, changes=None,
727 old=pull_request, new=None, changes=None,
728 source_changed=target_changed, target_changed=source_changed)
728 source_changed=target_changed, target_changed=source_changed)
729
729
730 change_in_found = 'target repo' if target_changed else 'source repo'
730 change_in_found = 'target repo' if target_changed else 'source repo'
731 log.debug('Updating pull request because of change in %s detected',
731 log.debug('Updating pull request because of change in %s detected',
732 change_in_found)
732 change_in_found)
733
733
734 # Finally there is a need for an update, in case of source change
734 # Finally there is a need for an update, in case of source change
735 # we create a new version, else just an update
735 # we create a new version, else just an update
736 if source_changed:
736 if source_changed:
737 pull_request_version = self._create_version_from_snapshot(pull_request)
737 pull_request_version = self._create_version_from_snapshot(pull_request)
738 self._link_comments_to_version(pull_request_version)
738 self._link_comments_to_version(pull_request_version)
739 else:
739 else:
740 try:
740 try:
741 ver = pull_request.versions[-1]
741 ver = pull_request.versions[-1]
742 except IndexError:
742 except IndexError:
743 ver = None
743 ver = None
744
744
745 pull_request.pull_request_version_id = \
745 pull_request.pull_request_version_id = \
746 ver.pull_request_version_id if ver else None
746 ver.pull_request_version_id if ver else None
747 pull_request_version = pull_request
747 pull_request_version = pull_request
748
748
749 try:
749 try:
750 if target_ref_type in ('tag', 'branch', 'book'):
750 if target_ref_type in ('tag', 'branch', 'book'):
751 target_commit = target_repo.get_commit(target_ref_name)
751 target_commit = target_repo.get_commit(target_ref_name)
752 else:
752 else:
753 target_commit = target_repo.get_commit(target_ref_id)
753 target_commit = target_repo.get_commit(target_ref_id)
754 except CommitDoesNotExistError:
754 except CommitDoesNotExistError:
755 return UpdateResponse(
755 return UpdateResponse(
756 executed=False,
756 executed=False,
757 reason=UpdateFailureReason.MISSING_TARGET_REF,
757 reason=UpdateFailureReason.MISSING_TARGET_REF,
758 old=pull_request, new=None, changes=None,
758 old=pull_request, new=None, changes=None,
759 source_changed=source_changed, target_changed=target_changed)
759 source_changed=source_changed, target_changed=target_changed)
760
760
761 # re-compute commit ids
761 # re-compute commit ids
762 old_commit_ids = pull_request.revisions
762 old_commit_ids = pull_request.revisions
763 pre_load = ["author", "branch", "date", "message"]
763 pre_load = ["author", "branch", "date", "message"]
764 commit_ranges = target_repo.compare(
764 commit_ranges = target_repo.compare(
765 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
765 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
766 pre_load=pre_load)
766 pre_load=pre_load)
767
767
768 ancestor = target_repo.get_common_ancestor(
768 ancestor = target_repo.get_common_ancestor(
769 target_commit.raw_id, source_commit.raw_id, source_repo)
769 target_commit.raw_id, source_commit.raw_id, source_repo)
770
770
771 pull_request.source_ref = '%s:%s:%s' % (
771 pull_request.source_ref = '%s:%s:%s' % (
772 source_ref_type, source_ref_name, source_commit.raw_id)
772 source_ref_type, source_ref_name, source_commit.raw_id)
773 pull_request.target_ref = '%s:%s:%s' % (
773 pull_request.target_ref = '%s:%s:%s' % (
774 target_ref_type, target_ref_name, ancestor)
774 target_ref_type, target_ref_name, ancestor)
775
775
776 pull_request.revisions = [
776 pull_request.revisions = [
777 commit.raw_id for commit in reversed(commit_ranges)]
777 commit.raw_id for commit in reversed(commit_ranges)]
778 pull_request.updated_on = datetime.datetime.now()
778 pull_request.updated_on = datetime.datetime.now()
779 Session().add(pull_request)
779 Session().add(pull_request)
780 new_commit_ids = pull_request.revisions
780 new_commit_ids = pull_request.revisions
781
781
782 old_diff_data, new_diff_data = self._generate_update_diffs(
782 old_diff_data, new_diff_data = self._generate_update_diffs(
783 pull_request, pull_request_version)
783 pull_request, pull_request_version)
784
784
785 # calculate commit and file changes
785 # calculate commit and file changes
786 changes = self._calculate_commit_id_changes(
786 changes = self._calculate_commit_id_changes(
787 old_commit_ids, new_commit_ids)
787 old_commit_ids, new_commit_ids)
788 file_changes = self._calculate_file_changes(
788 file_changes = self._calculate_file_changes(
789 old_diff_data, new_diff_data)
789 old_diff_data, new_diff_data)
790
790
791 # set comments as outdated if DIFFS changed
791 # set comments as outdated if DIFFS changed
792 CommentsModel().outdate_comments(
792 CommentsModel().outdate_comments(
793 pull_request, old_diff_data=old_diff_data,
793 pull_request, old_diff_data=old_diff_data,
794 new_diff_data=new_diff_data)
794 new_diff_data=new_diff_data)
795
795
796 commit_changes = (changes.added or changes.removed)
796 commit_changes = (changes.added or changes.removed)
797 file_node_changes = (
797 file_node_changes = (
798 file_changes.added or file_changes.modified or file_changes.removed)
798 file_changes.added or file_changes.modified or file_changes.removed)
799 pr_has_changes = commit_changes or file_node_changes
799 pr_has_changes = commit_changes or file_node_changes
800
800
801 # Add an automatic comment to the pull request, in case
801 # Add an automatic comment to the pull request, in case
802 # anything has changed
802 # anything has changed
803 if pr_has_changes:
803 if pr_has_changes:
804 update_comment = CommentsModel().create(
804 update_comment = CommentsModel().create(
805 text=self._render_update_message(changes, file_changes),
805 text=self._render_update_message(changes, file_changes),
806 repo=pull_request.target_repo,
806 repo=pull_request.target_repo,
807 user=pull_request.author,
807 user=pull_request.author,
808 pull_request=pull_request,
808 pull_request=pull_request,
809 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
809 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
810
810
811 # Update status to "Under Review" for added commits
811 # Update status to "Under Review" for added commits
812 for commit_id in changes.added:
812 for commit_id in changes.added:
813 ChangesetStatusModel().set_status(
813 ChangesetStatusModel().set_status(
814 repo=pull_request.source_repo,
814 repo=pull_request.source_repo,
815 status=ChangesetStatus.STATUS_UNDER_REVIEW,
815 status=ChangesetStatus.STATUS_UNDER_REVIEW,
816 comment=update_comment,
816 comment=update_comment,
817 user=pull_request.author,
817 user=pull_request.author,
818 pull_request=pull_request,
818 pull_request=pull_request,
819 revision=commit_id)
819 revision=commit_id)
820
820
821 log.debug(
821 log.debug(
822 'Updated pull request %s, added_ids: %s, common_ids: %s, '
822 'Updated pull request %s, added_ids: %s, common_ids: %s, '
823 'removed_ids: %s', pull_request.pull_request_id,
823 'removed_ids: %s', pull_request.pull_request_id,
824 changes.added, changes.common, changes.removed)
824 changes.added, changes.common, changes.removed)
825 log.debug(
825 log.debug(
826 'Updated pull request with the following file changes: %s',
826 'Updated pull request with the following file changes: %s',
827 file_changes)
827 file_changes)
828
828
829 log.info(
829 log.info(
830 "Updated pull request %s from commit %s to commit %s, "
830 "Updated pull request %s from commit %s to commit %s, "
831 "stored new version %s of this pull request.",
831 "stored new version %s of this pull request.",
832 pull_request.pull_request_id, source_ref_id,
832 pull_request.pull_request_id, source_ref_id,
833 pull_request.source_ref_parts.commit_id,
833 pull_request.source_ref_parts.commit_id,
834 pull_request_version.pull_request_version_id)
834 pull_request_version.pull_request_version_id)
835 Session().commit()
835 Session().commit()
836 self._trigger_pull_request_hook(
836 self._trigger_pull_request_hook(
837 pull_request, pull_request.author, 'update')
837 pull_request, pull_request.author, 'update')
838
838
839 return UpdateResponse(
839 return UpdateResponse(
840 executed=True, reason=UpdateFailureReason.NONE,
840 executed=True, reason=UpdateFailureReason.NONE,
841 old=pull_request, new=pull_request_version, changes=changes,
841 old=pull_request, new=pull_request_version, changes=changes,
842 source_changed=source_changed, target_changed=target_changed)
842 source_changed=source_changed, target_changed=target_changed)
843
843
844 def _create_version_from_snapshot(self, pull_request):
844 def _create_version_from_snapshot(self, pull_request):
845 version = PullRequestVersion()
845 version = PullRequestVersion()
846 version.title = pull_request.title
846 version.title = pull_request.title
847 version.description = pull_request.description
847 version.description = pull_request.description
848 version.status = pull_request.status
848 version.status = pull_request.status
849 version.created_on = datetime.datetime.now()
849 version.created_on = datetime.datetime.now()
850 version.updated_on = pull_request.updated_on
850 version.updated_on = pull_request.updated_on
851 version.user_id = pull_request.user_id
851 version.user_id = pull_request.user_id
852 version.source_repo = pull_request.source_repo
852 version.source_repo = pull_request.source_repo
853 version.source_ref = pull_request.source_ref
853 version.source_ref = pull_request.source_ref
854 version.target_repo = pull_request.target_repo
854 version.target_repo = pull_request.target_repo
855 version.target_ref = pull_request.target_ref
855 version.target_ref = pull_request.target_ref
856
856
857 version._last_merge_source_rev = pull_request._last_merge_source_rev
857 version._last_merge_source_rev = pull_request._last_merge_source_rev
858 version._last_merge_target_rev = pull_request._last_merge_target_rev
858 version._last_merge_target_rev = pull_request._last_merge_target_rev
859 version.last_merge_status = pull_request.last_merge_status
859 version.last_merge_status = pull_request.last_merge_status
860 version.shadow_merge_ref = pull_request.shadow_merge_ref
860 version.shadow_merge_ref = pull_request.shadow_merge_ref
861 version.merge_rev = pull_request.merge_rev
861 version.merge_rev = pull_request.merge_rev
862 version.reviewer_data = pull_request.reviewer_data
862 version.reviewer_data = pull_request.reviewer_data
863
863
864 version.revisions = pull_request.revisions
864 version.revisions = pull_request.revisions
865 version.pull_request = pull_request
865 version.pull_request = pull_request
866 Session().add(version)
866 Session().add(version)
867 Session().flush()
867 Session().flush()
868
868
869 return version
869 return version
870
870
871 def _generate_update_diffs(self, pull_request, pull_request_version):
871 def _generate_update_diffs(self, pull_request, pull_request_version):
872
872
873 diff_context = (
873 diff_context = (
874 self.DIFF_CONTEXT +
874 self.DIFF_CONTEXT +
875 CommentsModel.needed_extra_diff_context())
875 CommentsModel.needed_extra_diff_context())
876
876
877 source_repo = pull_request_version.source_repo
877 source_repo = pull_request_version.source_repo
878 source_ref_id = pull_request_version.source_ref_parts.commit_id
878 source_ref_id = pull_request_version.source_ref_parts.commit_id
879 target_ref_id = pull_request_version.target_ref_parts.commit_id
879 target_ref_id = pull_request_version.target_ref_parts.commit_id
880 old_diff = self._get_diff_from_pr_or_version(
880 old_diff = self._get_diff_from_pr_or_version(
881 source_repo, source_ref_id, target_ref_id, context=diff_context)
881 source_repo, source_ref_id, target_ref_id, context=diff_context)
882
882
883 source_repo = pull_request.source_repo
883 source_repo = pull_request.source_repo
884 source_ref_id = pull_request.source_ref_parts.commit_id
884 source_ref_id = pull_request.source_ref_parts.commit_id
885 target_ref_id = pull_request.target_ref_parts.commit_id
885 target_ref_id = pull_request.target_ref_parts.commit_id
886
886
887 new_diff = self._get_diff_from_pr_or_version(
887 new_diff = self._get_diff_from_pr_or_version(
888 source_repo, source_ref_id, target_ref_id, context=diff_context)
888 source_repo, source_ref_id, target_ref_id, context=diff_context)
889
889
890 old_diff_data = diffs.DiffProcessor(old_diff)
890 old_diff_data = diffs.DiffProcessor(old_diff)
891 old_diff_data.prepare()
891 old_diff_data.prepare()
892 new_diff_data = diffs.DiffProcessor(new_diff)
892 new_diff_data = diffs.DiffProcessor(new_diff)
893 new_diff_data.prepare()
893 new_diff_data.prepare()
894
894
895 return old_diff_data, new_diff_data
895 return old_diff_data, new_diff_data
896
896
897 def _link_comments_to_version(self, pull_request_version):
897 def _link_comments_to_version(self, pull_request_version):
898 """
898 """
899 Link all unlinked comments of this pull request to the given version.
899 Link all unlinked comments of this pull request to the given version.
900
900
901 :param pull_request_version: The `PullRequestVersion` to which
901 :param pull_request_version: The `PullRequestVersion` to which
902 the comments shall be linked.
902 the comments shall be linked.
903
903
904 """
904 """
905 pull_request = pull_request_version.pull_request
905 pull_request = pull_request_version.pull_request
906 comments = ChangesetComment.query()\
906 comments = ChangesetComment.query()\
907 .filter(
907 .filter(
908 # TODO: johbo: Should we query for the repo at all here?
908 # TODO: johbo: Should we query for the repo at all here?
909 # Pending decision on how comments of PRs are to be related
909 # Pending decision on how comments of PRs are to be related
910 # to either the source repo, the target repo or no repo at all.
910 # to either the source repo, the target repo or no repo at all.
911 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
911 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
912 ChangesetComment.pull_request == pull_request,
912 ChangesetComment.pull_request == pull_request,
913 ChangesetComment.pull_request_version == None)\
913 ChangesetComment.pull_request_version == None)\
914 .order_by(ChangesetComment.comment_id.asc())
914 .order_by(ChangesetComment.comment_id.asc())
915
915
916 # TODO: johbo: Find out why this breaks if it is done in a bulk
916 # TODO: johbo: Find out why this breaks if it is done in a bulk
917 # operation.
917 # operation.
918 for comment in comments:
918 for comment in comments:
919 comment.pull_request_version_id = (
919 comment.pull_request_version_id = (
920 pull_request_version.pull_request_version_id)
920 pull_request_version.pull_request_version_id)
921 Session().add(comment)
921 Session().add(comment)
922
922
923 def _calculate_commit_id_changes(self, old_ids, new_ids):
923 def _calculate_commit_id_changes(self, old_ids, new_ids):
924 added = [x for x in new_ids if x not in old_ids]
924 added = [x for x in new_ids if x not in old_ids]
925 common = [x for x in new_ids if x in old_ids]
925 common = [x for x in new_ids if x in old_ids]
926 removed = [x for x in old_ids if x not in new_ids]
926 removed = [x for x in old_ids if x not in new_ids]
927 total = new_ids
927 total = new_ids
928 return ChangeTuple(added, common, removed, total)
928 return ChangeTuple(added, common, removed, total)
929
929
930 def _calculate_file_changes(self, old_diff_data, new_diff_data):
930 def _calculate_file_changes(self, old_diff_data, new_diff_data):
931
931
932 old_files = OrderedDict()
932 old_files = OrderedDict()
933 for diff_data in old_diff_data.parsed_diff:
933 for diff_data in old_diff_data.parsed_diff:
934 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
934 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
935
935
936 added_files = []
936 added_files = []
937 modified_files = []
937 modified_files = []
938 removed_files = []
938 removed_files = []
939 for diff_data in new_diff_data.parsed_diff:
939 for diff_data in new_diff_data.parsed_diff:
940 new_filename = diff_data['filename']
940 new_filename = diff_data['filename']
941 new_hash = md5_safe(diff_data['raw_diff'])
941 new_hash = md5_safe(diff_data['raw_diff'])
942
942
943 old_hash = old_files.get(new_filename)
943 old_hash = old_files.get(new_filename)
944 if not old_hash:
944 if not old_hash:
945 # file is not present in old diff, means it's added
945 # file is not present in old diff, means it's added
946 added_files.append(new_filename)
946 added_files.append(new_filename)
947 else:
947 else:
948 if new_hash != old_hash:
948 if new_hash != old_hash:
949 modified_files.append(new_filename)
949 modified_files.append(new_filename)
950 # now remove a file from old, since we have seen it already
950 # now remove a file from old, since we have seen it already
951 del old_files[new_filename]
951 del old_files[new_filename]
952
952
953 # removed files is when there are present in old, but not in NEW,
953 # removed files is when there are present in old, but not in NEW,
954 # since we remove old files that are present in new diff, left-overs
954 # since we remove old files that are present in new diff, left-overs
955 # if any should be the removed files
955 # if any should be the removed files
956 removed_files.extend(old_files.keys())
956 removed_files.extend(old_files.keys())
957
957
958 return FileChangeTuple(added_files, modified_files, removed_files)
958 return FileChangeTuple(added_files, modified_files, removed_files)
959
959
960 def _render_update_message(self, changes, file_changes):
960 def _render_update_message(self, changes, file_changes):
961 """
961 """
962 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
962 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
963 so it's always looking the same disregarding on which default
963 so it's always looking the same disregarding on which default
964 renderer system is using.
964 renderer system is using.
965
965
966 :param changes: changes named tuple
966 :param changes: changes named tuple
967 :param file_changes: file changes named tuple
967 :param file_changes: file changes named tuple
968
968
969 """
969 """
970 new_status = ChangesetStatus.get_status_lbl(
970 new_status = ChangesetStatus.get_status_lbl(
971 ChangesetStatus.STATUS_UNDER_REVIEW)
971 ChangesetStatus.STATUS_UNDER_REVIEW)
972
972
973 changed_files = (
973 changed_files = (
974 file_changes.added + file_changes.modified + file_changes.removed)
974 file_changes.added + file_changes.modified + file_changes.removed)
975
975
976 params = {
976 params = {
977 'under_review_label': new_status,
977 'under_review_label': new_status,
978 'added_commits': changes.added,
978 'added_commits': changes.added,
979 'removed_commits': changes.removed,
979 'removed_commits': changes.removed,
980 'changed_files': changed_files,
980 'changed_files': changed_files,
981 'added_files': file_changes.added,
981 'added_files': file_changes.added,
982 'modified_files': file_changes.modified,
982 'modified_files': file_changes.modified,
983 'removed_files': file_changes.removed,
983 'removed_files': file_changes.removed,
984 }
984 }
985 renderer = RstTemplateRenderer()
985 renderer = RstTemplateRenderer()
986 return renderer.render('pull_request_update.mako', **params)
986 return renderer.render('pull_request_update.mako', **params)
987
987
988 def edit(self, pull_request, title, description, description_renderer, user):
988 def edit(self, pull_request, title, description, description_renderer, user):
989 pull_request = self.__get_pull_request(pull_request)
989 pull_request = self.__get_pull_request(pull_request)
990 old_data = pull_request.get_api_data(with_merge_state=False)
990 old_data = pull_request.get_api_data(with_merge_state=False)
991 if pull_request.is_closed():
991 if pull_request.is_closed():
992 raise ValueError('This pull request is closed')
992 raise ValueError('This pull request is closed')
993 if title:
993 if title:
994 pull_request.title = title
994 pull_request.title = title
995 pull_request.description = description
995 pull_request.description = description
996 pull_request.updated_on = datetime.datetime.now()
996 pull_request.updated_on = datetime.datetime.now()
997 pull_request.description_renderer = description_renderer
997 pull_request.description_renderer = description_renderer
998 Session().add(pull_request)
998 Session().add(pull_request)
999 self._log_audit_action(
999 self._log_audit_action(
1000 'repo.pull_request.edit', {'old_data': old_data},
1000 'repo.pull_request.edit', {'old_data': old_data},
1001 user, pull_request)
1001 user, pull_request)
1002
1002
1003 def update_reviewers(self, pull_request, reviewer_data, user):
1003 def update_reviewers(self, pull_request, reviewer_data, user):
1004 """
1004 """
1005 Update the reviewers in the pull request
1005 Update the reviewers in the pull request
1006
1006
1007 :param pull_request: the pr to update
1007 :param pull_request: the pr to update
1008 :param reviewer_data: list of tuples
1008 :param reviewer_data: list of tuples
1009 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1009 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1010 """
1010 """
1011 pull_request = self.__get_pull_request(pull_request)
1011 pull_request = self.__get_pull_request(pull_request)
1012 if pull_request.is_closed():
1012 if pull_request.is_closed():
1013 raise ValueError('This pull request is closed')
1013 raise ValueError('This pull request is closed')
1014
1014
1015 reviewers = {}
1015 reviewers = {}
1016 for user_id, reasons, mandatory, rules in reviewer_data:
1016 for user_id, reasons, mandatory, rules in reviewer_data:
1017 if isinstance(user_id, (int, basestring)):
1017 if isinstance(user_id, (int, basestring)):
1018 user_id = self._get_user(user_id).user_id
1018 user_id = self._get_user(user_id).user_id
1019 reviewers[user_id] = {
1019 reviewers[user_id] = {
1020 'reasons': reasons, 'mandatory': mandatory}
1020 'reasons': reasons, 'mandatory': mandatory}
1021
1021
1022 reviewers_ids = set(reviewers.keys())
1022 reviewers_ids = set(reviewers.keys())
1023 current_reviewers = PullRequestReviewers.query()\
1023 current_reviewers = PullRequestReviewers.query()\
1024 .filter(PullRequestReviewers.pull_request ==
1024 .filter(PullRequestReviewers.pull_request ==
1025 pull_request).all()
1025 pull_request).all()
1026 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1026 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1027
1027
1028 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1028 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1029 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1029 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1030
1030
1031 log.debug("Adding %s reviewers", ids_to_add)
1031 log.debug("Adding %s reviewers", ids_to_add)
1032 log.debug("Removing %s reviewers", ids_to_remove)
1032 log.debug("Removing %s reviewers", ids_to_remove)
1033 changed = False
1033 changed = False
1034 for uid in ids_to_add:
1034 for uid in ids_to_add:
1035 changed = True
1035 changed = True
1036 _usr = self._get_user(uid)
1036 _usr = self._get_user(uid)
1037 reviewer = PullRequestReviewers()
1037 reviewer = PullRequestReviewers()
1038 reviewer.user = _usr
1038 reviewer.user = _usr
1039 reviewer.pull_request = pull_request
1039 reviewer.pull_request = pull_request
1040 reviewer.reasons = reviewers[uid]['reasons']
1040 reviewer.reasons = reviewers[uid]['reasons']
1041 # NOTE(marcink): mandatory shouldn't be changed now
1041 # NOTE(marcink): mandatory shouldn't be changed now
1042 # reviewer.mandatory = reviewers[uid]['reasons']
1042 # reviewer.mandatory = reviewers[uid]['reasons']
1043 Session().add(reviewer)
1043 Session().add(reviewer)
1044 self._log_audit_action(
1044 self._log_audit_action(
1045 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1045 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1046 user, pull_request)
1046 user, pull_request)
1047
1047
1048 for uid in ids_to_remove:
1048 for uid in ids_to_remove:
1049 changed = True
1049 changed = True
1050 reviewers = PullRequestReviewers.query()\
1050 reviewers = PullRequestReviewers.query()\
1051 .filter(PullRequestReviewers.user_id == uid,
1051 .filter(PullRequestReviewers.user_id == uid,
1052 PullRequestReviewers.pull_request == pull_request)\
1052 PullRequestReviewers.pull_request == pull_request)\
1053 .all()
1053 .all()
1054 # use .all() in case we accidentally added the same person twice
1054 # use .all() in case we accidentally added the same person twice
1055 # this CAN happen due to the lack of DB checks
1055 # this CAN happen due to the lack of DB checks
1056 for obj in reviewers:
1056 for obj in reviewers:
1057 old_data = obj.get_dict()
1057 old_data = obj.get_dict()
1058 Session().delete(obj)
1058 Session().delete(obj)
1059 self._log_audit_action(
1059 self._log_audit_action(
1060 'repo.pull_request.reviewer.delete',
1060 'repo.pull_request.reviewer.delete',
1061 {'old_data': old_data}, user, pull_request)
1061 {'old_data': old_data}, user, pull_request)
1062
1062
1063 if changed:
1063 if changed:
1064 pull_request.updated_on = datetime.datetime.now()
1064 pull_request.updated_on = datetime.datetime.now()
1065 Session().add(pull_request)
1065 Session().add(pull_request)
1066
1066
1067 self.notify_reviewers(pull_request, ids_to_add)
1067 self.notify_reviewers(pull_request, ids_to_add)
1068 return ids_to_add, ids_to_remove
1068 return ids_to_add, ids_to_remove
1069
1069
1070 def get_url(self, pull_request, request=None, permalink=False):
1070 def get_url(self, pull_request, request=None, permalink=False):
1071 if not request:
1071 if not request:
1072 request = get_current_request()
1072 request = get_current_request()
1073
1073
1074 if permalink:
1074 if permalink:
1075 return request.route_url(
1075 return request.route_url(
1076 'pull_requests_global',
1076 'pull_requests_global',
1077 pull_request_id=pull_request.pull_request_id,)
1077 pull_request_id=pull_request.pull_request_id,)
1078 else:
1078 else:
1079 return request.route_url('pullrequest_show',
1079 return request.route_url('pullrequest_show',
1080 repo_name=safe_str(pull_request.target_repo.repo_name),
1080 repo_name=safe_str(pull_request.target_repo.repo_name),
1081 pull_request_id=pull_request.pull_request_id,)
1081 pull_request_id=pull_request.pull_request_id,)
1082
1082
1083 def get_shadow_clone_url(self, pull_request, request=None):
1083 def get_shadow_clone_url(self, pull_request, request=None):
1084 """
1084 """
1085 Returns qualified url pointing to the shadow repository. If this pull
1085 Returns qualified url pointing to the shadow repository. If this pull
1086 request is closed there is no shadow repository and ``None`` will be
1086 request is closed there is no shadow repository and ``None`` will be
1087 returned.
1087 returned.
1088 """
1088 """
1089 if pull_request.is_closed():
1089 if pull_request.is_closed():
1090 return None
1090 return None
1091 else:
1091 else:
1092 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1092 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1093 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1093 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1094
1094
1095 def notify_reviewers(self, pull_request, reviewers_ids):
1095 def notify_reviewers(self, pull_request, reviewers_ids):
1096 # notification to reviewers
1096 # notification to reviewers
1097 if not reviewers_ids:
1097 if not reviewers_ids:
1098 return
1098 return
1099
1099
1100 pull_request_obj = pull_request
1100 pull_request_obj = pull_request
1101 # get the current participants of this pull request
1101 # get the current participants of this pull request
1102 recipients = reviewers_ids
1102 recipients = reviewers_ids
1103 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1103 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1104
1104
1105 pr_source_repo = pull_request_obj.source_repo
1105 pr_source_repo = pull_request_obj.source_repo
1106 pr_target_repo = pull_request_obj.target_repo
1106 pr_target_repo = pull_request_obj.target_repo
1107
1107
1108 pr_url = h.route_url('pullrequest_show',
1108 pr_url = h.route_url('pullrequest_show',
1109 repo_name=pr_target_repo.repo_name,
1109 repo_name=pr_target_repo.repo_name,
1110 pull_request_id=pull_request_obj.pull_request_id,)
1110 pull_request_id=pull_request_obj.pull_request_id,)
1111
1111
1112 # set some variables for email notification
1112 # set some variables for email notification
1113 pr_target_repo_url = h.route_url(
1113 pr_target_repo_url = h.route_url(
1114 'repo_summary', repo_name=pr_target_repo.repo_name)
1114 'repo_summary', repo_name=pr_target_repo.repo_name)
1115
1115
1116 pr_source_repo_url = h.route_url(
1116 pr_source_repo_url = h.route_url(
1117 'repo_summary', repo_name=pr_source_repo.repo_name)
1117 'repo_summary', repo_name=pr_source_repo.repo_name)
1118
1118
1119 # pull request specifics
1119 # pull request specifics
1120 pull_request_commits = [
1120 pull_request_commits = [
1121 (x.raw_id, x.message)
1121 (x.raw_id, x.message)
1122 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1122 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1123
1123
1124 kwargs = {
1124 kwargs = {
1125 'user': pull_request.author,
1125 'user': pull_request.author,
1126 'pull_request': pull_request_obj,
1126 'pull_request': pull_request_obj,
1127 'pull_request_commits': pull_request_commits,
1127 'pull_request_commits': pull_request_commits,
1128
1128
1129 'pull_request_target_repo': pr_target_repo,
1129 'pull_request_target_repo': pr_target_repo,
1130 'pull_request_target_repo_url': pr_target_repo_url,
1130 'pull_request_target_repo_url': pr_target_repo_url,
1131
1131
1132 'pull_request_source_repo': pr_source_repo,
1132 'pull_request_source_repo': pr_source_repo,
1133 'pull_request_source_repo_url': pr_source_repo_url,
1133 'pull_request_source_repo_url': pr_source_repo_url,
1134
1134
1135 'pull_request_url': pr_url,
1135 'pull_request_url': pr_url,
1136 }
1136 }
1137
1137
1138 # pre-generate the subject for notification itself
1138 # pre-generate the subject for notification itself
1139 (subject,
1139 (subject,
1140 _h, _e, # we don't care about those
1140 _h, _e, # we don't care about those
1141 body_plaintext) = EmailNotificationModel().render_email(
1141 body_plaintext) = EmailNotificationModel().render_email(
1142 notification_type, **kwargs)
1142 notification_type, **kwargs)
1143
1143
1144 # create notification objects, and emails
1144 # create notification objects, and emails
1145 NotificationModel().create(
1145 NotificationModel().create(
1146 created_by=pull_request.author,
1146 created_by=pull_request.author,
1147 notification_subject=subject,
1147 notification_subject=subject,
1148 notification_body=body_plaintext,
1148 notification_body=body_plaintext,
1149 notification_type=notification_type,
1149 notification_type=notification_type,
1150 recipients=recipients,
1150 recipients=recipients,
1151 email_kwargs=kwargs,
1151 email_kwargs=kwargs,
1152 )
1152 )
1153
1153
1154 def delete(self, pull_request, user):
1154 def delete(self, pull_request, user):
1155 pull_request = self.__get_pull_request(pull_request)
1155 pull_request = self.__get_pull_request(pull_request)
1156 old_data = pull_request.get_api_data(with_merge_state=False)
1156 old_data = pull_request.get_api_data(with_merge_state=False)
1157 self._cleanup_merge_workspace(pull_request)
1157 self._cleanup_merge_workspace(pull_request)
1158 self._log_audit_action(
1158 self._log_audit_action(
1159 'repo.pull_request.delete', {'old_data': old_data},
1159 'repo.pull_request.delete', {'old_data': old_data},
1160 user, pull_request)
1160 user, pull_request)
1161 Session().delete(pull_request)
1161 Session().delete(pull_request)
1162
1162
1163 def close_pull_request(self, pull_request, user):
1163 def close_pull_request(self, pull_request, user):
1164 pull_request = self.__get_pull_request(pull_request)
1164 pull_request = self.__get_pull_request(pull_request)
1165 self._cleanup_merge_workspace(pull_request)
1165 self._cleanup_merge_workspace(pull_request)
1166 pull_request.status = PullRequest.STATUS_CLOSED
1166 pull_request.status = PullRequest.STATUS_CLOSED
1167 pull_request.updated_on = datetime.datetime.now()
1167 pull_request.updated_on = datetime.datetime.now()
1168 Session().add(pull_request)
1168 Session().add(pull_request)
1169 self._trigger_pull_request_hook(
1169 self._trigger_pull_request_hook(
1170 pull_request, pull_request.author, 'close')
1170 pull_request, pull_request.author, 'close')
1171
1171
1172 pr_data = pull_request.get_api_data(with_merge_state=False)
1172 pr_data = pull_request.get_api_data(with_merge_state=False)
1173 self._log_audit_action(
1173 self._log_audit_action(
1174 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1174 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1175
1175
1176 def close_pull_request_with_comment(
1176 def close_pull_request_with_comment(
1177 self, pull_request, user, repo, message=None):
1177 self, pull_request, user, repo, message=None):
1178
1178
1179 pull_request_review_status = pull_request.calculated_review_status()
1179 pull_request_review_status = pull_request.calculated_review_status()
1180
1180
1181 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1181 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1182 # approved only if we have voting consent
1182 # approved only if we have voting consent
1183 status = ChangesetStatus.STATUS_APPROVED
1183 status = ChangesetStatus.STATUS_APPROVED
1184 else:
1184 else:
1185 status = ChangesetStatus.STATUS_REJECTED
1185 status = ChangesetStatus.STATUS_REJECTED
1186 status_lbl = ChangesetStatus.get_status_lbl(status)
1186 status_lbl = ChangesetStatus.get_status_lbl(status)
1187
1187
1188 default_message = (
1188 default_message = (
1189 'Closing with status change {transition_icon} {status}.'
1189 'Closing with status change {transition_icon} {status}.'
1190 ).format(transition_icon='>', status=status_lbl)
1190 ).format(transition_icon='>', status=status_lbl)
1191 text = message or default_message
1191 text = message or default_message
1192
1192
1193 # create a comment, and link it to new status
1193 # create a comment, and link it to new status
1194 comment = CommentsModel().create(
1194 comment = CommentsModel().create(
1195 text=text,
1195 text=text,
1196 repo=repo.repo_id,
1196 repo=repo.repo_id,
1197 user=user.user_id,
1197 user=user.user_id,
1198 pull_request=pull_request.pull_request_id,
1198 pull_request=pull_request.pull_request_id,
1199 status_change=status_lbl,
1199 status_change=status_lbl,
1200 status_change_type=status,
1200 status_change_type=status,
1201 closing_pr=True
1201 closing_pr=True
1202 )
1202 )
1203
1203
1204 # calculate old status before we change it
1204 # calculate old status before we change it
1205 old_calculated_status = pull_request.calculated_review_status()
1205 old_calculated_status = pull_request.calculated_review_status()
1206 ChangesetStatusModel().set_status(
1206 ChangesetStatusModel().set_status(
1207 repo.repo_id,
1207 repo.repo_id,
1208 status,
1208 status,
1209 user.user_id,
1209 user.user_id,
1210 comment=comment,
1210 comment=comment,
1211 pull_request=pull_request.pull_request_id
1211 pull_request=pull_request.pull_request_id
1212 )
1212 )
1213
1213
1214 Session().flush()
1214 Session().flush()
1215 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1215 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1216 # we now calculate the status of pull request again, and based on that
1216 # we now calculate the status of pull request again, and based on that
1217 # calculation trigger status change. This might happen in cases
1217 # calculation trigger status change. This might happen in cases
1218 # that non-reviewer admin closes a pr, which means his vote doesn't
1218 # that non-reviewer admin closes a pr, which means his vote doesn't
1219 # change the status, while if he's a reviewer this might change it.
1219 # change the status, while if he's a reviewer this might change it.
1220 calculated_status = pull_request.calculated_review_status()
1220 calculated_status = pull_request.calculated_review_status()
1221 if old_calculated_status != calculated_status:
1221 if old_calculated_status != calculated_status:
1222 self._trigger_pull_request_hook(
1222 self._trigger_pull_request_hook(
1223 pull_request, user, 'review_status_change')
1223 pull_request, user, 'review_status_change')
1224
1224
1225 # finally close the PR
1225 # finally close the PR
1226 PullRequestModel().close_pull_request(
1226 PullRequestModel().close_pull_request(
1227 pull_request.pull_request_id, user)
1227 pull_request.pull_request_id, user)
1228
1228
1229 return comment, status
1229 return comment, status
1230
1230
1231 def merge_status(self, pull_request, translator=None,
1231 def merge_status(self, pull_request, translator=None,
1232 force_shadow_repo_refresh=False):
1232 force_shadow_repo_refresh=False):
1233 _ = translator or get_current_request().translate
1233 _ = translator or get_current_request().translate
1234
1234
1235 if not self._is_merge_enabled(pull_request):
1235 if not self._is_merge_enabled(pull_request):
1236 return False, _('Server-side pull request merging is disabled.')
1236 return False, _('Server-side pull request merging is disabled.')
1237 if pull_request.is_closed():
1237 if pull_request.is_closed():
1238 return False, _('This pull request is closed.')
1238 return False, _('This pull request is closed.')
1239 merge_possible, msg = self._check_repo_requirements(
1239 merge_possible, msg = self._check_repo_requirements(
1240 target=pull_request.target_repo, source=pull_request.source_repo,
1240 target=pull_request.target_repo, source=pull_request.source_repo,
1241 translator=_)
1241 translator=_)
1242 if not merge_possible:
1242 if not merge_possible:
1243 return merge_possible, msg
1243 return merge_possible, msg
1244
1244
1245 try:
1245 try:
1246 resp = self._try_merge(
1246 resp = self._try_merge(
1247 pull_request,
1247 pull_request,
1248 force_shadow_repo_refresh=force_shadow_repo_refresh)
1248 force_shadow_repo_refresh=force_shadow_repo_refresh)
1249 log.debug("Merge response: %s", resp)
1249 log.debug("Merge response: %s", resp)
1250 status = resp.possible, self.merge_status_message(
1250 status = resp.possible, self.merge_status_message(
1251 resp.failure_reason)
1251 resp.failure_reason)
1252 except NotImplementedError:
1252 except NotImplementedError:
1253 status = False, _('Pull request merging is not supported.')
1253 status = False, _('Pull request merging is not supported.')
1254
1254
1255 return status
1255 return status
1256
1256
1257 def _check_repo_requirements(self, target, source, translator):
1257 def _check_repo_requirements(self, target, source, translator):
1258 """
1258 """
1259 Check if `target` and `source` have compatible requirements.
1259 Check if `target` and `source` have compatible requirements.
1260
1260
1261 Currently this is just checking for largefiles.
1261 Currently this is just checking for largefiles.
1262 """
1262 """
1263 _ = translator
1263 _ = translator
1264 target_has_largefiles = self._has_largefiles(target)
1264 target_has_largefiles = self._has_largefiles(target)
1265 source_has_largefiles = self._has_largefiles(source)
1265 source_has_largefiles = self._has_largefiles(source)
1266 merge_possible = True
1266 merge_possible = True
1267 message = u''
1267 message = u''
1268
1268
1269 if target_has_largefiles != source_has_largefiles:
1269 if target_has_largefiles != source_has_largefiles:
1270 merge_possible = False
1270 merge_possible = False
1271 if source_has_largefiles:
1271 if source_has_largefiles:
1272 message = _(
1272 message = _(
1273 'Target repository large files support is disabled.')
1273 'Target repository large files support is disabled.')
1274 else:
1274 else:
1275 message = _(
1275 message = _(
1276 'Source repository large files support is disabled.')
1276 'Source repository large files support is disabled.')
1277
1277
1278 return merge_possible, message
1278 return merge_possible, message
1279
1279
1280 def _has_largefiles(self, repo):
1280 def _has_largefiles(self, repo):
1281 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1281 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1282 'extensions', 'largefiles')
1282 'extensions', 'largefiles')
1283 return largefiles_ui and largefiles_ui[0].active
1283 return largefiles_ui and largefiles_ui[0].active
1284
1284
1285 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1285 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1286 """
1286 """
1287 Try to merge the pull request and return the merge status.
1287 Try to merge the pull request and return the merge status.
1288 """
1288 """
1289 log.debug(
1289 log.debug(
1290 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1290 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1291 pull_request.pull_request_id, force_shadow_repo_refresh)
1291 pull_request.pull_request_id, force_shadow_repo_refresh)
1292 target_vcs = pull_request.target_repo.scm_instance()
1292 target_vcs = pull_request.target_repo.scm_instance()
1293
1293
1294 # Refresh the target reference.
1294 # Refresh the target reference.
1295 try:
1295 try:
1296 target_ref = self._refresh_reference(
1296 target_ref = self._refresh_reference(
1297 pull_request.target_ref_parts, target_vcs)
1297 pull_request.target_ref_parts, target_vcs)
1298 except CommitDoesNotExistError:
1298 except CommitDoesNotExistError:
1299 merge_state = MergeResponse(
1299 merge_state = MergeResponse(
1300 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1300 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1301 return merge_state
1301 return merge_state
1302
1302
1303 target_locked = pull_request.target_repo.locked
1303 target_locked = pull_request.target_repo.locked
1304 if target_locked and target_locked[0]:
1304 if target_locked and target_locked[0]:
1305 log.debug("The target repository is locked.")
1305 log.debug("The target repository is locked.")
1306 merge_state = MergeResponse(
1306 merge_state = MergeResponse(
1307 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1307 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1308 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1308 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1309 pull_request, target_ref):
1309 pull_request, target_ref):
1310 log.debug("Refreshing the merge status of the repository.")
1310 log.debug("Refreshing the merge status of the repository.")
1311 merge_state = self._refresh_merge_state(
1311 merge_state = self._refresh_merge_state(
1312 pull_request, target_vcs, target_ref)
1312 pull_request, target_vcs, target_ref)
1313 else:
1313 else:
1314 possible = pull_request.\
1314 possible = pull_request.\
1315 last_merge_status == MergeFailureReason.NONE
1315 last_merge_status == MergeFailureReason.NONE
1316 merge_state = MergeResponse(
1316 merge_state = MergeResponse(
1317 possible, False, None, pull_request.last_merge_status)
1317 possible, False, None, pull_request.last_merge_status)
1318
1318
1319 return merge_state
1319 return merge_state
1320
1320
1321 def _refresh_reference(self, reference, vcs_repository):
1321 def _refresh_reference(self, reference, vcs_repository):
1322 if reference.type in ('branch', 'book'):
1322 if reference.type in ('branch', 'book'):
1323 name_or_id = reference.name
1323 name_or_id = reference.name
1324 else:
1324 else:
1325 name_or_id = reference.commit_id
1325 name_or_id = reference.commit_id
1326 refreshed_commit = vcs_repository.get_commit(name_or_id)
1326 refreshed_commit = vcs_repository.get_commit(name_or_id)
1327 refreshed_reference = Reference(
1327 refreshed_reference = Reference(
1328 reference.type, reference.name, refreshed_commit.raw_id)
1328 reference.type, reference.name, refreshed_commit.raw_id)
1329 return refreshed_reference
1329 return refreshed_reference
1330
1330
1331 def _needs_merge_state_refresh(self, pull_request, target_reference):
1331 def _needs_merge_state_refresh(self, pull_request, target_reference):
1332 return not(
1332 return not(
1333 pull_request.revisions and
1333 pull_request.revisions and
1334 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1334 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1335 target_reference.commit_id == pull_request._last_merge_target_rev)
1335 target_reference.commit_id == pull_request._last_merge_target_rev)
1336
1336
1337 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1337 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1338 workspace_id = self._workspace_id(pull_request)
1338 workspace_id = self._workspace_id(pull_request)
1339 source_vcs = pull_request.source_repo.scm_instance()
1339 source_vcs = pull_request.source_repo.scm_instance()
1340 repo_id = pull_request.target_repo.repo_id
1340 repo_id = pull_request.target_repo.repo_id
1341 use_rebase = self._use_rebase_for_merging(pull_request)
1341 use_rebase = self._use_rebase_for_merging(pull_request)
1342 close_branch = self._close_branch_before_merging(pull_request)
1342 close_branch = self._close_branch_before_merging(pull_request)
1343 merge_state = target_vcs.merge(
1343 merge_state = target_vcs.merge(
1344 repo_id, workspace_id,
1344 repo_id, workspace_id,
1345 target_reference, source_vcs, pull_request.source_ref_parts,
1345 target_reference, source_vcs, pull_request.source_ref_parts,
1346 dry_run=True, use_rebase=use_rebase,
1346 dry_run=True, use_rebase=use_rebase,
1347 close_branch=close_branch)
1347 close_branch=close_branch)
1348
1348
1349 # Do not store the response if there was an unknown error.
1349 # Do not store the response if there was an unknown error.
1350 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1350 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1351 pull_request._last_merge_source_rev = \
1351 pull_request._last_merge_source_rev = \
1352 pull_request.source_ref_parts.commit_id
1352 pull_request.source_ref_parts.commit_id
1353 pull_request._last_merge_target_rev = target_reference.commit_id
1353 pull_request._last_merge_target_rev = target_reference.commit_id
1354 pull_request.last_merge_status = merge_state.failure_reason
1354 pull_request.last_merge_status = merge_state.failure_reason
1355 pull_request.shadow_merge_ref = merge_state.merge_ref
1355 pull_request.shadow_merge_ref = merge_state.merge_ref
1356 Session().add(pull_request)
1356 Session().add(pull_request)
1357 Session().commit()
1357 Session().commit()
1358
1358
1359 return merge_state
1359 return merge_state
1360
1360
1361 def _workspace_id(self, pull_request):
1361 def _workspace_id(self, pull_request):
1362 workspace_id = 'pr-%s' % pull_request.pull_request_id
1362 workspace_id = 'pr-%s' % pull_request.pull_request_id
1363 return workspace_id
1363 return workspace_id
1364
1364
1365 def merge_status_message(self, status_code):
1365 def merge_status_message(self, status_code):
1366 """
1366 """
1367 Return a human friendly error message for the given merge status code.
1367 Return a human friendly error message for the given merge status code.
1368 """
1368 """
1369 return self.MERGE_STATUS_MESSAGES[status_code]
1369 return self.MERGE_STATUS_MESSAGES[status_code]
1370
1370
1371 def generate_repo_data(self, repo, commit_id=None, branch=None,
1371 def generate_repo_data(self, repo, commit_id=None, branch=None,
1372 bookmark=None, translator=None):
1372 bookmark=None, translator=None):
1373 from rhodecode.model.repo import RepoModel
1373 from rhodecode.model.repo import RepoModel
1374
1374
1375 all_refs, selected_ref = \
1375 all_refs, selected_ref = \
1376 self._get_repo_pullrequest_sources(
1376 self._get_repo_pullrequest_sources(
1377 repo.scm_instance(), commit_id=commit_id,
1377 repo.scm_instance(), commit_id=commit_id,
1378 branch=branch, bookmark=bookmark, translator=translator)
1378 branch=branch, bookmark=bookmark, translator=translator)
1379
1379
1380 refs_select2 = []
1380 refs_select2 = []
1381 for element in all_refs:
1381 for element in all_refs:
1382 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1382 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1383 refs_select2.append({'text': element[1], 'children': children})
1383 refs_select2.append({'text': element[1], 'children': children})
1384
1384
1385 return {
1385 return {
1386 'user': {
1386 'user': {
1387 'user_id': repo.user.user_id,
1387 'user_id': repo.user.user_id,
1388 'username': repo.user.username,
1388 'username': repo.user.username,
1389 'firstname': repo.user.first_name,
1389 'firstname': repo.user.first_name,
1390 'lastname': repo.user.last_name,
1390 'lastname': repo.user.last_name,
1391 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1391 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1392 },
1392 },
1393 'name': repo.repo_name,
1393 'name': repo.repo_name,
1394 'link': RepoModel().get_url(repo),
1394 'link': RepoModel().get_url(repo),
1395 'description': h.chop_at_smart(repo.description_safe, '\n'),
1395 'description': h.chop_at_smart(repo.description_safe, '\n'),
1396 'refs': {
1396 'refs': {
1397 'all_refs': all_refs,
1397 'all_refs': all_refs,
1398 'selected_ref': selected_ref,
1398 'selected_ref': selected_ref,
1399 'select2_refs': refs_select2
1399 'select2_refs': refs_select2
1400 }
1400 }
1401 }
1401 }
1402
1402
1403 def generate_pullrequest_title(self, source, source_ref, target):
1403 def generate_pullrequest_title(self, source, source_ref, target):
1404 return u'{source}#{at_ref} to {target}'.format(
1404 return u'{source}#{at_ref} to {target}'.format(
1405 source=source,
1405 source=source,
1406 at_ref=source_ref,
1406 at_ref=source_ref,
1407 target=target,
1407 target=target,
1408 )
1408 )
1409
1409
1410 def _cleanup_merge_workspace(self, pull_request):
1410 def _cleanup_merge_workspace(self, pull_request):
1411 # Merging related cleanup
1411 # Merging related cleanup
1412 repo_id = pull_request.target_repo.repo_id
1412 repo_id = pull_request.target_repo.repo_id
1413 target_scm = pull_request.target_repo.scm_instance()
1413 target_scm = pull_request.target_repo.scm_instance()
1414 workspace_id = self._workspace_id(pull_request)
1414 workspace_id = self._workspace_id(pull_request)
1415
1415
1416 try:
1416 try:
1417 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1417 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1418 except NotImplementedError:
1418 except NotImplementedError:
1419 pass
1419 pass
1420
1420
1421 def _get_repo_pullrequest_sources(
1421 def _get_repo_pullrequest_sources(
1422 self, repo, commit_id=None, branch=None, bookmark=None,
1422 self, repo, commit_id=None, branch=None, bookmark=None,
1423 translator=None):
1423 translator=None):
1424 """
1424 """
1425 Return a structure with repo's interesting commits, suitable for
1425 Return a structure with repo's interesting commits, suitable for
1426 the selectors in pullrequest controller
1426 the selectors in pullrequest controller
1427
1427
1428 :param commit_id: a commit that must be in the list somehow
1428 :param commit_id: a commit that must be in the list somehow
1429 and selected by default
1429 and selected by default
1430 :param branch: a branch that must be in the list and selected
1430 :param branch: a branch that must be in the list and selected
1431 by default - even if closed
1431 by default - even if closed
1432 :param bookmark: a bookmark that must be in the list and selected
1432 :param bookmark: a bookmark that must be in the list and selected
1433 """
1433 """
1434 _ = translator or get_current_request().translate
1434 _ = translator or get_current_request().translate
1435
1435
1436 commit_id = safe_str(commit_id) if commit_id else None
1436 commit_id = safe_str(commit_id) if commit_id else None
1437 branch = safe_str(branch) if branch else None
1437 branch = safe_str(branch) if branch else None
1438 bookmark = safe_str(bookmark) if bookmark else None
1438 bookmark = safe_str(bookmark) if bookmark else None
1439
1439
1440 selected = None
1440 selected = None
1441
1441
1442 # order matters: first source that has commit_id in it will be selected
1442 # order matters: first source that has commit_id in it will be selected
1443 sources = []
1443 sources = []
1444 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1444 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1445 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1445 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1446
1446
1447 if commit_id:
1447 if commit_id:
1448 ref_commit = (h.short_id(commit_id), commit_id)
1448 ref_commit = (h.short_id(commit_id), commit_id)
1449 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1449 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1450
1450
1451 sources.append(
1451 sources.append(
1452 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1452 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1453 )
1453 )
1454
1454
1455 groups = []
1455 groups = []
1456 for group_key, ref_list, group_name, match in sources:
1456 for group_key, ref_list, group_name, match in sources:
1457 group_refs = []
1457 group_refs = []
1458 for ref_name, ref_id in ref_list:
1458 for ref_name, ref_id in ref_list:
1459 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1459 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1460 group_refs.append((ref_key, ref_name))
1460 group_refs.append((ref_key, ref_name))
1461
1461
1462 if not selected:
1462 if not selected:
1463 if set([commit_id, match]) & set([ref_id, ref_name]):
1463 if set([commit_id, match]) & set([ref_id, ref_name]):
1464 selected = ref_key
1464 selected = ref_key
1465
1465
1466 if group_refs:
1466 if group_refs:
1467 groups.append((group_refs, group_name))
1467 groups.append((group_refs, group_name))
1468
1468
1469 if not selected:
1469 if not selected:
1470 ref = commit_id or branch or bookmark
1470 ref = commit_id or branch or bookmark
1471 if ref:
1471 if ref:
1472 raise CommitDoesNotExistError(
1472 raise CommitDoesNotExistError(
1473 'No commit refs could be found matching: %s' % ref)
1473 'No commit refs could be found matching: %s' % ref)
1474 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1474 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1475 selected = 'branch:%s:%s' % (
1475 selected = 'branch:%s:%s' % (
1476 repo.DEFAULT_BRANCH_NAME,
1476 repo.DEFAULT_BRANCH_NAME,
1477 repo.branches[repo.DEFAULT_BRANCH_NAME]
1477 repo.branches[repo.DEFAULT_BRANCH_NAME]
1478 )
1478 )
1479 elif repo.commit_ids:
1479 elif repo.commit_ids:
1480 # make the user select in this case
1480 # make the user select in this case
1481 selected = None
1481 selected = None
1482 else:
1482 else:
1483 raise EmptyRepositoryError()
1483 raise EmptyRepositoryError()
1484 return groups, selected
1484 return groups, selected
1485
1485
1486 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1486 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1487 return self._get_diff_from_pr_or_version(
1487 return self._get_diff_from_pr_or_version(
1488 source_repo, source_ref_id, target_ref_id, context=context)
1488 source_repo, source_ref_id, target_ref_id, context=context)
1489
1489
1490 def _get_diff_from_pr_or_version(
1490 def _get_diff_from_pr_or_version(
1491 self, source_repo, source_ref_id, target_ref_id, context):
1491 self, source_repo, source_ref_id, target_ref_id, context):
1492 target_commit = source_repo.get_commit(
1492 target_commit = source_repo.get_commit(
1493 commit_id=safe_str(target_ref_id))
1493 commit_id=safe_str(target_ref_id))
1494 source_commit = source_repo.get_commit(
1494 source_commit = source_repo.get_commit(
1495 commit_id=safe_str(source_ref_id))
1495 commit_id=safe_str(source_ref_id))
1496 if isinstance(source_repo, Repository):
1496 if isinstance(source_repo, Repository):
1497 vcs_repo = source_repo.scm_instance()
1497 vcs_repo = source_repo.scm_instance()
1498 else:
1498 else:
1499 vcs_repo = source_repo
1499 vcs_repo = source_repo
1500
1500
1501 # TODO: johbo: In the context of an update, we cannot reach
1501 # TODO: johbo: In the context of an update, we cannot reach
1502 # the old commit anymore with our normal mechanisms. It needs
1502 # the old commit anymore with our normal mechanisms. It needs
1503 # some sort of special support in the vcs layer to avoid this
1503 # some sort of special support in the vcs layer to avoid this
1504 # workaround.
1504 # workaround.
1505 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1505 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1506 vcs_repo.alias == 'git'):
1506 vcs_repo.alias == 'git'):
1507 source_commit.raw_id = safe_str(source_ref_id)
1507 source_commit.raw_id = safe_str(source_ref_id)
1508
1508
1509 log.debug('calculating diff between '
1509 log.debug('calculating diff between '
1510 'source_ref:%s and target_ref:%s for repo `%s`',
1510 'source_ref:%s and target_ref:%s for repo `%s`',
1511 target_ref_id, source_ref_id,
1511 target_ref_id, source_ref_id,
1512 safe_unicode(vcs_repo.path))
1512 safe_unicode(vcs_repo.path))
1513
1513
1514 vcs_diff = vcs_repo.get_diff(
1514 vcs_diff = vcs_repo.get_diff(
1515 commit1=target_commit, commit2=source_commit, context=context)
1515 commit1=target_commit, commit2=source_commit, context=context)
1516 return vcs_diff
1516 return vcs_diff
1517
1517
1518 def _is_merge_enabled(self, pull_request):
1518 def _is_merge_enabled(self, pull_request):
1519 return self._get_general_setting(
1519 return self._get_general_setting(
1520 pull_request, 'rhodecode_pr_merge_enabled')
1520 pull_request, 'rhodecode_pr_merge_enabled')
1521
1521
1522 def _use_rebase_for_merging(self, pull_request):
1522 def _use_rebase_for_merging(self, pull_request):
1523 repo_type = pull_request.target_repo.repo_type
1523 repo_type = pull_request.target_repo.repo_type
1524 if repo_type == 'hg':
1524 if repo_type == 'hg':
1525 return self._get_general_setting(
1525 return self._get_general_setting(
1526 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1526 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1527 elif repo_type == 'git':
1527 elif repo_type == 'git':
1528 return self._get_general_setting(
1528 return self._get_general_setting(
1529 pull_request, 'rhodecode_git_use_rebase_for_merging')
1529 pull_request, 'rhodecode_git_use_rebase_for_merging')
1530
1530
1531 return False
1531 return False
1532
1532
1533 def _close_branch_before_merging(self, pull_request):
1533 def _close_branch_before_merging(self, pull_request):
1534 repo_type = pull_request.target_repo.repo_type
1534 repo_type = pull_request.target_repo.repo_type
1535 if repo_type == 'hg':
1535 if repo_type == 'hg':
1536 return self._get_general_setting(
1536 return self._get_general_setting(
1537 pull_request, 'rhodecode_hg_close_branch_before_merging')
1537 pull_request, 'rhodecode_hg_close_branch_before_merging')
1538 elif repo_type == 'git':
1538 elif repo_type == 'git':
1539 return self._get_general_setting(
1539 return self._get_general_setting(
1540 pull_request, 'rhodecode_git_close_branch_before_merging')
1540 pull_request, 'rhodecode_git_close_branch_before_merging')
1541
1541
1542 return False
1542 return False
1543
1543
1544 def _get_general_setting(self, pull_request, settings_key, default=False):
1544 def _get_general_setting(self, pull_request, settings_key, default=False):
1545 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1545 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1546 settings = settings_model.get_general_settings()
1546 settings = settings_model.get_general_settings()
1547 return settings.get(settings_key, default)
1547 return settings.get(settings_key, default)
1548
1548
1549 def _log_audit_action(self, action, action_data, user, pull_request):
1549 def _log_audit_action(self, action, action_data, user, pull_request):
1550 audit_logger.store(
1550 audit_logger.store(
1551 action=action,
1551 action=action,
1552 action_data=action_data,
1552 action_data=action_data,
1553 user=user,
1553 user=user,
1554 repo=pull_request.target_repo)
1554 repo=pull_request.target_repo)
1555
1555
1556 def get_reviewer_functions(self):
1556 def get_reviewer_functions(self):
1557 """
1557 """
1558 Fetches functions for validation and fetching default reviewers.
1558 Fetches functions for validation and fetching default reviewers.
1559 If available we use the EE package, else we fallback to CE
1559 If available we use the EE package, else we fallback to CE
1560 package functions
1560 package functions
1561 """
1561 """
1562 try:
1562 try:
1563 from rc_reviewers.utils import get_default_reviewers_data
1563 from rc_reviewers.utils import get_default_reviewers_data
1564 from rc_reviewers.utils import validate_default_reviewers
1564 from rc_reviewers.utils import validate_default_reviewers
1565 except ImportError:
1565 except ImportError:
1566 from rhodecode.apps.repository.utils import \
1566 from rhodecode.apps.repository.utils import \
1567 get_default_reviewers_data
1567 get_default_reviewers_data
1568 from rhodecode.apps.repository.utils import \
1568 from rhodecode.apps.repository.utils import \
1569 validate_default_reviewers
1569 validate_default_reviewers
1570
1570
1571 return get_default_reviewers_data, validate_default_reviewers
1571 return get_default_reviewers_data, validate_default_reviewers
1572
1572
1573
1573
1574 class MergeCheck(object):
1574 class MergeCheck(object):
1575 """
1575 """
1576 Perform Merge Checks and returns a check object which stores information
1576 Perform Merge Checks and returns a check object which stores information
1577 about merge errors, and merge conditions
1577 about merge errors, and merge conditions
1578 """
1578 """
1579 TODO_CHECK = 'todo'
1579 TODO_CHECK = 'todo'
1580 PERM_CHECK = 'perm'
1580 PERM_CHECK = 'perm'
1581 REVIEW_CHECK = 'review'
1581 REVIEW_CHECK = 'review'
1582 MERGE_CHECK = 'merge'
1582 MERGE_CHECK = 'merge'
1583
1583
1584 def __init__(self):
1584 def __init__(self):
1585 self.review_status = None
1585 self.review_status = None
1586 self.merge_possible = None
1586 self.merge_possible = None
1587 self.merge_msg = ''
1587 self.merge_msg = ''
1588 self.failed = None
1588 self.failed = None
1589 self.errors = []
1589 self.errors = []
1590 self.error_details = OrderedDict()
1590 self.error_details = OrderedDict()
1591
1591
1592 def push_error(self, error_type, message, error_key, details):
1592 def push_error(self, error_type, message, error_key, details):
1593 self.failed = True
1593 self.failed = True
1594 self.errors.append([error_type, message])
1594 self.errors.append([error_type, message])
1595 self.error_details[error_key] = dict(
1595 self.error_details[error_key] = dict(
1596 details=details,
1596 details=details,
1597 error_type=error_type,
1597 error_type=error_type,
1598 message=message
1598 message=message
1599 )
1599 )
1600
1600
1601 @classmethod
1601 @classmethod
1602 def validate(cls, pull_request, user, translator, fail_early=False,
1602 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1603 force_shadow_repo_refresh=False):
1603 force_shadow_repo_refresh=False):
1604 _ = translator
1604 _ = translator
1605 merge_check = cls()
1605 merge_check = cls()
1606
1606
1607 # permissions to merge
1607 # permissions to merge
1608 user_allowed_to_merge = PullRequestModel().check_user_merge(
1608 user_allowed_to_merge = PullRequestModel().check_user_merge(
1609 pull_request, user)
1609 pull_request, auth_user)
1610 if not user_allowed_to_merge:
1610 if not user_allowed_to_merge:
1611 log.debug("MergeCheck: cannot merge, approval is pending.")
1611 log.debug("MergeCheck: cannot merge, approval is pending.")
1612
1612
1613 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1613 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1614 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1614 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1615 if fail_early:
1616 return merge_check
1617
1618 # permission to merge into the target branch
1619 target_commit_id = pull_request.target_ref_parts.commit_id
1620 if pull_request.target_ref_parts.type == 'branch':
1621 branch_name = pull_request.target_ref_parts.name
1622 else:
1623 # for mercurial we can always figure out the branch from the commit
1624 # in case of bookmark
1625 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1626 branch_name = target_commit.branch
1627
1628 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1629 pull_request.target_repo.repo_name, branch_name)
1630 if branch_perm and branch_perm == 'branch.none':
1631 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1632 branch_name, rule)
1633 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1615 if fail_early:
1634 if fail_early:
1616 return merge_check
1635 return merge_check
1617
1636
1618 # review status, must be always present
1637 # review status, must be always present
1619 review_status = pull_request.calculated_review_status()
1638 review_status = pull_request.calculated_review_status()
1620 merge_check.review_status = review_status
1639 merge_check.review_status = review_status
1621
1640
1622 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1641 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1623 if not status_approved:
1642 if not status_approved:
1624 log.debug("MergeCheck: cannot merge, approval is pending.")
1643 log.debug("MergeCheck: cannot merge, approval is pending.")
1625
1644
1626 msg = _('Pull request reviewer approval is pending.')
1645 msg = _('Pull request reviewer approval is pending.')
1627
1646
1628 merge_check.push_error(
1647 merge_check.push_error(
1629 'warning', msg, cls.REVIEW_CHECK, review_status)
1648 'warning', msg, cls.REVIEW_CHECK, review_status)
1630
1649
1631 if fail_early:
1650 if fail_early:
1632 return merge_check
1651 return merge_check
1633
1652
1634 # left over TODOs
1653 # left over TODOs
1635 todos = CommentsModel().get_unresolved_todos(pull_request)
1654 todos = CommentsModel().get_unresolved_todos(pull_request)
1636 if todos:
1655 if todos:
1637 log.debug("MergeCheck: cannot merge, {} "
1656 log.debug("MergeCheck: cannot merge, {} "
1638 "unresolved todos left.".format(len(todos)))
1657 "unresolved todos left.".format(len(todos)))
1639
1658
1640 if len(todos) == 1:
1659 if len(todos) == 1:
1641 msg = _('Cannot merge, {} TODO still not resolved.').format(
1660 msg = _('Cannot merge, {} TODO still not resolved.').format(
1642 len(todos))
1661 len(todos))
1643 else:
1662 else:
1644 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1663 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1645 len(todos))
1664 len(todos))
1646
1665
1647 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1666 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1648
1667
1649 if fail_early:
1668 if fail_early:
1650 return merge_check
1669 return merge_check
1651
1670
1652 # merge possible, here is the filesystem simulation + shadow repo
1671 # merge possible, here is the filesystem simulation + shadow repo
1653 merge_status, msg = PullRequestModel().merge_status(
1672 merge_status, msg = PullRequestModel().merge_status(
1654 pull_request, translator=translator,
1673 pull_request, translator=translator,
1655 force_shadow_repo_refresh=force_shadow_repo_refresh)
1674 force_shadow_repo_refresh=force_shadow_repo_refresh)
1656 merge_check.merge_possible = merge_status
1675 merge_check.merge_possible = merge_status
1657 merge_check.merge_msg = msg
1676 merge_check.merge_msg = msg
1658 if not merge_status:
1677 if not merge_status:
1659 log.debug(
1678 log.debug(
1660 "MergeCheck: cannot merge, pull request merge not possible.")
1679 "MergeCheck: cannot merge, pull request merge not possible.")
1661 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1680 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1662
1681
1663 if fail_early:
1682 if fail_early:
1664 return merge_check
1683 return merge_check
1665
1684
1666 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1685 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1667 return merge_check
1686 return merge_check
1668
1687
1669 @classmethod
1688 @classmethod
1670 def get_merge_conditions(cls, pull_request, translator):
1689 def get_merge_conditions(cls, pull_request, translator):
1671 _ = translator
1690 _ = translator
1672 merge_details = {}
1691 merge_details = {}
1673
1692
1674 model = PullRequestModel()
1693 model = PullRequestModel()
1675 use_rebase = model._use_rebase_for_merging(pull_request)
1694 use_rebase = model._use_rebase_for_merging(pull_request)
1676
1695
1677 if use_rebase:
1696 if use_rebase:
1678 merge_details['merge_strategy'] = dict(
1697 merge_details['merge_strategy'] = dict(
1679 details={},
1698 details={},
1680 message=_('Merge strategy: rebase')
1699 message=_('Merge strategy: rebase')
1681 )
1700 )
1682 else:
1701 else:
1683 merge_details['merge_strategy'] = dict(
1702 merge_details['merge_strategy'] = dict(
1684 details={},
1703 details={},
1685 message=_('Merge strategy: explicit merge commit')
1704 message=_('Merge strategy: explicit merge commit')
1686 )
1705 )
1687
1706
1688 close_branch = model._close_branch_before_merging(pull_request)
1707 close_branch = model._close_branch_before_merging(pull_request)
1689 if close_branch:
1708 if close_branch:
1690 repo_type = pull_request.target_repo.repo_type
1709 repo_type = pull_request.target_repo.repo_type
1691 if repo_type == 'hg':
1710 if repo_type == 'hg':
1692 close_msg = _('Source branch will be closed after merge.')
1711 close_msg = _('Source branch will be closed after merge.')
1693 elif repo_type == 'git':
1712 elif repo_type == 'git':
1694 close_msg = _('Source branch will be deleted after merge.')
1713 close_msg = _('Source branch will be deleted after merge.')
1695
1714
1696 merge_details['close_branch'] = dict(
1715 merge_details['close_branch'] = dict(
1697 details={},
1716 details={},
1698 message=close_msg
1717 message=close_msg
1699 )
1718 )
1700
1719
1701 return merge_details
1720 return merge_details
1702
1721
1703 ChangeTuple = collections.namedtuple(
1722 ChangeTuple = collections.namedtuple(
1704 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1723 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1705
1724
1706 FileChangeTuple = collections.namedtuple(
1725 FileChangeTuple = collections.namedtuple(
1707 'FileChangeTuple', ['added', 'modified', 'removed'])
1726 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now