##// END OF EJS Templates
pull-requests: validate ref types for pull request so users cannot provide wrongs ones.
marcink -
r3302:2e5cf174 stable
parent child Browse files
Show More
@@ -1,349 +1,368 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 @pytest.mark.parametrize('source_ref', [
60 'bookmarg:default:initial'
61 ])
62 def test_create_with_wrong_refs_data(self, backend, source_ref):
63
64 data = self._prepare_data(backend)
65 data['source_ref'] = source_ref
66
67 id_, params = build_data(
68 self.apikey_regular, 'create_pull_request', **data)
69
70 response = api_call(self.app, params)
71
72 expected = "Ref `{}` type is not allowed. " \
73 "Only:['bookmark', 'book', 'tag', 'branch'] " \
74 "are possible.".format(source_ref)
75 assert_error(id_, expected, given=response.body)
76
77 @pytest.mark.backends("git", "hg")
59 def test_create_with_correct_data(self, backend):
78 def test_create_with_correct_data(self, backend):
60 data = self._prepare_data(backend)
79 data = self._prepare_data(backend)
61 RepoModel().revoke_user_permission(
80 RepoModel().revoke_user_permission(
62 self.source.repo_name, User.DEFAULT_USER)
81 self.source.repo_name, User.DEFAULT_USER)
63 id_, params = build_data(
82 id_, params = build_data(
64 self.apikey_regular, 'create_pull_request', **data)
83 self.apikey_regular, 'create_pull_request', **data)
65 response = api_call(self.app, params)
84 response = api_call(self.app, params)
66 expected_message = "Created new pull request `{title}`".format(
85 expected_message = "Created new pull request `{title}`".format(
67 title=data['title'])
86 title=data['title'])
68 result = response.json
87 result = response.json
69 assert result['error'] is None
88 assert result['error'] is None
70 assert result['result']['msg'] == expected_message
89 assert result['result']['msg'] == expected_message
71 pull_request_id = result['result']['pull_request_id']
90 pull_request_id = result['result']['pull_request_id']
72 pull_request = PullRequestModel().get(pull_request_id)
91 pull_request = PullRequestModel().get(pull_request_id)
73 assert pull_request.title == data['title']
92 assert pull_request.title == data['title']
74 assert pull_request.description == data['description']
93 assert pull_request.description == data['description']
75 assert pull_request.source_ref == data['source_ref']
94 assert pull_request.source_ref == data['source_ref']
76 assert pull_request.target_ref == data['target_ref']
95 assert pull_request.target_ref == data['target_ref']
77 assert pull_request.source_repo.repo_name == data['source_repo']
96 assert pull_request.source_repo.repo_name == data['source_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
97 assert pull_request.target_repo.repo_name == data['target_repo']
79 assert pull_request.revisions == [self.commit_ids['change']]
98 assert pull_request.revisions == [self.commit_ids['change']]
80 assert len(pull_request.reviewers) == 1
99 assert len(pull_request.reviewers) == 1
81
100
82 @pytest.mark.backends("git", "hg")
101 @pytest.mark.backends("git", "hg")
83 def test_create_with_empty_description(self, backend):
102 def test_create_with_empty_description(self, backend):
84 data = self._prepare_data(backend)
103 data = self._prepare_data(backend)
85 data.pop('description')
104 data.pop('description')
86 id_, params = build_data(
105 id_, params = build_data(
87 self.apikey_regular, 'create_pull_request', **data)
106 self.apikey_regular, 'create_pull_request', **data)
88 response = api_call(self.app, params)
107 response = api_call(self.app, params)
89 expected_message = "Created new pull request `{title}`".format(
108 expected_message = "Created new pull request `{title}`".format(
90 title=data['title'])
109 title=data['title'])
91 result = response.json
110 result = response.json
92 assert result['error'] is None
111 assert result['error'] is None
93 assert result['result']['msg'] == expected_message
112 assert result['result']['msg'] == expected_message
94 pull_request_id = result['result']['pull_request_id']
113 pull_request_id = result['result']['pull_request_id']
95 pull_request = PullRequestModel().get(pull_request_id)
114 pull_request = PullRequestModel().get(pull_request_id)
96 assert pull_request.description == ''
115 assert pull_request.description == ''
97
116
98 @pytest.mark.backends("git", "hg")
117 @pytest.mark.backends("git", "hg")
99 def test_create_with_empty_title(self, backend):
118 def test_create_with_empty_title(self, backend):
100 data = self._prepare_data(backend)
119 data = self._prepare_data(backend)
101 data.pop('title')
120 data.pop('title')
102 id_, params = build_data(
121 id_, params = build_data(
103 self.apikey_regular, 'create_pull_request', **data)
122 self.apikey_regular, 'create_pull_request', **data)
104 response = api_call(self.app, params)
123 response = api_call(self.app, params)
105 result = response.json
124 result = response.json
106 pull_request_id = result['result']['pull_request_id']
125 pull_request_id = result['result']['pull_request_id']
107 pull_request = PullRequestModel().get(pull_request_id)
126 pull_request = PullRequestModel().get(pull_request_id)
108 data['ref'] = backend.default_branch_name
127 data['ref'] = backend.default_branch_name
109 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
128 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
110 assert pull_request.title == title
129 assert pull_request.title == title
111
130
112 @pytest.mark.backends("git", "hg")
131 @pytest.mark.backends("git", "hg")
113 def test_create_with_reviewers_specified_by_names(
132 def test_create_with_reviewers_specified_by_names(
114 self, backend, no_notifications):
133 self, backend, no_notifications):
115 data = self._prepare_data(backend)
134 data = self._prepare_data(backend)
116 reviewers = [
135 reviewers = [
117 {'username': TEST_USER_REGULAR_LOGIN,
136 {'username': TEST_USER_REGULAR_LOGIN,
118 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
137 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
119 {'username': TEST_USER_ADMIN_LOGIN,
138 {'username': TEST_USER_ADMIN_LOGIN,
120 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
139 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
121 'mandatory': True},
140 'mandatory': True},
122 ]
141 ]
123 data['reviewers'] = reviewers
142 data['reviewers'] = reviewers
124
143
125 id_, params = build_data(
144 id_, params = build_data(
126 self.apikey_regular, 'create_pull_request', **data)
145 self.apikey_regular, 'create_pull_request', **data)
127 response = api_call(self.app, params)
146 response = api_call(self.app, params)
128
147
129 expected_message = "Created new pull request `{title}`".format(
148 expected_message = "Created new pull request `{title}`".format(
130 title=data['title'])
149 title=data['title'])
131 result = response.json
150 result = response.json
132 assert result['error'] is None
151 assert result['error'] is None
133 assert result['result']['msg'] == expected_message
152 assert result['result']['msg'] == expected_message
134 pull_request_id = result['result']['pull_request_id']
153 pull_request_id = result['result']['pull_request_id']
135 pull_request = PullRequestModel().get(pull_request_id)
154 pull_request = PullRequestModel().get(pull_request_id)
136
155
137 actual_reviewers = []
156 actual_reviewers = []
138 for rev in pull_request.reviewers:
157 for rev in pull_request.reviewers:
139 entry = {
158 entry = {
140 'username': rev.user.username,
159 'username': rev.user.username,
141 'reasons': rev.reasons,
160 'reasons': rev.reasons,
142 }
161 }
143 if rev.mandatory:
162 if rev.mandatory:
144 entry['mandatory'] = rev.mandatory
163 entry['mandatory'] = rev.mandatory
145 actual_reviewers.append(entry)
164 actual_reviewers.append(entry)
146
165
147 owner_username = pull_request.target_repo.user.username
166 owner_username = pull_request.target_repo.user.username
148 for spec_reviewer in reviewers[::]:
167 for spec_reviewer in reviewers[::]:
149 # default reviewer will be added who is an owner of the repo
168 # default reviewer will be added who is an owner of the repo
150 # this get's overridden by a add owner to reviewers rule
169 # this get's overridden by a add owner to reviewers rule
151 if spec_reviewer['username'] == owner_username:
170 if spec_reviewer['username'] == owner_username:
152 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
171 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
153 # since owner is more important, we don't inherit mandatory flag
172 # since owner is more important, we don't inherit mandatory flag
154 del spec_reviewer['mandatory']
173 del spec_reviewer['mandatory']
155
174
156 assert sorted(actual_reviewers, key=lambda e: e['username']) \
175 assert sorted(actual_reviewers, key=lambda e: e['username']) \
157 == sorted(reviewers, key=lambda e: e['username'])
176 == sorted(reviewers, key=lambda e: e['username'])
158
177
159 @pytest.mark.backends("git", "hg")
178 @pytest.mark.backends("git", "hg")
160 def test_create_with_reviewers_specified_by_ids(
179 def test_create_with_reviewers_specified_by_ids(
161 self, backend, no_notifications):
180 self, backend, no_notifications):
162 data = self._prepare_data(backend)
181 data = self._prepare_data(backend)
163 reviewers = [
182 reviewers = [
164 {'username': UserModel().get_by_username(
183 {'username': UserModel().get_by_username(
165 TEST_USER_REGULAR_LOGIN).user_id,
184 TEST_USER_REGULAR_LOGIN).user_id,
166 'reasons': ['added manually']},
185 'reasons': ['added manually']},
167 {'username': UserModel().get_by_username(
186 {'username': UserModel().get_by_username(
168 TEST_USER_ADMIN_LOGIN).user_id,
187 TEST_USER_ADMIN_LOGIN).user_id,
169 'reasons': ['added manually']},
188 'reasons': ['added manually']},
170 ]
189 ]
171
190
172 data['reviewers'] = reviewers
191 data['reviewers'] = reviewers
173 id_, params = build_data(
192 id_, params = build_data(
174 self.apikey_regular, 'create_pull_request', **data)
193 self.apikey_regular, 'create_pull_request', **data)
175 response = api_call(self.app, params)
194 response = api_call(self.app, params)
176
195
177 expected_message = "Created new pull request `{title}`".format(
196 expected_message = "Created new pull request `{title}`".format(
178 title=data['title'])
197 title=data['title'])
179 result = response.json
198 result = response.json
180 assert result['error'] is None
199 assert result['error'] is None
181 assert result['result']['msg'] == expected_message
200 assert result['result']['msg'] == expected_message
182 pull_request_id = result['result']['pull_request_id']
201 pull_request_id = result['result']['pull_request_id']
183 pull_request = PullRequestModel().get(pull_request_id)
202 pull_request = PullRequestModel().get(pull_request_id)
184
203
185 actual_reviewers = []
204 actual_reviewers = []
186 for rev in pull_request.reviewers:
205 for rev in pull_request.reviewers:
187 entry = {
206 entry = {
188 'username': rev.user.user_id,
207 'username': rev.user.user_id,
189 'reasons': rev.reasons,
208 'reasons': rev.reasons,
190 }
209 }
191 if rev.mandatory:
210 if rev.mandatory:
192 entry['mandatory'] = rev.mandatory
211 entry['mandatory'] = rev.mandatory
193 actual_reviewers.append(entry)
212 actual_reviewers.append(entry)
194
213
195 owner_user_id = pull_request.target_repo.user.user_id
214 owner_user_id = pull_request.target_repo.user.user_id
196 for spec_reviewer in reviewers[::]:
215 for spec_reviewer in reviewers[::]:
197 # default reviewer will be added who is an owner of the repo
216 # default reviewer will be added who is an owner of the repo
198 # this get's overridden by a add owner to reviewers rule
217 # this get's overridden by a add owner to reviewers rule
199 if spec_reviewer['username'] == owner_user_id:
218 if spec_reviewer['username'] == owner_user_id:
200 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
219 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
201
220
202 assert sorted(actual_reviewers, key=lambda e: e['username']) \
221 assert sorted(actual_reviewers, key=lambda e: e['username']) \
203 == sorted(reviewers, key=lambda e: e['username'])
222 == sorted(reviewers, key=lambda e: e['username'])
204
223
205 @pytest.mark.backends("git", "hg")
224 @pytest.mark.backends("git", "hg")
206 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
225 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
207 data = self._prepare_data(backend)
226 data = self._prepare_data(backend)
208 data['reviewers'] = [{'username': 'somebody'}]
227 data['reviewers'] = [{'username': 'somebody'}]
209 id_, params = build_data(
228 id_, params = build_data(
210 self.apikey_regular, 'create_pull_request', **data)
229 self.apikey_regular, 'create_pull_request', **data)
211 response = api_call(self.app, params)
230 response = api_call(self.app, params)
212 expected_message = 'user `somebody` does not exist'
231 expected_message = 'user `somebody` does not exist'
213 assert_error(id_, expected_message, given=response.body)
232 assert_error(id_, expected_message, given=response.body)
214
233
215 @pytest.mark.backends("git", "hg")
234 @pytest.mark.backends("git", "hg")
216 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
235 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
217 data = self._prepare_data(backend)
236 data = self._prepare_data(backend)
218 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
237 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
219 data['reviewers'] = reviewers
238 data['reviewers'] = reviewers
220 id_, params = build_data(
239 id_, params = build_data(
221 self.apikey_regular, 'create_pull_request', **data)
240 self.apikey_regular, 'create_pull_request', **data)
222 response = api_call(self.app, params)
241 response = api_call(self.app, params)
223 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
242 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
224 assert_error(id_, expected_message, given=response.body)
243 assert_error(id_, expected_message, given=response.body)
225
244
226 @pytest.mark.backends("git", "hg")
245 @pytest.mark.backends("git", "hg")
227 def test_create_with_no_commit_hashes(self, backend):
246 def test_create_with_no_commit_hashes(self, backend):
228 data = self._prepare_data(backend)
247 data = self._prepare_data(backend)
229 expected_source_ref = data['source_ref']
248 expected_source_ref = data['source_ref']
230 expected_target_ref = data['target_ref']
249 expected_target_ref = data['target_ref']
231 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
250 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
232 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
251 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
233 id_, params = build_data(
252 id_, params = build_data(
234 self.apikey_regular, 'create_pull_request', **data)
253 self.apikey_regular, 'create_pull_request', **data)
235 response = api_call(self.app, params)
254 response = api_call(self.app, params)
236 expected_message = "Created new pull request `{title}`".format(
255 expected_message = "Created new pull request `{title}`".format(
237 title=data['title'])
256 title=data['title'])
238 result = response.json
257 result = response.json
239 assert result['result']['msg'] == expected_message
258 assert result['result']['msg'] == expected_message
240 pull_request_id = result['result']['pull_request_id']
259 pull_request_id = result['result']['pull_request_id']
241 pull_request = PullRequestModel().get(pull_request_id)
260 pull_request = PullRequestModel().get(pull_request_id)
242 assert pull_request.source_ref == expected_source_ref
261 assert pull_request.source_ref == expected_source_ref
243 assert pull_request.target_ref == expected_target_ref
262 assert pull_request.target_ref == expected_target_ref
244
263
245 @pytest.mark.backends("git", "hg")
264 @pytest.mark.backends("git", "hg")
246 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
265 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
247 def test_create_fails_with_wrong_repo(self, backend, data_key):
266 def test_create_fails_with_wrong_repo(self, backend, data_key):
248 repo_name = 'fake-repo'
267 repo_name = 'fake-repo'
249 data = self._prepare_data(backend)
268 data = self._prepare_data(backend)
250 data[data_key] = repo_name
269 data[data_key] = repo_name
251 id_, params = build_data(
270 id_, params = build_data(
252 self.apikey_regular, 'create_pull_request', **data)
271 self.apikey_regular, 'create_pull_request', **data)
253 response = api_call(self.app, params)
272 response = api_call(self.app, params)
254 expected_message = 'repository `{}` does not exist'.format(repo_name)
273 expected_message = 'repository `{}` does not exist'.format(repo_name)
255 assert_error(id_, expected_message, given=response.body)
274 assert_error(id_, expected_message, given=response.body)
256
275
257 @pytest.mark.backends("git", "hg")
276 @pytest.mark.backends("git", "hg")
258 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
277 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
259 def test_create_fails_with_non_existing_branch(self, backend, data_key):
278 def test_create_fails_with_non_existing_branch(self, backend, data_key):
260 branch_name = 'test-branch'
279 branch_name = 'test-branch'
261 data = self._prepare_data(backend)
280 data = self._prepare_data(backend)
262 data[data_key] = "branch:{}".format(branch_name)
281 data[data_key] = "branch:{}".format(branch_name)
263 id_, params = build_data(
282 id_, params = build_data(
264 self.apikey_regular, 'create_pull_request', **data)
283 self.apikey_regular, 'create_pull_request', **data)
265 response = api_call(self.app, params)
284 response = api_call(self.app, params)
266 expected_message = 'The specified value:{type}:`{name}` ' \
285 expected_message = 'The specified value:{type}:`{name}` ' \
267 'does not exist, or is not allowed.'.format(type='branch',
286 'does not exist, or is not allowed.'.format(type='branch',
268 name=branch_name)
287 name=branch_name)
269 assert_error(id_, expected_message, given=response.body)
288 assert_error(id_, expected_message, given=response.body)
270
289
271 @pytest.mark.backends("git", "hg")
290 @pytest.mark.backends("git", "hg")
272 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
291 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
273 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
292 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
274 data = self._prepare_data(backend)
293 data = self._prepare_data(backend)
275 ref = 'stange-ref'
294 ref = 'stange-ref'
276 data[data_key] = ref
295 data[data_key] = ref
277 id_, params = build_data(
296 id_, params = build_data(
278 self.apikey_regular, 'create_pull_request', **data)
297 self.apikey_regular, 'create_pull_request', **data)
279 response = api_call(self.app, params)
298 response = api_call(self.app, params)
280 expected_message = (
299 expected_message = (
281 'Ref `{ref}` given in a wrong format. Please check the API'
300 'Ref `{ref}` given in a wrong format. Please check the API'
282 ' documentation for more details'.format(ref=ref))
301 ' documentation for more details'.format(ref=ref))
283 assert_error(id_, expected_message, given=response.body)
302 assert_error(id_, expected_message, given=response.body)
284
303
285 @pytest.mark.backends("git", "hg")
304 @pytest.mark.backends("git", "hg")
286 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
305 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
287 def test_create_fails_with_non_existing_ref(self, backend, data_key):
306 def test_create_fails_with_non_existing_ref(self, backend, data_key):
288 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
307 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
289 ref = self._get_full_ref(backend, commit_id)
308 ref = self._get_full_ref(backend, commit_id)
290 data = self._prepare_data(backend)
309 data = self._prepare_data(backend)
291 data[data_key] = ref
310 data[data_key] = ref
292 id_, params = build_data(
311 id_, params = build_data(
293 self.apikey_regular, 'create_pull_request', **data)
312 self.apikey_regular, 'create_pull_request', **data)
294 response = api_call(self.app, params)
313 response = api_call(self.app, params)
295 expected_message = 'Ref `{}` does not exist'.format(ref)
314 expected_message = 'Ref `{}` does not exist'.format(ref)
296 assert_error(id_, expected_message, given=response.body)
315 assert_error(id_, expected_message, given=response.body)
297
316
298 @pytest.mark.backends("git", "hg")
317 @pytest.mark.backends("git", "hg")
299 def test_create_fails_when_no_revisions(self, backend):
318 def test_create_fails_when_no_revisions(self, backend):
300 data = self._prepare_data(backend, source_head='initial')
319 data = self._prepare_data(backend, source_head='initial')
301 id_, params = build_data(
320 id_, params = build_data(
302 self.apikey_regular, 'create_pull_request', **data)
321 self.apikey_regular, 'create_pull_request', **data)
303 response = api_call(self.app, params)
322 response = api_call(self.app, params)
304 expected_message = 'no commits found'
323 expected_message = 'no commits found'
305 assert_error(id_, expected_message, given=response.body)
324 assert_error(id_, expected_message, given=response.body)
306
325
307 @pytest.mark.backends("git", "hg")
326 @pytest.mark.backends("git", "hg")
308 def test_create_fails_when_no_permissions(self, backend):
327 def test_create_fails_when_no_permissions(self, backend):
309 data = self._prepare_data(backend)
328 data = self._prepare_data(backend)
310 RepoModel().revoke_user_permission(
329 RepoModel().revoke_user_permission(
311 self.source.repo_name, self.test_user)
330 self.source.repo_name, self.test_user)
312 RepoModel().revoke_user_permission(
331 RepoModel().revoke_user_permission(
313 self.source.repo_name, User.DEFAULT_USER)
332 self.source.repo_name, User.DEFAULT_USER)
314
333
315 id_, params = build_data(
334 id_, params = build_data(
316 self.apikey_regular, 'create_pull_request', **data)
335 self.apikey_regular, 'create_pull_request', **data)
317 response = api_call(self.app, params)
336 response = api_call(self.app, params)
318 expected_message = 'repository `{}` does not exist'.format(
337 expected_message = 'repository `{}` does not exist'.format(
319 self.source.repo_name)
338 self.source.repo_name)
320 assert_error(id_, expected_message, given=response.body)
339 assert_error(id_, expected_message, given=response.body)
321
340
322 def _prepare_data(
341 def _prepare_data(
323 self, backend, source_head='change', target_head='initial'):
342 self, backend, source_head='change', target_head='initial'):
324 commits = [
343 commits = [
325 {'message': 'initial'},
344 {'message': 'initial'},
326 {'message': 'change'},
345 {'message': 'change'},
327 {'message': 'new-feature', 'parents': ['initial']},
346 {'message': 'new-feature', 'parents': ['initial']},
328 ]
347 ]
329 self.commit_ids = backend.create_master_repo(commits)
348 self.commit_ids = backend.create_master_repo(commits)
330 self.source = backend.create_repo(heads=[source_head])
349 self.source = backend.create_repo(heads=[source_head])
331 self.target = backend.create_repo(heads=[target_head])
350 self.target = backend.create_repo(heads=[target_head])
332
351
333 data = {
352 data = {
334 'source_repo': self.source.repo_name,
353 'source_repo': self.source.repo_name,
335 'target_repo': self.target.repo_name,
354 'target_repo': self.target.repo_name,
336 'source_ref': self._get_full_ref(
355 'source_ref': self._get_full_ref(
337 backend, self.commit_ids[source_head]),
356 backend, self.commit_ids[source_head]),
338 'target_ref': self._get_full_ref(
357 'target_ref': self._get_full_ref(
339 backend, self.commit_ids[target_head]),
358 backend, self.commit_ids[target_head]),
340 'title': 'Test PR 1',
359 'title': 'Test PR 1',
341 'description': 'Test'
360 'description': 'Test'
342 }
361 }
343 RepoModel().grant_user_permission(
362 RepoModel().grant_user_permission(
344 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
363 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
345 return data
364 return data
346
365
347 def _get_full_ref(self, backend, commit_id):
366 def _get_full_ref(self, backend, commit_id):
348 return 'branch:{branch}:{commit_id}'.format(
367 return 'branch:{branch}:{commit_id}'.format(
349 branch=backend.default_branch_name, commit_id=commit_id)
368 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,295 +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 = 'bookmark: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 = (
90 expected_message = (
91 'The specified value:ancestor:`ref` does not exist, or is not allowed.')
91 'The specified value:bookmark:`ref` does not exist, or is not allowed.')
92 assert excinfo.value.message == expected_message
92 assert excinfo.value.message == expected_message
93
93
94 def test_branch_is_not_found(self):
94 def test_branch_is_not_found(self):
95 repo = Mock()
95 repo = Mock()
96 ref = 'branch:non-existing-one'
96 ref = 'branch:non-existing-one'
97 with patch('rhodecode.api.utils._get_ref_hash')\
97 with patch('rhodecode.api.utils._get_ref_hash')\
98 as _get_ref_hash:
98 as _get_ref_hash:
99 _get_ref_hash.side_effect = KeyError()
99 _get_ref_hash.side_effect = KeyError()
100 with pytest.raises(JSONRPCError) as excinfo:
100 with pytest.raises(JSONRPCError) as excinfo:
101 utils.resolve_ref_or_error(ref, repo)
101 utils.resolve_ref_or_error(ref, repo)
102 expected_message = (
102 expected_message = (
103 'The specified value:branch:`non-existing-one` does not exist, or is not allowed.')
103 'The specified value:branch:`non-existing-one` does not exist, or is not allowed.')
104 assert excinfo.value.message == expected_message
104 assert excinfo.value.message == expected_message
105
105
106 def test_bookmark_is_not_found(self):
106 def test_bookmark_is_not_found(self):
107 repo = Mock()
107 repo = Mock()
108 ref = 'bookmark:non-existing-one'
108 ref = 'bookmark:non-existing-one'
109 with patch('rhodecode.api.utils._get_ref_hash')\
109 with patch('rhodecode.api.utils._get_ref_hash')\
110 as _get_ref_hash:
110 as _get_ref_hash:
111 _get_ref_hash.side_effect = KeyError()
111 _get_ref_hash.side_effect = KeyError()
112 with pytest.raises(JSONRPCError) as excinfo:
112 with pytest.raises(JSONRPCError) as excinfo:
113 utils.resolve_ref_or_error(ref, repo)
113 utils.resolve_ref_or_error(ref, repo)
114 expected_message = (
114 expected_message = (
115 'The specified value:bookmark:`non-existing-one` does not exist, or is not allowed.')
115 'The specified value:bookmark:`non-existing-one` does not exist, or is not allowed.')
116 assert excinfo.value.message == expected_message
116 assert excinfo.value.message == expected_message
117
117
118 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
118 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
119 def test_ref_cannot_be_parsed(self, ref):
119 def test_ref_cannot_be_parsed(self, ref):
120 repo = Mock()
120 repo = Mock()
121 with pytest.raises(JSONRPCError) as excinfo:
121 with pytest.raises(JSONRPCError) as excinfo:
122 utils.resolve_ref_or_error(ref, repo)
122 utils.resolve_ref_or_error(ref, repo)
123 expected_message = (
123 expected_message = (
124 'Ref `{ref}` given in a wrong format. Please check the API'
124 'Ref `{ref}` given in a wrong format. Please check the API'
125 ' documentation for more details'.format(ref=ref)
125 ' documentation for more details'.format(ref=ref)
126 )
126 )
127 assert excinfo.value.message == expected_message
127 assert excinfo.value.message == expected_message
128
128
129
129
130 class TestGetRefHash(object):
130 class TestGetRefHash(object):
131 def setup(self):
131 def setup(self):
132 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
132 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
133 self.bookmark_name = 'test-bookmark'
133 self.bookmark_name = 'test-bookmark'
134
134
135 @pytest.mark.parametrize("alias, branch_name", [
135 @pytest.mark.parametrize("alias, branch_name", [
136 ("git", "master"),
136 ("git", "master"),
137 ("hg", "default")
137 ("hg", "default")
138 ])
138 ])
139 def test_returns_hash_by_branch_name(self, alias, branch_name):
139 def test_returns_hash_by_branch_name(self, alias, branch_name):
140 with patch('rhodecode.model.db.Repository') as repo:
140 with patch('rhodecode.model.db.Repository') as repo:
141 repo.scm_instance().alias = alias
141 repo.scm_instance().alias = alias
142 repo.scm_instance().branches = {branch_name: self.commit_hash}
142 repo.scm_instance().branches = {branch_name: self.commit_hash}
143 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
143 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
144 assert result_hash == self.commit_hash
144 assert result_hash == self.commit_hash
145
145
146 @pytest.mark.parametrize("alias, branch_name", [
146 @pytest.mark.parametrize("alias, branch_name", [
147 ("git", "master"),
147 ("git", "master"),
148 ("hg", "default")
148 ("hg", "default")
149 ])
149 ])
150 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):
151 with patch('rhodecode.model.db.Repository') as repo:
151 with patch('rhodecode.model.db.Repository') as repo:
152 repo.scm_instance().alias = alias
152 repo.scm_instance().alias = alias
153 repo.scm_instance().branches = {}
153 repo.scm_instance().branches = {}
154 with pytest.raises(KeyError):
154 with pytest.raises(KeyError):
155 utils._get_ref_hash(repo, 'branch', branch_name)
155 utils._get_ref_hash(repo, 'branch', branch_name)
156
156
157 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
157 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
158 with patch('rhodecode.model.db.Repository') as repo:
158 with patch('rhodecode.model.db.Repository') as repo:
159 repo.scm_instance().alias = 'hg'
159 repo.scm_instance().alias = 'hg'
160 repo.scm_instance().bookmarks = {
160 repo.scm_instance().bookmarks = {
161 self.bookmark_name: self.commit_hash}
161 self.bookmark_name: self.commit_hash}
162 result_hash = utils._get_ref_hash(
162 result_hash = utils._get_ref_hash(
163 repo, 'bookmark', self.bookmark_name)
163 repo, 'bookmark', self.bookmark_name)
164 assert result_hash == self.commit_hash
164 assert result_hash == self.commit_hash
165
165
166 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):
167 with patch('rhodecode.model.db.Repository') as repo:
167 with patch('rhodecode.model.db.Repository') as repo:
168 repo.scm_instance().alias = 'hg'
168 repo.scm_instance().alias = 'hg'
169 repo.scm_instance().bookmarks = {}
169 repo.scm_instance().bookmarks = {}
170 with pytest.raises(KeyError):
170 with pytest.raises(KeyError):
171 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
171 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
172
172
173 def test_raises_error_when_bookmark_is_specified_for_git(self):
173 def test_raises_error_when_bookmark_is_specified_for_git(self):
174 with patch('rhodecode.model.db.Repository') as repo:
174 with patch('rhodecode.model.db.Repository') as repo:
175 repo.scm_instance().alias = 'git'
175 repo.scm_instance().alias = 'git'
176 repo.scm_instance().bookmarks = {
176 repo.scm_instance().bookmarks = {
177 self.bookmark_name: self.commit_hash}
177 self.bookmark_name: self.commit_hash}
178 with pytest.raises(ValueError):
178 with pytest.raises(ValueError):
179 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
179 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
180
180
181
181
182 class TestUserByNameOrError(object):
182 class TestUserByNameOrError(object):
183 def test_user_found_by_id(self):
183 def test_user_found_by_id(self):
184 fake_user = Mock(id=123)
184 fake_user = Mock(id=123)
185
185
186 patcher = patch('rhodecode.model.user.UserModel.get_user')
186 patcher = patch('rhodecode.model.user.UserModel.get_user')
187 with patcher as get_user:
187 with patcher as get_user:
188 get_user.return_value = fake_user
188 get_user.return_value = fake_user
189
189
190 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
190 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
191 with patcher as get_by_username:
191 with patcher as get_by_username:
192 result = utils.get_user_or_error(123)
192 result = utils.get_user_or_error(123)
193 assert result == fake_user
193 assert result == fake_user
194
194
195 def test_user_not_found_by_id_as_str(self):
195 def test_user_not_found_by_id_as_str(self):
196 fake_user = Mock(id=123)
196 fake_user = Mock(id=123)
197
197
198 patcher = patch('rhodecode.model.user.UserModel.get_user')
198 patcher = patch('rhodecode.model.user.UserModel.get_user')
199 with patcher as get_user:
199 with patcher as get_user:
200 get_user.return_value = fake_user
200 get_user.return_value = fake_user
201 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
201 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
202 with patcher as get_by_username:
202 with patcher as get_by_username:
203 get_by_username.return_value = None
203 get_by_username.return_value = None
204
204
205 with pytest.raises(JSONRPCError):
205 with pytest.raises(JSONRPCError):
206 utils.get_user_or_error('123')
206 utils.get_user_or_error('123')
207
207
208 def test_user_found_by_name(self):
208 def test_user_found_by_name(self):
209 fake_user = Mock(id=123)
209 fake_user = Mock(id=123)
210
210
211 patcher = patch('rhodecode.model.user.UserModel.get_user')
211 patcher = patch('rhodecode.model.user.UserModel.get_user')
212 with patcher as get_user:
212 with patcher as get_user:
213 get_user.return_value = None
213 get_user.return_value = None
214
214
215 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
215 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
216 with patcher as get_by_username:
216 with patcher as get_by_username:
217 get_by_username.return_value = fake_user
217 get_by_username.return_value = fake_user
218
218
219 result = utils.get_user_or_error('test')
219 result = utils.get_user_or_error('test')
220 assert result == fake_user
220 assert result == fake_user
221
221
222 def test_user_not_found_by_id(self):
222 def test_user_not_found_by_id(self):
223 patcher = patch('rhodecode.model.user.UserModel.get_user')
223 patcher = patch('rhodecode.model.user.UserModel.get_user')
224 with patcher as get_user:
224 with patcher as get_user:
225 get_user.return_value = None
225 get_user.return_value = None
226 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
226 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
227 with patcher as get_by_username:
227 with patcher as get_by_username:
228 get_by_username.return_value = None
228 get_by_username.return_value = None
229
229
230 with pytest.raises(JSONRPCError) as excinfo:
230 with pytest.raises(JSONRPCError) as excinfo:
231 utils.get_user_or_error(123)
231 utils.get_user_or_error(123)
232
232
233 expected_message = 'user `123` does not exist'
233 expected_message = 'user `123` does not exist'
234 assert excinfo.value.message == expected_message
234 assert excinfo.value.message == expected_message
235
235
236 def test_user_not_found_by_name(self):
236 def test_user_not_found_by_name(self):
237 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
237 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
238 with patcher as get_by_username:
238 with patcher as get_by_username:
239 get_by_username.return_value = None
239 get_by_username.return_value = None
240 with pytest.raises(JSONRPCError) as excinfo:
240 with pytest.raises(JSONRPCError) as excinfo:
241 utils.get_user_or_error('test')
241 utils.get_user_or_error('test')
242
242
243 expected_message = 'user `test` does not exist'
243 expected_message = 'user `test` does not exist'
244 assert excinfo.value.message == expected_message
244 assert excinfo.value.message == expected_message
245
245
246
246
247 class TestGetCommitDict(object):
247 class TestGetCommitDict(object):
248 @pytest.mark.parametrize('filename, expected', [
248 @pytest.mark.parametrize('filename, expected', [
249 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
249 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
250 (b'sp\xa4cial', u'sp\ufffdcial'),
250 (b'sp\xa4cial', u'sp\ufffdcial'),
251 ])
251 ])
252 def test_decodes_filenames_to_unicode(self, filename, expected):
252 def test_decodes_filenames_to_unicode(self, filename, expected):
253 result = utils._get_commit_dict(filename=filename, op='A')
253 result = utils._get_commit_dict(filename=filename, op='A')
254 assert result['filename'] == expected
254 assert result['filename'] == expected
255
255
256
256
257 class TestRepoAccess(object):
257 class TestRepoAccess(object):
258 def setup_method(self, method):
258 def setup_method(self, method):
259
259
260 self.admin_perm_patch = patch(
260 self.admin_perm_patch = patch(
261 'rhodecode.api.utils.HasPermissionAnyApi')
261 'rhodecode.api.utils.HasPermissionAnyApi')
262 self.repo_perm_patch = patch(
262 self.repo_perm_patch = patch(
263 'rhodecode.api.utils.HasRepoPermissionAnyApi')
263 'rhodecode.api.utils.HasRepoPermissionAnyApi')
264
264
265 def test_has_superadmin_permission_checks_for_admin(self):
265 def test_has_superadmin_permission_checks_for_admin(self):
266 admin_mock = Mock()
266 admin_mock = Mock()
267 with self.admin_perm_patch as amock:
267 with self.admin_perm_patch as amock:
268 amock.return_value = admin_mock
268 amock.return_value = admin_mock
269 assert utils.has_superadmin_permission('fake_user')
269 assert utils.has_superadmin_permission('fake_user')
270 amock.assert_called_once_with('hg.admin')
270 amock.assert_called_once_with('hg.admin')
271
271
272 admin_mock.assert_called_once_with(user='fake_user')
272 admin_mock.assert_called_once_with(user='fake_user')
273
273
274 def test_has_repo_permissions_checks_for_repo_access(self):
274 def test_has_repo_permissions_checks_for_repo_access(self):
275 repo_mock = Mock()
275 repo_mock = Mock()
276 fake_repo = Mock()
276 fake_repo = Mock()
277 with self.repo_perm_patch as rmock:
277 with self.repo_perm_patch as rmock:
278 rmock.return_value = repo_mock
278 rmock.return_value = repo_mock
279 assert utils.validate_repo_permissions(
279 assert utils.validate_repo_permissions(
280 'fake_user', 'fake_repo_id', fake_repo,
280 'fake_user', 'fake_repo_id', fake_repo,
281 ['perm1', 'perm2'])
281 ['perm1', 'perm2'])
282 rmock.assert_called_once_with(*['perm1', 'perm2'])
282 rmock.assert_called_once_with(*['perm1', 'perm2'])
283
283
284 repo_mock.assert_called_once_with(
284 repo_mock.assert_called_once_with(
285 user='fake_user', repo_name=fake_repo.repo_name)
285 user='fake_user', repo_name=fake_repo.repo_name)
286
286
287 def test_has_repo_permissions_raises_not_found(self):
287 def test_has_repo_permissions_raises_not_found(self):
288 repo_mock = Mock(return_value=False)
288 repo_mock = Mock(return_value=False)
289 fake_repo = Mock()
289 fake_repo = Mock()
290 with self.repo_perm_patch as rmock:
290 with self.repo_perm_patch as rmock:
291 rmock.return_value = repo_mock
291 rmock.return_value = repo_mock
292 with pytest.raises(JSONRPCError) as excinfo:
292 with pytest.raises(JSONRPCError) as excinfo:
293 utils.validate_repo_permissions(
293 utils.validate_repo_permissions(
294 'fake_user', 'fake_repo_id', fake_repo, 'perms')
294 'fake_user', 'fake_repo_id', fake_repo, 'perms')
295 assert 'fake_repo_id' in excinfo
295 assert 'fake_repo_id' in excinfo
@@ -1,441 +1,449 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 _get_ref_hash(repo, type_, name):
392 vcs_repo = repo.scm_instance()
393 if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
394 return vcs_repo.branches[name]
395 elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
396 return vcs_repo.bookmarks[name]
397 else:
398 raise ValueError()
399
400
401 def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
402 allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']
403
392 def _parse_ref(type_, name, hash_=None):
404 def _parse_ref(type_, name, hash_=None):
393 return type_, name, hash_
405 return type_, name, hash_
394
406
395 try:
407 try:
396 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
408 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
397 except TypeError:
409 except TypeError:
398 raise JSONRPCError(
410 raise JSONRPCError(
399 'Ref `{ref}` given in a wrong format. Please check the API'
411 'Ref `{ref}` given in a wrong format. Please check the API'
400 ' documentation for more details'.format(ref=ref))
412 ' documentation for more details'.format(ref=ref))
401
413
414 if ref_type not in allowed_ref_types:
415 raise JSONRPCError(
416 'Ref `{ref}` type is not allowed. '
417 'Only:{allowed_refs} are possible.'.format(
418 ref=ref, allowed_refs=allowed_ref_types))
419
402 try:
420 try:
403 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
421 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
404 except (KeyError, ValueError):
422 except (KeyError, ValueError):
405 raise JSONRPCError(
423 raise JSONRPCError(
406 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
424 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
407 type=ref_type, name=ref_name))
425 type=ref_type, name=ref_name))
408
426
409 return ':'.join([ref_type, ref_name, ref_hash])
427 return ':'.join([ref_type, ref_name, ref_hash])
410
428
411
429
412 def _get_commit_dict(
430 def _get_commit_dict(
413 filename, op, new_revision=None, old_revision=None,
431 filename, op, new_revision=None, old_revision=None,
414 raw_diff=None, stats=None):
432 raw_diff=None, stats=None):
415 if stats is None:
433 if stats is None:
416 stats = {
434 stats = {
417 "added": None,
435 "added": None,
418 "binary": None,
436 "binary": None,
419 "deleted": None
437 "deleted": None
420 }
438 }
421 return {
439 return {
422 "filename": safe_unicode(filename),
440 "filename": safe_unicode(filename),
423 "op": op,
441 "op": op,
424
442
425 # extra details
443 # extra details
426 "new_revision": new_revision,
444 "new_revision": new_revision,
427 "old_revision": old_revision,
445 "old_revision": old_revision,
428
446
429 "raw_diff": raw_diff,
447 "raw_diff": raw_diff,
430 "stats": stats
448 "stats": stats
431 }
449 }
432
433
434 def _get_ref_hash(repo, type_, name):
435 vcs_repo = repo.scm_instance()
436 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
437 return vcs_repo.branches[name]
438 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
439 return vcs_repo.bookmarks[name]
440 else:
441 raise ValueError()
@@ -1,1737 +1,1739 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 = diffs.DEFAULT_CONTEXT
76 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
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 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
133 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
132
134
133 def __get_pull_request(self, pull_request):
135 def __get_pull_request(self, pull_request):
134 return self._get_instance((
136 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
137 PullRequest, PullRequestVersion), pull_request)
136
138
137 def _check_perms(self, perms, pull_request, user, api=False):
139 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
140 if not api:
139 return h.HasRepoPermissionAny(*perms)(
141 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
142 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
143 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
144 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
145 user=user, repo_name=pull_request.target_repo.repo_name)
144
146
145 def check_user_read(self, pull_request, user, api=False):
147 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
149 return self._check_perms(_perms, pull_request, user, api)
148
150
149 def check_user_merge(self, pull_request, user, api=False):
151 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
153 return self._check_perms(_perms, pull_request, user, api)
152
154
153 def check_user_update(self, pull_request, user, api=False):
155 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
156 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
157 return self.check_user_merge(pull_request, user, api) or owner
156
158
157 def check_user_delete(self, pull_request, user):
159 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
160 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
161 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
162 return self._check_perms(_perms, pull_request, user) or owner
161
163
162 def check_user_change_status(self, pull_request, user, api=False):
164 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
165 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
166 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
167 return self.check_user_update(pull_request, user, api) or reviewer
166
168
167 def check_user_comment(self, pull_request, user):
169 def check_user_comment(self, pull_request, user):
168 owner = user.user_id == pull_request.user_id
170 owner = user.user_id == pull_request.user_id
169 return self.check_user_read(pull_request, user) or owner
171 return self.check_user_read(pull_request, user) or owner
170
172
171 def get(self, pull_request):
173 def get(self, pull_request):
172 return self.__get_pull_request(pull_request)
174 return self.__get_pull_request(pull_request)
173
175
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
176 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 opened_by=None, order_by=None,
177 opened_by=None, order_by=None,
176 order_dir='desc'):
178 order_dir='desc'):
177 repo = None
179 repo = None
178 if repo_name:
180 if repo_name:
179 repo = self._get_repo(repo_name)
181 repo = self._get_repo(repo_name)
180
182
181 q = PullRequest.query()
183 q = PullRequest.query()
182
184
183 # source or target
185 # source or target
184 if repo and source:
186 if repo and source:
185 q = q.filter(PullRequest.source_repo == repo)
187 q = q.filter(PullRequest.source_repo == repo)
186 elif repo:
188 elif repo:
187 q = q.filter(PullRequest.target_repo == repo)
189 q = q.filter(PullRequest.target_repo == repo)
188
190
189 # closed,opened
191 # closed,opened
190 if statuses:
192 if statuses:
191 q = q.filter(PullRequest.status.in_(statuses))
193 q = q.filter(PullRequest.status.in_(statuses))
192
194
193 # opened by filter
195 # opened by filter
194 if opened_by:
196 if opened_by:
195 q = q.filter(PullRequest.user_id.in_(opened_by))
197 q = q.filter(PullRequest.user_id.in_(opened_by))
196
198
197 if order_by:
199 if order_by:
198 order_map = {
200 order_map = {
199 'name_raw': PullRequest.pull_request_id,
201 'name_raw': PullRequest.pull_request_id,
200 'title': PullRequest.title,
202 'title': PullRequest.title,
201 'updated_on_raw': PullRequest.updated_on,
203 'updated_on_raw': PullRequest.updated_on,
202 'target_repo': PullRequest.target_repo_id
204 'target_repo': PullRequest.target_repo_id
203 }
205 }
204 if order_dir == 'asc':
206 if order_dir == 'asc':
205 q = q.order_by(order_map[order_by].asc())
207 q = q.order_by(order_map[order_by].asc())
206 else:
208 else:
207 q = q.order_by(order_map[order_by].desc())
209 q = q.order_by(order_map[order_by].desc())
208
210
209 return q
211 return q
210
212
211 def count_all(self, repo_name, source=False, statuses=None,
213 def count_all(self, repo_name, source=False, statuses=None,
212 opened_by=None):
214 opened_by=None):
213 """
215 """
214 Count the number of pull requests for a specific repository.
216 Count the number of pull requests for a specific repository.
215
217
216 :param repo_name: target or source repo
218 :param repo_name: target or source repo
217 :param source: boolean flag to specify if repo_name refers to source
219 :param source: boolean flag to specify if repo_name refers to source
218 :param statuses: list of pull request statuses
220 :param statuses: list of pull request statuses
219 :param opened_by: author user of the pull request
221 :param opened_by: author user of the pull request
220 :returns: int number of pull requests
222 :returns: int number of pull requests
221 """
223 """
222 q = self._prepare_get_all_query(
224 q = self._prepare_get_all_query(
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
225 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224
226
225 return q.count()
227 return q.count()
226
228
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
229 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 offset=0, length=None, order_by=None, order_dir='desc'):
230 offset=0, length=None, order_by=None, order_dir='desc'):
229 """
231 """
230 Get all pull requests for a specific repository.
232 Get all pull requests for a specific repository.
231
233
232 :param repo_name: target or source repo
234 :param repo_name: target or source repo
233 :param source: boolean flag to specify if repo_name refers to source
235 :param source: boolean flag to specify if repo_name refers to source
234 :param statuses: list of pull request statuses
236 :param statuses: list of pull request statuses
235 :param opened_by: author user of the pull request
237 :param opened_by: author user of the pull request
236 :param offset: pagination offset
238 :param offset: pagination offset
237 :param length: length of returned list
239 :param length: length of returned list
238 :param order_by: order of the returned list
240 :param order_by: order of the returned list
239 :param order_dir: 'asc' or 'desc' ordering direction
241 :param order_dir: 'asc' or 'desc' ordering direction
240 :returns: list of pull requests
242 :returns: list of pull requests
241 """
243 """
242 q = self._prepare_get_all_query(
244 q = self._prepare_get_all_query(
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
245 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 order_by=order_by, order_dir=order_dir)
246 order_by=order_by, order_dir=order_dir)
245
247
246 if length:
248 if length:
247 pull_requests = q.limit(length).offset(offset).all()
249 pull_requests = q.limit(length).offset(offset).all()
248 else:
250 else:
249 pull_requests = q.all()
251 pull_requests = q.all()
250
252
251 return pull_requests
253 return pull_requests
252
254
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
255 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 opened_by=None):
256 opened_by=None):
255 """
257 """
256 Count the number of pull requests for a specific repository that are
258 Count the number of pull requests for a specific repository that are
257 awaiting review.
259 awaiting review.
258
260
259 :param repo_name: target or source repo
261 :param repo_name: target or source repo
260 :param source: boolean flag to specify if repo_name refers to source
262 :param source: boolean flag to specify if repo_name refers to source
261 :param statuses: list of pull request statuses
263 :param statuses: list of pull request statuses
262 :param opened_by: author user of the pull request
264 :param opened_by: author user of the pull request
263 :returns: int number of pull requests
265 :returns: int number of pull requests
264 """
266 """
265 pull_requests = self.get_awaiting_review(
267 pull_requests = self.get_awaiting_review(
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
268 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267
269
268 return len(pull_requests)
270 return len(pull_requests)
269
271
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
272 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 opened_by=None, offset=0, length=None,
273 opened_by=None, offset=0, length=None,
272 order_by=None, order_dir='desc'):
274 order_by=None, order_dir='desc'):
273 """
275 """
274 Get all pull requests for a specific repository that are awaiting
276 Get all pull requests for a specific repository that are awaiting
275 review.
277 review.
276
278
277 :param repo_name: target or source repo
279 :param repo_name: target or source repo
278 :param source: boolean flag to specify if repo_name refers to source
280 :param source: boolean flag to specify if repo_name refers to source
279 :param statuses: list of pull request statuses
281 :param statuses: list of pull request statuses
280 :param opened_by: author user of the pull request
282 :param opened_by: author user of the pull request
281 :param offset: pagination offset
283 :param offset: pagination offset
282 :param length: length of returned list
284 :param length: length of returned list
283 :param order_by: order of the returned list
285 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
286 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
287 :returns: list of pull requests
286 """
288 """
287 pull_requests = self.get_all(
289 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
290 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
291 order_by=order_by, order_dir=order_dir)
290
292
291 _filtered_pull_requests = []
293 _filtered_pull_requests = []
292 for pr in pull_requests:
294 for pr in pull_requests:
293 status = pr.calculated_review_status()
295 status = pr.calculated_review_status()
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
296 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
297 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 _filtered_pull_requests.append(pr)
298 _filtered_pull_requests.append(pr)
297 if length:
299 if length:
298 return _filtered_pull_requests[offset:offset+length]
300 return _filtered_pull_requests[offset:offset+length]
299 else:
301 else:
300 return _filtered_pull_requests
302 return _filtered_pull_requests
301
303
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
304 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 opened_by=None, user_id=None):
305 opened_by=None, user_id=None):
304 """
306 """
305 Count the number of pull requests for a specific repository that are
307 Count the number of pull requests for a specific repository that are
306 awaiting review from a specific user.
308 awaiting review from a specific user.
307
309
308 :param repo_name: target or source repo
310 :param repo_name: target or source repo
309 :param source: boolean flag to specify if repo_name refers to source
311 :param source: boolean flag to specify if repo_name refers to source
310 :param statuses: list of pull request statuses
312 :param statuses: list of pull request statuses
311 :param opened_by: author user of the pull request
313 :param opened_by: author user of the pull request
312 :param user_id: reviewer user of the pull request
314 :param user_id: reviewer user of the pull request
313 :returns: int number of pull requests
315 :returns: int number of pull requests
314 """
316 """
315 pull_requests = self.get_awaiting_my_review(
317 pull_requests = self.get_awaiting_my_review(
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
318 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 user_id=user_id)
319 user_id=user_id)
318
320
319 return len(pull_requests)
321 return len(pull_requests)
320
322
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
323 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 opened_by=None, user_id=None, offset=0,
324 opened_by=None, user_id=None, offset=0,
323 length=None, order_by=None, order_dir='desc'):
325 length=None, order_by=None, order_dir='desc'):
324 """
326 """
325 Get all pull requests for a specific repository that are awaiting
327 Get all pull requests for a specific repository that are awaiting
326 review from a specific user.
328 review from a specific user.
327
329
328 :param repo_name: target or source repo
330 :param repo_name: target or source repo
329 :param source: boolean flag to specify if repo_name refers to source
331 :param source: boolean flag to specify if repo_name refers to source
330 :param statuses: list of pull request statuses
332 :param statuses: list of pull request statuses
331 :param opened_by: author user of the pull request
333 :param opened_by: author user of the pull request
332 :param user_id: reviewer user of the pull request
334 :param user_id: reviewer user of the pull request
333 :param offset: pagination offset
335 :param offset: pagination offset
334 :param length: length of returned list
336 :param length: length of returned list
335 :param order_by: order of the returned list
337 :param order_by: order of the returned list
336 :param order_dir: 'asc' or 'desc' ordering direction
338 :param order_dir: 'asc' or 'desc' ordering direction
337 :returns: list of pull requests
339 :returns: list of pull requests
338 """
340 """
339 pull_requests = self.get_all(
341 pull_requests = self.get_all(
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
342 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 order_by=order_by, order_dir=order_dir)
343 order_by=order_by, order_dir=order_dir)
342
344
343 _my = PullRequestModel().get_not_reviewed(user_id)
345 _my = PullRequestModel().get_not_reviewed(user_id)
344 my_participation = []
346 my_participation = []
345 for pr in pull_requests:
347 for pr in pull_requests:
346 if pr in _my:
348 if pr in _my:
347 my_participation.append(pr)
349 my_participation.append(pr)
348 _filtered_pull_requests = my_participation
350 _filtered_pull_requests = my_participation
349 if length:
351 if length:
350 return _filtered_pull_requests[offset:offset+length]
352 return _filtered_pull_requests[offset:offset+length]
351 else:
353 else:
352 return _filtered_pull_requests
354 return _filtered_pull_requests
353
355
354 def get_not_reviewed(self, user_id):
356 def get_not_reviewed(self, user_id):
355 return [
357 return [
356 x.pull_request for x in PullRequestReviewers.query().filter(
358 x.pull_request for x in PullRequestReviewers.query().filter(
357 PullRequestReviewers.user_id == user_id).all()
359 PullRequestReviewers.user_id == user_id).all()
358 ]
360 ]
359
361
360 def _prepare_participating_query(self, user_id=None, statuses=None,
362 def _prepare_participating_query(self, user_id=None, statuses=None,
361 order_by=None, order_dir='desc'):
363 order_by=None, order_dir='desc'):
362 q = PullRequest.query()
364 q = PullRequest.query()
363 if user_id:
365 if user_id:
364 reviewers_subquery = Session().query(
366 reviewers_subquery = Session().query(
365 PullRequestReviewers.pull_request_id).filter(
367 PullRequestReviewers.pull_request_id).filter(
366 PullRequestReviewers.user_id == user_id).subquery()
368 PullRequestReviewers.user_id == user_id).subquery()
367 user_filter = or_(
369 user_filter = or_(
368 PullRequest.user_id == user_id,
370 PullRequest.user_id == user_id,
369 PullRequest.pull_request_id.in_(reviewers_subquery)
371 PullRequest.pull_request_id.in_(reviewers_subquery)
370 )
372 )
371 q = PullRequest.query().filter(user_filter)
373 q = PullRequest.query().filter(user_filter)
372
374
373 # closed,opened
375 # closed,opened
374 if statuses:
376 if statuses:
375 q = q.filter(PullRequest.status.in_(statuses))
377 q = q.filter(PullRequest.status.in_(statuses))
376
378
377 if order_by:
379 if order_by:
378 order_map = {
380 order_map = {
379 'name_raw': PullRequest.pull_request_id,
381 'name_raw': PullRequest.pull_request_id,
380 'title': PullRequest.title,
382 'title': PullRequest.title,
381 'updated_on_raw': PullRequest.updated_on,
383 'updated_on_raw': PullRequest.updated_on,
382 'target_repo': PullRequest.target_repo_id
384 'target_repo': PullRequest.target_repo_id
383 }
385 }
384 if order_dir == 'asc':
386 if order_dir == 'asc':
385 q = q.order_by(order_map[order_by].asc())
387 q = q.order_by(order_map[order_by].asc())
386 else:
388 else:
387 q = q.order_by(order_map[order_by].desc())
389 q = q.order_by(order_map[order_by].desc())
388
390
389 return q
391 return q
390
392
391 def count_im_participating_in(self, user_id=None, statuses=None):
393 def count_im_participating_in(self, user_id=None, statuses=None):
392 q = self._prepare_participating_query(user_id, statuses=statuses)
394 q = self._prepare_participating_query(user_id, statuses=statuses)
393 return q.count()
395 return q.count()
394
396
395 def get_im_participating_in(
397 def get_im_participating_in(
396 self, user_id=None, statuses=None, offset=0,
398 self, user_id=None, statuses=None, offset=0,
397 length=None, order_by=None, order_dir='desc'):
399 length=None, order_by=None, order_dir='desc'):
398 """
400 """
399 Get all Pull requests that i'm participating in, or i have opened
401 Get all Pull requests that i'm participating in, or i have opened
400 """
402 """
401
403
402 q = self._prepare_participating_query(
404 q = self._prepare_participating_query(
403 user_id, statuses=statuses, order_by=order_by,
405 user_id, statuses=statuses, order_by=order_by,
404 order_dir=order_dir)
406 order_dir=order_dir)
405
407
406 if length:
408 if length:
407 pull_requests = q.limit(length).offset(offset).all()
409 pull_requests = q.limit(length).offset(offset).all()
408 else:
410 else:
409 pull_requests = q.all()
411 pull_requests = q.all()
410
412
411 return pull_requests
413 return pull_requests
412
414
413 def get_versions(self, pull_request):
415 def get_versions(self, pull_request):
414 """
416 """
415 returns version of pull request sorted by ID descending
417 returns version of pull request sorted by ID descending
416 """
418 """
417 return PullRequestVersion.query()\
419 return PullRequestVersion.query()\
418 .filter(PullRequestVersion.pull_request == pull_request)\
420 .filter(PullRequestVersion.pull_request == pull_request)\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
421 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 .all()
422 .all()
421
423
422 def get_pr_version(self, pull_request_id, version=None):
424 def get_pr_version(self, pull_request_id, version=None):
423 at_version = None
425 at_version = None
424
426
425 if version and version == 'latest':
427 if version and version == 'latest':
426 pull_request_ver = PullRequest.get(pull_request_id)
428 pull_request_ver = PullRequest.get(pull_request_id)
427 pull_request_obj = pull_request_ver
429 pull_request_obj = pull_request_ver
428 _org_pull_request_obj = pull_request_obj
430 _org_pull_request_obj = pull_request_obj
429 at_version = 'latest'
431 at_version = 'latest'
430 elif version:
432 elif version:
431 pull_request_ver = PullRequestVersion.get_or_404(version)
433 pull_request_ver = PullRequestVersion.get_or_404(version)
432 pull_request_obj = pull_request_ver
434 pull_request_obj = pull_request_ver
433 _org_pull_request_obj = pull_request_ver.pull_request
435 _org_pull_request_obj = pull_request_ver.pull_request
434 at_version = pull_request_ver.pull_request_version_id
436 at_version = pull_request_ver.pull_request_version_id
435 else:
437 else:
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
438 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 pull_request_id)
439 pull_request_id)
438
440
439 pull_request_display_obj = PullRequest.get_pr_display_object(
441 pull_request_display_obj = PullRequest.get_pr_display_object(
440 pull_request_obj, _org_pull_request_obj)
442 pull_request_obj, _org_pull_request_obj)
441
443
442 return _org_pull_request_obj, pull_request_obj, \
444 return _org_pull_request_obj, pull_request_obj, \
443 pull_request_display_obj, at_version
445 pull_request_display_obj, at_version
444
446
445 def create(self, created_by, source_repo, source_ref, target_repo,
447 def create(self, created_by, source_repo, source_ref, target_repo,
446 target_ref, revisions, reviewers, title, description=None,
448 target_ref, revisions, reviewers, title, description=None,
447 description_renderer=None,
449 description_renderer=None,
448 reviewer_data=None, translator=None, auth_user=None):
450 reviewer_data=None, translator=None, auth_user=None):
449 translator = translator or get_current_request().translate
451 translator = translator or get_current_request().translate
450
452
451 created_by_user = self._get_user(created_by)
453 created_by_user = self._get_user(created_by)
452 auth_user = auth_user or created_by_user.AuthUser()
454 auth_user = auth_user or created_by_user.AuthUser()
453 source_repo = self._get_repo(source_repo)
455 source_repo = self._get_repo(source_repo)
454 target_repo = self._get_repo(target_repo)
456 target_repo = self._get_repo(target_repo)
455
457
456 pull_request = PullRequest()
458 pull_request = PullRequest()
457 pull_request.source_repo = source_repo
459 pull_request.source_repo = source_repo
458 pull_request.source_ref = source_ref
460 pull_request.source_ref = source_ref
459 pull_request.target_repo = target_repo
461 pull_request.target_repo = target_repo
460 pull_request.target_ref = target_ref
462 pull_request.target_ref = target_ref
461 pull_request.revisions = revisions
463 pull_request.revisions = revisions
462 pull_request.title = title
464 pull_request.title = title
463 pull_request.description = description
465 pull_request.description = description
464 pull_request.description_renderer = description_renderer
466 pull_request.description_renderer = description_renderer
465 pull_request.author = created_by_user
467 pull_request.author = created_by_user
466 pull_request.reviewer_data = reviewer_data
468 pull_request.reviewer_data = reviewer_data
467
469
468 Session().add(pull_request)
470 Session().add(pull_request)
469 Session().flush()
471 Session().flush()
470
472
471 reviewer_ids = set()
473 reviewer_ids = set()
472 # members / reviewers
474 # members / reviewers
473 for reviewer_object in reviewers:
475 for reviewer_object in reviewers:
474 user_id, reasons, mandatory, rules = reviewer_object
476 user_id, reasons, mandatory, rules = reviewer_object
475 user = self._get_user(user_id)
477 user = self._get_user(user_id)
476
478
477 # skip duplicates
479 # skip duplicates
478 if user.user_id in reviewer_ids:
480 if user.user_id in reviewer_ids:
479 continue
481 continue
480
482
481 reviewer_ids.add(user.user_id)
483 reviewer_ids.add(user.user_id)
482
484
483 reviewer = PullRequestReviewers()
485 reviewer = PullRequestReviewers()
484 reviewer.user = user
486 reviewer.user = user
485 reviewer.pull_request = pull_request
487 reviewer.pull_request = pull_request
486 reviewer.reasons = reasons
488 reviewer.reasons = reasons
487 reviewer.mandatory = mandatory
489 reviewer.mandatory = mandatory
488
490
489 # NOTE(marcink): pick only first rule for now
491 # NOTE(marcink): pick only first rule for now
490 rule_id = list(rules)[0] if rules else None
492 rule_id = list(rules)[0] if rules else None
491 rule = RepoReviewRule.get(rule_id) if rule_id else None
493 rule = RepoReviewRule.get(rule_id) if rule_id else None
492 if rule:
494 if rule:
493 review_group = rule.user_group_vote_rule(user_id)
495 review_group = rule.user_group_vote_rule(user_id)
494 # we check if this particular reviewer is member of a voting group
496 # we check if this particular reviewer is member of a voting group
495 if review_group:
497 if review_group:
496 # NOTE(marcink):
498 # NOTE(marcink):
497 # can be that user is member of more but we pick the first same,
499 # can be that user is member of more but we pick the first same,
498 # same as default reviewers algo
500 # same as default reviewers algo
499 review_group = review_group[0]
501 review_group = review_group[0]
500
502
501 rule_data = {
503 rule_data = {
502 'rule_name':
504 'rule_name':
503 rule.review_rule_name,
505 rule.review_rule_name,
504 'rule_user_group_entry_id':
506 'rule_user_group_entry_id':
505 review_group.repo_review_rule_users_group_id,
507 review_group.repo_review_rule_users_group_id,
506 'rule_user_group_name':
508 'rule_user_group_name':
507 review_group.users_group.users_group_name,
509 review_group.users_group.users_group_name,
508 'rule_user_group_members':
510 'rule_user_group_members':
509 [x.user.username for x in review_group.users_group.members],
511 [x.user.username for x in review_group.users_group.members],
510 'rule_user_group_members_id':
512 'rule_user_group_members_id':
511 [x.user.user_id for x in review_group.users_group.members],
513 [x.user.user_id for x in review_group.users_group.members],
512 }
514 }
513 # e.g {'vote_rule': -1, 'mandatory': True}
515 # e.g {'vote_rule': -1, 'mandatory': True}
514 rule_data.update(review_group.rule_data())
516 rule_data.update(review_group.rule_data())
515
517
516 reviewer.rule_data = rule_data
518 reviewer.rule_data = rule_data
517
519
518 Session().add(reviewer)
520 Session().add(reviewer)
519 Session().flush()
521 Session().flush()
520
522
521 # Set approval status to "Under Review" for all commits which are
523 # Set approval status to "Under Review" for all commits which are
522 # part of this pull request.
524 # part of this pull request.
523 ChangesetStatusModel().set_status(
525 ChangesetStatusModel().set_status(
524 repo=target_repo,
526 repo=target_repo,
525 status=ChangesetStatus.STATUS_UNDER_REVIEW,
527 status=ChangesetStatus.STATUS_UNDER_REVIEW,
526 user=created_by_user,
528 user=created_by_user,
527 pull_request=pull_request
529 pull_request=pull_request
528 )
530 )
529 # we commit early at this point. This has to do with a fact
531 # 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
532 # that before queries do some row-locking. And because of that
531 # we need to commit and finish transation before below validate call
533 # we need to commit and finish transation before below validate call
532 # that for large repos could be long resulting in long row locks
534 # that for large repos could be long resulting in long row locks
533 Session().commit()
535 Session().commit()
534
536
535 # prepare workspace, and run initial merge simulation
537 # prepare workspace, and run initial merge simulation
536 MergeCheck.validate(
538 MergeCheck.validate(
537 pull_request, auth_user=auth_user, translator=translator)
539 pull_request, auth_user=auth_user, translator=translator)
538
540
539 self.notify_reviewers(pull_request, reviewer_ids)
541 self.notify_reviewers(pull_request, reviewer_ids)
540 self._trigger_pull_request_hook(
542 self._trigger_pull_request_hook(
541 pull_request, created_by_user, 'create')
543 pull_request, created_by_user, 'create')
542
544
543 creation_data = pull_request.get_api_data(with_merge_state=False)
545 creation_data = pull_request.get_api_data(with_merge_state=False)
544 self._log_audit_action(
546 self._log_audit_action(
545 'repo.pull_request.create', {'data': creation_data},
547 'repo.pull_request.create', {'data': creation_data},
546 auth_user, pull_request)
548 auth_user, pull_request)
547
549
548 return pull_request
550 return pull_request
549
551
550 def _trigger_pull_request_hook(self, pull_request, user, action):
552 def _trigger_pull_request_hook(self, pull_request, user, action):
551 pull_request = self.__get_pull_request(pull_request)
553 pull_request = self.__get_pull_request(pull_request)
552 target_scm = pull_request.target_repo.scm_instance()
554 target_scm = pull_request.target_repo.scm_instance()
553 if action == 'create':
555 if action == 'create':
554 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
556 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
555 elif action == 'merge':
557 elif action == 'merge':
556 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
558 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
557 elif action == 'close':
559 elif action == 'close':
558 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
560 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
559 elif action == 'review_status_change':
561 elif action == 'review_status_change':
560 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
562 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
561 elif action == 'update':
563 elif action == 'update':
562 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
564 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
563 else:
565 else:
564 return
566 return
565
567
566 trigger_hook(
568 trigger_hook(
567 username=user.username,
569 username=user.username,
568 repo_name=pull_request.target_repo.repo_name,
570 repo_name=pull_request.target_repo.repo_name,
569 repo_alias=target_scm.alias,
571 repo_alias=target_scm.alias,
570 pull_request=pull_request)
572 pull_request=pull_request)
571
573
572 def _get_commit_ids(self, pull_request):
574 def _get_commit_ids(self, pull_request):
573 """
575 """
574 Return the commit ids of the merged pull request.
576 Return the commit ids of the merged pull request.
575
577
576 This method is not dealing correctly yet with the lack of autoupdates
578 This method is not dealing correctly yet with the lack of autoupdates
577 nor with the implicit target updates.
579 nor with the implicit target updates.
578 For example: if a commit in the source repo is already in the target it
580 For example: if a commit in the source repo is already in the target it
579 will be reported anyways.
581 will be reported anyways.
580 """
582 """
581 merge_rev = pull_request.merge_rev
583 merge_rev = pull_request.merge_rev
582 if merge_rev is None:
584 if merge_rev is None:
583 raise ValueError('This pull request was not merged yet')
585 raise ValueError('This pull request was not merged yet')
584
586
585 commit_ids = list(pull_request.revisions)
587 commit_ids = list(pull_request.revisions)
586 if merge_rev not in commit_ids:
588 if merge_rev not in commit_ids:
587 commit_ids.append(merge_rev)
589 commit_ids.append(merge_rev)
588
590
589 return commit_ids
591 return commit_ids
590
592
591 def merge_repo(self, pull_request, user, extras):
593 def merge_repo(self, pull_request, user, extras):
592 log.debug("Merging pull request %s", pull_request.pull_request_id)
594 log.debug("Merging pull request %s", pull_request.pull_request_id)
593 extras['user_agent'] = 'internal-merge'
595 extras['user_agent'] = 'internal-merge'
594 merge_state = self._merge_pull_request(pull_request, user, extras)
596 merge_state = self._merge_pull_request(pull_request, user, extras)
595 if merge_state.executed:
597 if merge_state.executed:
596 log.debug(
598 log.debug(
597 "Merge was successful, updating the pull request comments.")
599 "Merge was successful, updating the pull request comments.")
598 self._comment_and_close_pr(pull_request, user, merge_state)
600 self._comment_and_close_pr(pull_request, user, merge_state)
599
601
600 self._log_audit_action(
602 self._log_audit_action(
601 'repo.pull_request.merge',
603 'repo.pull_request.merge',
602 {'merge_state': merge_state.__dict__},
604 {'merge_state': merge_state.__dict__},
603 user, pull_request)
605 user, pull_request)
604
606
605 else:
607 else:
606 log.warn("Merge failed, not updating the pull request.")
608 log.warn("Merge failed, not updating the pull request.")
607 return merge_state
609 return merge_state
608
610
609 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
611 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
610 target_vcs = pull_request.target_repo.scm_instance()
612 target_vcs = pull_request.target_repo.scm_instance()
611 source_vcs = pull_request.source_repo.scm_instance()
613 source_vcs = pull_request.source_repo.scm_instance()
612
614
613 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
615 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
614 pr_id=pull_request.pull_request_id,
616 pr_id=pull_request.pull_request_id,
615 pr_title=pull_request.title,
617 pr_title=pull_request.title,
616 source_repo=source_vcs.name,
618 source_repo=source_vcs.name,
617 source_ref_name=pull_request.source_ref_parts.name,
619 source_ref_name=pull_request.source_ref_parts.name,
618 target_repo=target_vcs.name,
620 target_repo=target_vcs.name,
619 target_ref_name=pull_request.target_ref_parts.name,
621 target_ref_name=pull_request.target_ref_parts.name,
620 )
622 )
621
623
622 workspace_id = self._workspace_id(pull_request)
624 workspace_id = self._workspace_id(pull_request)
623 repo_id = pull_request.target_repo.repo_id
625 repo_id = pull_request.target_repo.repo_id
624 use_rebase = self._use_rebase_for_merging(pull_request)
626 use_rebase = self._use_rebase_for_merging(pull_request)
625 close_branch = self._close_branch_before_merging(pull_request)
627 close_branch = self._close_branch_before_merging(pull_request)
626
628
627 target_ref = self._refresh_reference(
629 target_ref = self._refresh_reference(
628 pull_request.target_ref_parts, target_vcs)
630 pull_request.target_ref_parts, target_vcs)
629
631
630 callback_daemon, extras = prepare_callback_daemon(
632 callback_daemon, extras = prepare_callback_daemon(
631 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
633 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
632 host=vcs_settings.HOOKS_HOST,
634 host=vcs_settings.HOOKS_HOST,
633 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
635 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
634
636
635 with callback_daemon:
637 with callback_daemon:
636 # TODO: johbo: Implement a clean way to run a config_override
638 # TODO: johbo: Implement a clean way to run a config_override
637 # for a single call.
639 # for a single call.
638 target_vcs.config.set(
640 target_vcs.config.set(
639 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
641 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
640
642
641 user_name = user.short_contact
643 user_name = user.short_contact
642 merge_state = target_vcs.merge(
644 merge_state = target_vcs.merge(
643 repo_id, workspace_id, target_ref, source_vcs,
645 repo_id, workspace_id, target_ref, source_vcs,
644 pull_request.source_ref_parts,
646 pull_request.source_ref_parts,
645 user_name=user_name, user_email=user.email,
647 user_name=user_name, user_email=user.email,
646 message=message, use_rebase=use_rebase,
648 message=message, use_rebase=use_rebase,
647 close_branch=close_branch)
649 close_branch=close_branch)
648 return merge_state
650 return merge_state
649
651
650 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
652 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
651 pull_request.merge_rev = merge_state.merge_ref.commit_id
653 pull_request.merge_rev = merge_state.merge_ref.commit_id
652 pull_request.updated_on = datetime.datetime.now()
654 pull_request.updated_on = datetime.datetime.now()
653 close_msg = close_msg or 'Pull request merged and closed'
655 close_msg = close_msg or 'Pull request merged and closed'
654
656
655 CommentsModel().create(
657 CommentsModel().create(
656 text=safe_unicode(close_msg),
658 text=safe_unicode(close_msg),
657 repo=pull_request.target_repo.repo_id,
659 repo=pull_request.target_repo.repo_id,
658 user=user.user_id,
660 user=user.user_id,
659 pull_request=pull_request.pull_request_id,
661 pull_request=pull_request.pull_request_id,
660 f_path=None,
662 f_path=None,
661 line_no=None,
663 line_no=None,
662 closing_pr=True
664 closing_pr=True
663 )
665 )
664
666
665 Session().add(pull_request)
667 Session().add(pull_request)
666 Session().flush()
668 Session().flush()
667 # TODO: paris: replace invalidation with less radical solution
669 # TODO: paris: replace invalidation with less radical solution
668 ScmModel().mark_for_invalidation(
670 ScmModel().mark_for_invalidation(
669 pull_request.target_repo.repo_name)
671 pull_request.target_repo.repo_name)
670 self._trigger_pull_request_hook(pull_request, user, 'merge')
672 self._trigger_pull_request_hook(pull_request, user, 'merge')
671
673
672 def has_valid_update_type(self, pull_request):
674 def has_valid_update_type(self, pull_request):
673 source_ref_type = pull_request.source_ref_parts.type
675 source_ref_type = pull_request.source_ref_parts.type
674 return source_ref_type in ['book', 'branch', 'tag']
676 return source_ref_type in self.REF_TYPES
675
677
676 def update_commits(self, pull_request):
678 def update_commits(self, pull_request):
677 """
679 """
678 Get the updated list of commits for the pull request
680 Get the updated list of commits for the pull request
679 and return the new pull request version and the list
681 and return the new pull request version and the list
680 of commits processed by this update action
682 of commits processed by this update action
681 """
683 """
682 pull_request = self.__get_pull_request(pull_request)
684 pull_request = self.__get_pull_request(pull_request)
683 source_ref_type = pull_request.source_ref_parts.type
685 source_ref_type = pull_request.source_ref_parts.type
684 source_ref_name = pull_request.source_ref_parts.name
686 source_ref_name = pull_request.source_ref_parts.name
685 source_ref_id = pull_request.source_ref_parts.commit_id
687 source_ref_id = pull_request.source_ref_parts.commit_id
686
688
687 target_ref_type = pull_request.target_ref_parts.type
689 target_ref_type = pull_request.target_ref_parts.type
688 target_ref_name = pull_request.target_ref_parts.name
690 target_ref_name = pull_request.target_ref_parts.name
689 target_ref_id = pull_request.target_ref_parts.commit_id
691 target_ref_id = pull_request.target_ref_parts.commit_id
690
692
691 if not self.has_valid_update_type(pull_request):
693 if not self.has_valid_update_type(pull_request):
692 log.debug(
694 log.debug(
693 "Skipping update of pull request %s due to ref type: %s",
695 "Skipping update of pull request %s due to ref type: %s",
694 pull_request, source_ref_type)
696 pull_request, source_ref_type)
695 return UpdateResponse(
697 return UpdateResponse(
696 executed=False,
698 executed=False,
697 reason=UpdateFailureReason.WRONG_REF_TYPE,
699 reason=UpdateFailureReason.WRONG_REF_TYPE,
698 old=pull_request, new=None, changes=None,
700 old=pull_request, new=None, changes=None,
699 source_changed=False, target_changed=False)
701 source_changed=False, target_changed=False)
700
702
701 # source repo
703 # source repo
702 source_repo = pull_request.source_repo.scm_instance()
704 source_repo = pull_request.source_repo.scm_instance()
703 try:
705 try:
704 source_commit = source_repo.get_commit(commit_id=source_ref_name)
706 source_commit = source_repo.get_commit(commit_id=source_ref_name)
705 except CommitDoesNotExistError:
707 except CommitDoesNotExistError:
706 return UpdateResponse(
708 return UpdateResponse(
707 executed=False,
709 executed=False,
708 reason=UpdateFailureReason.MISSING_SOURCE_REF,
710 reason=UpdateFailureReason.MISSING_SOURCE_REF,
709 old=pull_request, new=None, changes=None,
711 old=pull_request, new=None, changes=None,
710 source_changed=False, target_changed=False)
712 source_changed=False, target_changed=False)
711
713
712 source_changed = source_ref_id != source_commit.raw_id
714 source_changed = source_ref_id != source_commit.raw_id
713
715
714 # target repo
716 # target repo
715 target_repo = pull_request.target_repo.scm_instance()
717 target_repo = pull_request.target_repo.scm_instance()
716 try:
718 try:
717 target_commit = target_repo.get_commit(commit_id=target_ref_name)
719 target_commit = target_repo.get_commit(commit_id=target_ref_name)
718 except CommitDoesNotExistError:
720 except CommitDoesNotExistError:
719 return UpdateResponse(
721 return UpdateResponse(
720 executed=False,
722 executed=False,
721 reason=UpdateFailureReason.MISSING_TARGET_REF,
723 reason=UpdateFailureReason.MISSING_TARGET_REF,
722 old=pull_request, new=None, changes=None,
724 old=pull_request, new=None, changes=None,
723 source_changed=False, target_changed=False)
725 source_changed=False, target_changed=False)
724 target_changed = target_ref_id != target_commit.raw_id
726 target_changed = target_ref_id != target_commit.raw_id
725
727
726 if not (source_changed or target_changed):
728 if not (source_changed or target_changed):
727 log.debug("Nothing changed in pull request %s", pull_request)
729 log.debug("Nothing changed in pull request %s", pull_request)
728 return UpdateResponse(
730 return UpdateResponse(
729 executed=False,
731 executed=False,
730 reason=UpdateFailureReason.NO_CHANGE,
732 reason=UpdateFailureReason.NO_CHANGE,
731 old=pull_request, new=None, changes=None,
733 old=pull_request, new=None, changes=None,
732 source_changed=target_changed, target_changed=source_changed)
734 source_changed=target_changed, target_changed=source_changed)
733
735
734 change_in_found = 'target repo' if target_changed else 'source repo'
736 change_in_found = 'target repo' if target_changed else 'source repo'
735 log.debug('Updating pull request because of change in %s detected',
737 log.debug('Updating pull request because of change in %s detected',
736 change_in_found)
738 change_in_found)
737
739
738 # Finally there is a need for an update, in case of source change
740 # Finally there is a need for an update, in case of source change
739 # we create a new version, else just an update
741 # we create a new version, else just an update
740 if source_changed:
742 if source_changed:
741 pull_request_version = self._create_version_from_snapshot(pull_request)
743 pull_request_version = self._create_version_from_snapshot(pull_request)
742 self._link_comments_to_version(pull_request_version)
744 self._link_comments_to_version(pull_request_version)
743 else:
745 else:
744 try:
746 try:
745 ver = pull_request.versions[-1]
747 ver = pull_request.versions[-1]
746 except IndexError:
748 except IndexError:
747 ver = None
749 ver = None
748
750
749 pull_request.pull_request_version_id = \
751 pull_request.pull_request_version_id = \
750 ver.pull_request_version_id if ver else None
752 ver.pull_request_version_id if ver else None
751 pull_request_version = pull_request
753 pull_request_version = pull_request
752
754
753 try:
755 try:
754 if target_ref_type in ('tag', 'branch', 'book'):
756 if target_ref_type in self.REF_TYPES:
755 target_commit = target_repo.get_commit(target_ref_name)
757 target_commit = target_repo.get_commit(target_ref_name)
756 else:
758 else:
757 target_commit = target_repo.get_commit(target_ref_id)
759 target_commit = target_repo.get_commit(target_ref_id)
758 except CommitDoesNotExistError:
760 except CommitDoesNotExistError:
759 return UpdateResponse(
761 return UpdateResponse(
760 executed=False,
762 executed=False,
761 reason=UpdateFailureReason.MISSING_TARGET_REF,
763 reason=UpdateFailureReason.MISSING_TARGET_REF,
762 old=pull_request, new=None, changes=None,
764 old=pull_request, new=None, changes=None,
763 source_changed=source_changed, target_changed=target_changed)
765 source_changed=source_changed, target_changed=target_changed)
764
766
765 # re-compute commit ids
767 # re-compute commit ids
766 old_commit_ids = pull_request.revisions
768 old_commit_ids = pull_request.revisions
767 pre_load = ["author", "branch", "date", "message"]
769 pre_load = ["author", "branch", "date", "message"]
768 commit_ranges = target_repo.compare(
770 commit_ranges = target_repo.compare(
769 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
771 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
770 pre_load=pre_load)
772 pre_load=pre_load)
771
773
772 ancestor = target_repo.get_common_ancestor(
774 ancestor = target_repo.get_common_ancestor(
773 target_commit.raw_id, source_commit.raw_id, source_repo)
775 target_commit.raw_id, source_commit.raw_id, source_repo)
774
776
775 pull_request.source_ref = '%s:%s:%s' % (
777 pull_request.source_ref = '%s:%s:%s' % (
776 source_ref_type, source_ref_name, source_commit.raw_id)
778 source_ref_type, source_ref_name, source_commit.raw_id)
777 pull_request.target_ref = '%s:%s:%s' % (
779 pull_request.target_ref = '%s:%s:%s' % (
778 target_ref_type, target_ref_name, ancestor)
780 target_ref_type, target_ref_name, ancestor)
779
781
780 pull_request.revisions = [
782 pull_request.revisions = [
781 commit.raw_id for commit in reversed(commit_ranges)]
783 commit.raw_id for commit in reversed(commit_ranges)]
782 pull_request.updated_on = datetime.datetime.now()
784 pull_request.updated_on = datetime.datetime.now()
783 Session().add(pull_request)
785 Session().add(pull_request)
784 new_commit_ids = pull_request.revisions
786 new_commit_ids = pull_request.revisions
785
787
786 old_diff_data, new_diff_data = self._generate_update_diffs(
788 old_diff_data, new_diff_data = self._generate_update_diffs(
787 pull_request, pull_request_version)
789 pull_request, pull_request_version)
788
790
789 # calculate commit and file changes
791 # calculate commit and file changes
790 changes = self._calculate_commit_id_changes(
792 changes = self._calculate_commit_id_changes(
791 old_commit_ids, new_commit_ids)
793 old_commit_ids, new_commit_ids)
792 file_changes = self._calculate_file_changes(
794 file_changes = self._calculate_file_changes(
793 old_diff_data, new_diff_data)
795 old_diff_data, new_diff_data)
794
796
795 # set comments as outdated if DIFFS changed
797 # set comments as outdated if DIFFS changed
796 CommentsModel().outdate_comments(
798 CommentsModel().outdate_comments(
797 pull_request, old_diff_data=old_diff_data,
799 pull_request, old_diff_data=old_diff_data,
798 new_diff_data=new_diff_data)
800 new_diff_data=new_diff_data)
799
801
800 commit_changes = (changes.added or changes.removed)
802 commit_changes = (changes.added or changes.removed)
801 file_node_changes = (
803 file_node_changes = (
802 file_changes.added or file_changes.modified or file_changes.removed)
804 file_changes.added or file_changes.modified or file_changes.removed)
803 pr_has_changes = commit_changes or file_node_changes
805 pr_has_changes = commit_changes or file_node_changes
804
806
805 # Add an automatic comment to the pull request, in case
807 # Add an automatic comment to the pull request, in case
806 # anything has changed
808 # anything has changed
807 if pr_has_changes:
809 if pr_has_changes:
808 update_comment = CommentsModel().create(
810 update_comment = CommentsModel().create(
809 text=self._render_update_message(changes, file_changes),
811 text=self._render_update_message(changes, file_changes),
810 repo=pull_request.target_repo,
812 repo=pull_request.target_repo,
811 user=pull_request.author,
813 user=pull_request.author,
812 pull_request=pull_request,
814 pull_request=pull_request,
813 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
815 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
814
816
815 # Update status to "Under Review" for added commits
817 # Update status to "Under Review" for added commits
816 for commit_id in changes.added:
818 for commit_id in changes.added:
817 ChangesetStatusModel().set_status(
819 ChangesetStatusModel().set_status(
818 repo=pull_request.source_repo,
820 repo=pull_request.source_repo,
819 status=ChangesetStatus.STATUS_UNDER_REVIEW,
821 status=ChangesetStatus.STATUS_UNDER_REVIEW,
820 comment=update_comment,
822 comment=update_comment,
821 user=pull_request.author,
823 user=pull_request.author,
822 pull_request=pull_request,
824 pull_request=pull_request,
823 revision=commit_id)
825 revision=commit_id)
824
826
825 log.debug(
827 log.debug(
826 'Updated pull request %s, added_ids: %s, common_ids: %s, '
828 'Updated pull request %s, added_ids: %s, common_ids: %s, '
827 'removed_ids: %s', pull_request.pull_request_id,
829 'removed_ids: %s', pull_request.pull_request_id,
828 changes.added, changes.common, changes.removed)
830 changes.added, changes.common, changes.removed)
829 log.debug(
831 log.debug(
830 'Updated pull request with the following file changes: %s',
832 'Updated pull request with the following file changes: %s',
831 file_changes)
833 file_changes)
832
834
833 log.info(
835 log.info(
834 "Updated pull request %s from commit %s to commit %s, "
836 "Updated pull request %s from commit %s to commit %s, "
835 "stored new version %s of this pull request.",
837 "stored new version %s of this pull request.",
836 pull_request.pull_request_id, source_ref_id,
838 pull_request.pull_request_id, source_ref_id,
837 pull_request.source_ref_parts.commit_id,
839 pull_request.source_ref_parts.commit_id,
838 pull_request_version.pull_request_version_id)
840 pull_request_version.pull_request_version_id)
839 Session().commit()
841 Session().commit()
840 self._trigger_pull_request_hook(
842 self._trigger_pull_request_hook(
841 pull_request, pull_request.author, 'update')
843 pull_request, pull_request.author, 'update')
842
844
843 return UpdateResponse(
845 return UpdateResponse(
844 executed=True, reason=UpdateFailureReason.NONE,
846 executed=True, reason=UpdateFailureReason.NONE,
845 old=pull_request, new=pull_request_version, changes=changes,
847 old=pull_request, new=pull_request_version, changes=changes,
846 source_changed=source_changed, target_changed=target_changed)
848 source_changed=source_changed, target_changed=target_changed)
847
849
848 def _create_version_from_snapshot(self, pull_request):
850 def _create_version_from_snapshot(self, pull_request):
849 version = PullRequestVersion()
851 version = PullRequestVersion()
850 version.title = pull_request.title
852 version.title = pull_request.title
851 version.description = pull_request.description
853 version.description = pull_request.description
852 version.status = pull_request.status
854 version.status = pull_request.status
853 version.created_on = datetime.datetime.now()
855 version.created_on = datetime.datetime.now()
854 version.updated_on = pull_request.updated_on
856 version.updated_on = pull_request.updated_on
855 version.user_id = pull_request.user_id
857 version.user_id = pull_request.user_id
856 version.source_repo = pull_request.source_repo
858 version.source_repo = pull_request.source_repo
857 version.source_ref = pull_request.source_ref
859 version.source_ref = pull_request.source_ref
858 version.target_repo = pull_request.target_repo
860 version.target_repo = pull_request.target_repo
859 version.target_ref = pull_request.target_ref
861 version.target_ref = pull_request.target_ref
860
862
861 version._last_merge_source_rev = pull_request._last_merge_source_rev
863 version._last_merge_source_rev = pull_request._last_merge_source_rev
862 version._last_merge_target_rev = pull_request._last_merge_target_rev
864 version._last_merge_target_rev = pull_request._last_merge_target_rev
863 version.last_merge_status = pull_request.last_merge_status
865 version.last_merge_status = pull_request.last_merge_status
864 version.shadow_merge_ref = pull_request.shadow_merge_ref
866 version.shadow_merge_ref = pull_request.shadow_merge_ref
865 version.merge_rev = pull_request.merge_rev
867 version.merge_rev = pull_request.merge_rev
866 version.reviewer_data = pull_request.reviewer_data
868 version.reviewer_data = pull_request.reviewer_data
867
869
868 version.revisions = pull_request.revisions
870 version.revisions = pull_request.revisions
869 version.pull_request = pull_request
871 version.pull_request = pull_request
870 Session().add(version)
872 Session().add(version)
871 Session().flush()
873 Session().flush()
872
874
873 return version
875 return version
874
876
875 def _generate_update_diffs(self, pull_request, pull_request_version):
877 def _generate_update_diffs(self, pull_request, pull_request_version):
876
878
877 diff_context = (
879 diff_context = (
878 self.DIFF_CONTEXT +
880 self.DIFF_CONTEXT +
879 CommentsModel.needed_extra_diff_context())
881 CommentsModel.needed_extra_diff_context())
880 hide_whitespace_changes = False
882 hide_whitespace_changes = False
881 source_repo = pull_request_version.source_repo
883 source_repo = pull_request_version.source_repo
882 source_ref_id = pull_request_version.source_ref_parts.commit_id
884 source_ref_id = pull_request_version.source_ref_parts.commit_id
883 target_ref_id = pull_request_version.target_ref_parts.commit_id
885 target_ref_id = pull_request_version.target_ref_parts.commit_id
884 old_diff = self._get_diff_from_pr_or_version(
886 old_diff = self._get_diff_from_pr_or_version(
885 source_repo, source_ref_id, target_ref_id,
887 source_repo, source_ref_id, target_ref_id,
886 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
888 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
887
889
888 source_repo = pull_request.source_repo
890 source_repo = pull_request.source_repo
889 source_ref_id = pull_request.source_ref_parts.commit_id
891 source_ref_id = pull_request.source_ref_parts.commit_id
890 target_ref_id = pull_request.target_ref_parts.commit_id
892 target_ref_id = pull_request.target_ref_parts.commit_id
891
893
892 new_diff = self._get_diff_from_pr_or_version(
894 new_diff = self._get_diff_from_pr_or_version(
893 source_repo, source_ref_id, target_ref_id,
895 source_repo, source_ref_id, target_ref_id,
894 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
896 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
895
897
896 old_diff_data = diffs.DiffProcessor(old_diff)
898 old_diff_data = diffs.DiffProcessor(old_diff)
897 old_diff_data.prepare()
899 old_diff_data.prepare()
898 new_diff_data = diffs.DiffProcessor(new_diff)
900 new_diff_data = diffs.DiffProcessor(new_diff)
899 new_diff_data.prepare()
901 new_diff_data.prepare()
900
902
901 return old_diff_data, new_diff_data
903 return old_diff_data, new_diff_data
902
904
903 def _link_comments_to_version(self, pull_request_version):
905 def _link_comments_to_version(self, pull_request_version):
904 """
906 """
905 Link all unlinked comments of this pull request to the given version.
907 Link all unlinked comments of this pull request to the given version.
906
908
907 :param pull_request_version: The `PullRequestVersion` to which
909 :param pull_request_version: The `PullRequestVersion` to which
908 the comments shall be linked.
910 the comments shall be linked.
909
911
910 """
912 """
911 pull_request = pull_request_version.pull_request
913 pull_request = pull_request_version.pull_request
912 comments = ChangesetComment.query()\
914 comments = ChangesetComment.query()\
913 .filter(
915 .filter(
914 # TODO: johbo: Should we query for the repo at all here?
916 # TODO: johbo: Should we query for the repo at all here?
915 # Pending decision on how comments of PRs are to be related
917 # Pending decision on how comments of PRs are to be related
916 # to either the source repo, the target repo or no repo at all.
918 # to either the source repo, the target repo or no repo at all.
917 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
919 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
918 ChangesetComment.pull_request == pull_request,
920 ChangesetComment.pull_request == pull_request,
919 ChangesetComment.pull_request_version == None)\
921 ChangesetComment.pull_request_version == None)\
920 .order_by(ChangesetComment.comment_id.asc())
922 .order_by(ChangesetComment.comment_id.asc())
921
923
922 # TODO: johbo: Find out why this breaks if it is done in a bulk
924 # TODO: johbo: Find out why this breaks if it is done in a bulk
923 # operation.
925 # operation.
924 for comment in comments:
926 for comment in comments:
925 comment.pull_request_version_id = (
927 comment.pull_request_version_id = (
926 pull_request_version.pull_request_version_id)
928 pull_request_version.pull_request_version_id)
927 Session().add(comment)
929 Session().add(comment)
928
930
929 def _calculate_commit_id_changes(self, old_ids, new_ids):
931 def _calculate_commit_id_changes(self, old_ids, new_ids):
930 added = [x for x in new_ids if x not in old_ids]
932 added = [x for x in new_ids if x not in old_ids]
931 common = [x for x in new_ids if x in old_ids]
933 common = [x for x in new_ids if x in old_ids]
932 removed = [x for x in old_ids if x not in new_ids]
934 removed = [x for x in old_ids if x not in new_ids]
933 total = new_ids
935 total = new_ids
934 return ChangeTuple(added, common, removed, total)
936 return ChangeTuple(added, common, removed, total)
935
937
936 def _calculate_file_changes(self, old_diff_data, new_diff_data):
938 def _calculate_file_changes(self, old_diff_data, new_diff_data):
937
939
938 old_files = OrderedDict()
940 old_files = OrderedDict()
939 for diff_data in old_diff_data.parsed_diff:
941 for diff_data in old_diff_data.parsed_diff:
940 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
942 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
941
943
942 added_files = []
944 added_files = []
943 modified_files = []
945 modified_files = []
944 removed_files = []
946 removed_files = []
945 for diff_data in new_diff_data.parsed_diff:
947 for diff_data in new_diff_data.parsed_diff:
946 new_filename = diff_data['filename']
948 new_filename = diff_data['filename']
947 new_hash = md5_safe(diff_data['raw_diff'])
949 new_hash = md5_safe(diff_data['raw_diff'])
948
950
949 old_hash = old_files.get(new_filename)
951 old_hash = old_files.get(new_filename)
950 if not old_hash:
952 if not old_hash:
951 # file is not present in old diff, means it's added
953 # file is not present in old diff, means it's added
952 added_files.append(new_filename)
954 added_files.append(new_filename)
953 else:
955 else:
954 if new_hash != old_hash:
956 if new_hash != old_hash:
955 modified_files.append(new_filename)
957 modified_files.append(new_filename)
956 # now remove a file from old, since we have seen it already
958 # now remove a file from old, since we have seen it already
957 del old_files[new_filename]
959 del old_files[new_filename]
958
960
959 # removed files is when there are present in old, but not in NEW,
961 # removed files is when there are present in old, but not in NEW,
960 # since we remove old files that are present in new diff, left-overs
962 # since we remove old files that are present in new diff, left-overs
961 # if any should be the removed files
963 # if any should be the removed files
962 removed_files.extend(old_files.keys())
964 removed_files.extend(old_files.keys())
963
965
964 return FileChangeTuple(added_files, modified_files, removed_files)
966 return FileChangeTuple(added_files, modified_files, removed_files)
965
967
966 def _render_update_message(self, changes, file_changes):
968 def _render_update_message(self, changes, file_changes):
967 """
969 """
968 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
970 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
969 so it's always looking the same disregarding on which default
971 so it's always looking the same disregarding on which default
970 renderer system is using.
972 renderer system is using.
971
973
972 :param changes: changes named tuple
974 :param changes: changes named tuple
973 :param file_changes: file changes named tuple
975 :param file_changes: file changes named tuple
974
976
975 """
977 """
976 new_status = ChangesetStatus.get_status_lbl(
978 new_status = ChangesetStatus.get_status_lbl(
977 ChangesetStatus.STATUS_UNDER_REVIEW)
979 ChangesetStatus.STATUS_UNDER_REVIEW)
978
980
979 changed_files = (
981 changed_files = (
980 file_changes.added + file_changes.modified + file_changes.removed)
982 file_changes.added + file_changes.modified + file_changes.removed)
981
983
982 params = {
984 params = {
983 'under_review_label': new_status,
985 'under_review_label': new_status,
984 'added_commits': changes.added,
986 'added_commits': changes.added,
985 'removed_commits': changes.removed,
987 'removed_commits': changes.removed,
986 'changed_files': changed_files,
988 'changed_files': changed_files,
987 'added_files': file_changes.added,
989 'added_files': file_changes.added,
988 'modified_files': file_changes.modified,
990 'modified_files': file_changes.modified,
989 'removed_files': file_changes.removed,
991 'removed_files': file_changes.removed,
990 }
992 }
991 renderer = RstTemplateRenderer()
993 renderer = RstTemplateRenderer()
992 return renderer.render('pull_request_update.mako', **params)
994 return renderer.render('pull_request_update.mako', **params)
993
995
994 def edit(self, pull_request, title, description, description_renderer, user):
996 def edit(self, pull_request, title, description, description_renderer, user):
995 pull_request = self.__get_pull_request(pull_request)
997 pull_request = self.__get_pull_request(pull_request)
996 old_data = pull_request.get_api_data(with_merge_state=False)
998 old_data = pull_request.get_api_data(with_merge_state=False)
997 if pull_request.is_closed():
999 if pull_request.is_closed():
998 raise ValueError('This pull request is closed')
1000 raise ValueError('This pull request is closed')
999 if title:
1001 if title:
1000 pull_request.title = title
1002 pull_request.title = title
1001 pull_request.description = description
1003 pull_request.description = description
1002 pull_request.updated_on = datetime.datetime.now()
1004 pull_request.updated_on = datetime.datetime.now()
1003 pull_request.description_renderer = description_renderer
1005 pull_request.description_renderer = description_renderer
1004 Session().add(pull_request)
1006 Session().add(pull_request)
1005 self._log_audit_action(
1007 self._log_audit_action(
1006 'repo.pull_request.edit', {'old_data': old_data},
1008 'repo.pull_request.edit', {'old_data': old_data},
1007 user, pull_request)
1009 user, pull_request)
1008
1010
1009 def update_reviewers(self, pull_request, reviewer_data, user):
1011 def update_reviewers(self, pull_request, reviewer_data, user):
1010 """
1012 """
1011 Update the reviewers in the pull request
1013 Update the reviewers in the pull request
1012
1014
1013 :param pull_request: the pr to update
1015 :param pull_request: the pr to update
1014 :param reviewer_data: list of tuples
1016 :param reviewer_data: list of tuples
1015 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1017 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1016 """
1018 """
1017 pull_request = self.__get_pull_request(pull_request)
1019 pull_request = self.__get_pull_request(pull_request)
1018 if pull_request.is_closed():
1020 if pull_request.is_closed():
1019 raise ValueError('This pull request is closed')
1021 raise ValueError('This pull request is closed')
1020
1022
1021 reviewers = {}
1023 reviewers = {}
1022 for user_id, reasons, mandatory, rules in reviewer_data:
1024 for user_id, reasons, mandatory, rules in reviewer_data:
1023 if isinstance(user_id, (int, basestring)):
1025 if isinstance(user_id, (int, basestring)):
1024 user_id = self._get_user(user_id).user_id
1026 user_id = self._get_user(user_id).user_id
1025 reviewers[user_id] = {
1027 reviewers[user_id] = {
1026 'reasons': reasons, 'mandatory': mandatory}
1028 'reasons': reasons, 'mandatory': mandatory}
1027
1029
1028 reviewers_ids = set(reviewers.keys())
1030 reviewers_ids = set(reviewers.keys())
1029 current_reviewers = PullRequestReviewers.query()\
1031 current_reviewers = PullRequestReviewers.query()\
1030 .filter(PullRequestReviewers.pull_request ==
1032 .filter(PullRequestReviewers.pull_request ==
1031 pull_request).all()
1033 pull_request).all()
1032 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1034 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1033
1035
1034 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1036 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1035 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1037 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1036
1038
1037 log.debug("Adding %s reviewers", ids_to_add)
1039 log.debug("Adding %s reviewers", ids_to_add)
1038 log.debug("Removing %s reviewers", ids_to_remove)
1040 log.debug("Removing %s reviewers", ids_to_remove)
1039 changed = False
1041 changed = False
1040 for uid in ids_to_add:
1042 for uid in ids_to_add:
1041 changed = True
1043 changed = True
1042 _usr = self._get_user(uid)
1044 _usr = self._get_user(uid)
1043 reviewer = PullRequestReviewers()
1045 reviewer = PullRequestReviewers()
1044 reviewer.user = _usr
1046 reviewer.user = _usr
1045 reviewer.pull_request = pull_request
1047 reviewer.pull_request = pull_request
1046 reviewer.reasons = reviewers[uid]['reasons']
1048 reviewer.reasons = reviewers[uid]['reasons']
1047 # NOTE(marcink): mandatory shouldn't be changed now
1049 # NOTE(marcink): mandatory shouldn't be changed now
1048 # reviewer.mandatory = reviewers[uid]['reasons']
1050 # reviewer.mandatory = reviewers[uid]['reasons']
1049 Session().add(reviewer)
1051 Session().add(reviewer)
1050 self._log_audit_action(
1052 self._log_audit_action(
1051 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1053 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1052 user, pull_request)
1054 user, pull_request)
1053
1055
1054 for uid in ids_to_remove:
1056 for uid in ids_to_remove:
1055 changed = True
1057 changed = True
1056 reviewers = PullRequestReviewers.query()\
1058 reviewers = PullRequestReviewers.query()\
1057 .filter(PullRequestReviewers.user_id == uid,
1059 .filter(PullRequestReviewers.user_id == uid,
1058 PullRequestReviewers.pull_request == pull_request)\
1060 PullRequestReviewers.pull_request == pull_request)\
1059 .all()
1061 .all()
1060 # use .all() in case we accidentally added the same person twice
1062 # use .all() in case we accidentally added the same person twice
1061 # this CAN happen due to the lack of DB checks
1063 # this CAN happen due to the lack of DB checks
1062 for obj in reviewers:
1064 for obj in reviewers:
1063 old_data = obj.get_dict()
1065 old_data = obj.get_dict()
1064 Session().delete(obj)
1066 Session().delete(obj)
1065 self._log_audit_action(
1067 self._log_audit_action(
1066 'repo.pull_request.reviewer.delete',
1068 'repo.pull_request.reviewer.delete',
1067 {'old_data': old_data}, user, pull_request)
1069 {'old_data': old_data}, user, pull_request)
1068
1070
1069 if changed:
1071 if changed:
1070 pull_request.updated_on = datetime.datetime.now()
1072 pull_request.updated_on = datetime.datetime.now()
1071 Session().add(pull_request)
1073 Session().add(pull_request)
1072
1074
1073 self.notify_reviewers(pull_request, ids_to_add)
1075 self.notify_reviewers(pull_request, ids_to_add)
1074 return ids_to_add, ids_to_remove
1076 return ids_to_add, ids_to_remove
1075
1077
1076 def get_url(self, pull_request, request=None, permalink=False):
1078 def get_url(self, pull_request, request=None, permalink=False):
1077 if not request:
1079 if not request:
1078 request = get_current_request()
1080 request = get_current_request()
1079
1081
1080 if permalink:
1082 if permalink:
1081 return request.route_url(
1083 return request.route_url(
1082 'pull_requests_global',
1084 'pull_requests_global',
1083 pull_request_id=pull_request.pull_request_id,)
1085 pull_request_id=pull_request.pull_request_id,)
1084 else:
1086 else:
1085 return request.route_url('pullrequest_show',
1087 return request.route_url('pullrequest_show',
1086 repo_name=safe_str(pull_request.target_repo.repo_name),
1088 repo_name=safe_str(pull_request.target_repo.repo_name),
1087 pull_request_id=pull_request.pull_request_id,)
1089 pull_request_id=pull_request.pull_request_id,)
1088
1090
1089 def get_shadow_clone_url(self, pull_request, request=None):
1091 def get_shadow_clone_url(self, pull_request, request=None):
1090 """
1092 """
1091 Returns qualified url pointing to the shadow repository. If this pull
1093 Returns qualified url pointing to the shadow repository. If this pull
1092 request is closed there is no shadow repository and ``None`` will be
1094 request is closed there is no shadow repository and ``None`` will be
1093 returned.
1095 returned.
1094 """
1096 """
1095 if pull_request.is_closed():
1097 if pull_request.is_closed():
1096 return None
1098 return None
1097 else:
1099 else:
1098 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1100 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1099 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1101 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1100
1102
1101 def notify_reviewers(self, pull_request, reviewers_ids):
1103 def notify_reviewers(self, pull_request, reviewers_ids):
1102 # notification to reviewers
1104 # notification to reviewers
1103 if not reviewers_ids:
1105 if not reviewers_ids:
1104 return
1106 return
1105
1107
1106 pull_request_obj = pull_request
1108 pull_request_obj = pull_request
1107 # get the current participants of this pull request
1109 # get the current participants of this pull request
1108 recipients = reviewers_ids
1110 recipients = reviewers_ids
1109 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1111 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1110
1112
1111 pr_source_repo = pull_request_obj.source_repo
1113 pr_source_repo = pull_request_obj.source_repo
1112 pr_target_repo = pull_request_obj.target_repo
1114 pr_target_repo = pull_request_obj.target_repo
1113
1115
1114 pr_url = h.route_url('pullrequest_show',
1116 pr_url = h.route_url('pullrequest_show',
1115 repo_name=pr_target_repo.repo_name,
1117 repo_name=pr_target_repo.repo_name,
1116 pull_request_id=pull_request_obj.pull_request_id,)
1118 pull_request_id=pull_request_obj.pull_request_id,)
1117
1119
1118 # set some variables for email notification
1120 # set some variables for email notification
1119 pr_target_repo_url = h.route_url(
1121 pr_target_repo_url = h.route_url(
1120 'repo_summary', repo_name=pr_target_repo.repo_name)
1122 'repo_summary', repo_name=pr_target_repo.repo_name)
1121
1123
1122 pr_source_repo_url = h.route_url(
1124 pr_source_repo_url = h.route_url(
1123 'repo_summary', repo_name=pr_source_repo.repo_name)
1125 'repo_summary', repo_name=pr_source_repo.repo_name)
1124
1126
1125 # pull request specifics
1127 # pull request specifics
1126 pull_request_commits = [
1128 pull_request_commits = [
1127 (x.raw_id, x.message)
1129 (x.raw_id, x.message)
1128 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1130 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1129
1131
1130 kwargs = {
1132 kwargs = {
1131 'user': pull_request.author,
1133 'user': pull_request.author,
1132 'pull_request': pull_request_obj,
1134 'pull_request': pull_request_obj,
1133 'pull_request_commits': pull_request_commits,
1135 'pull_request_commits': pull_request_commits,
1134
1136
1135 'pull_request_target_repo': pr_target_repo,
1137 'pull_request_target_repo': pr_target_repo,
1136 'pull_request_target_repo_url': pr_target_repo_url,
1138 'pull_request_target_repo_url': pr_target_repo_url,
1137
1139
1138 'pull_request_source_repo': pr_source_repo,
1140 'pull_request_source_repo': pr_source_repo,
1139 'pull_request_source_repo_url': pr_source_repo_url,
1141 'pull_request_source_repo_url': pr_source_repo_url,
1140
1142
1141 'pull_request_url': pr_url,
1143 'pull_request_url': pr_url,
1142 }
1144 }
1143
1145
1144 # pre-generate the subject for notification itself
1146 # pre-generate the subject for notification itself
1145 (subject,
1147 (subject,
1146 _h, _e, # we don't care about those
1148 _h, _e, # we don't care about those
1147 body_plaintext) = EmailNotificationModel().render_email(
1149 body_plaintext) = EmailNotificationModel().render_email(
1148 notification_type, **kwargs)
1150 notification_type, **kwargs)
1149
1151
1150 # create notification objects, and emails
1152 # create notification objects, and emails
1151 NotificationModel().create(
1153 NotificationModel().create(
1152 created_by=pull_request.author,
1154 created_by=pull_request.author,
1153 notification_subject=subject,
1155 notification_subject=subject,
1154 notification_body=body_plaintext,
1156 notification_body=body_plaintext,
1155 notification_type=notification_type,
1157 notification_type=notification_type,
1156 recipients=recipients,
1158 recipients=recipients,
1157 email_kwargs=kwargs,
1159 email_kwargs=kwargs,
1158 )
1160 )
1159
1161
1160 def delete(self, pull_request, user):
1162 def delete(self, pull_request, user):
1161 pull_request = self.__get_pull_request(pull_request)
1163 pull_request = self.__get_pull_request(pull_request)
1162 old_data = pull_request.get_api_data(with_merge_state=False)
1164 old_data = pull_request.get_api_data(with_merge_state=False)
1163 self._cleanup_merge_workspace(pull_request)
1165 self._cleanup_merge_workspace(pull_request)
1164 self._log_audit_action(
1166 self._log_audit_action(
1165 'repo.pull_request.delete', {'old_data': old_data},
1167 'repo.pull_request.delete', {'old_data': old_data},
1166 user, pull_request)
1168 user, pull_request)
1167 Session().delete(pull_request)
1169 Session().delete(pull_request)
1168
1170
1169 def close_pull_request(self, pull_request, user):
1171 def close_pull_request(self, pull_request, user):
1170 pull_request = self.__get_pull_request(pull_request)
1172 pull_request = self.__get_pull_request(pull_request)
1171 self._cleanup_merge_workspace(pull_request)
1173 self._cleanup_merge_workspace(pull_request)
1172 pull_request.status = PullRequest.STATUS_CLOSED
1174 pull_request.status = PullRequest.STATUS_CLOSED
1173 pull_request.updated_on = datetime.datetime.now()
1175 pull_request.updated_on = datetime.datetime.now()
1174 Session().add(pull_request)
1176 Session().add(pull_request)
1175 self._trigger_pull_request_hook(
1177 self._trigger_pull_request_hook(
1176 pull_request, pull_request.author, 'close')
1178 pull_request, pull_request.author, 'close')
1177
1179
1178 pr_data = pull_request.get_api_data(with_merge_state=False)
1180 pr_data = pull_request.get_api_data(with_merge_state=False)
1179 self._log_audit_action(
1181 self._log_audit_action(
1180 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1182 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1181
1183
1182 def close_pull_request_with_comment(
1184 def close_pull_request_with_comment(
1183 self, pull_request, user, repo, message=None, auth_user=None):
1185 self, pull_request, user, repo, message=None, auth_user=None):
1184
1186
1185 pull_request_review_status = pull_request.calculated_review_status()
1187 pull_request_review_status = pull_request.calculated_review_status()
1186
1188
1187 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1189 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1188 # approved only if we have voting consent
1190 # approved only if we have voting consent
1189 status = ChangesetStatus.STATUS_APPROVED
1191 status = ChangesetStatus.STATUS_APPROVED
1190 else:
1192 else:
1191 status = ChangesetStatus.STATUS_REJECTED
1193 status = ChangesetStatus.STATUS_REJECTED
1192 status_lbl = ChangesetStatus.get_status_lbl(status)
1194 status_lbl = ChangesetStatus.get_status_lbl(status)
1193
1195
1194 default_message = (
1196 default_message = (
1195 'Closing with status change {transition_icon} {status}.'
1197 'Closing with status change {transition_icon} {status}.'
1196 ).format(transition_icon='>', status=status_lbl)
1198 ).format(transition_icon='>', status=status_lbl)
1197 text = message or default_message
1199 text = message or default_message
1198
1200
1199 # create a comment, and link it to new status
1201 # create a comment, and link it to new status
1200 comment = CommentsModel().create(
1202 comment = CommentsModel().create(
1201 text=text,
1203 text=text,
1202 repo=repo.repo_id,
1204 repo=repo.repo_id,
1203 user=user.user_id,
1205 user=user.user_id,
1204 pull_request=pull_request.pull_request_id,
1206 pull_request=pull_request.pull_request_id,
1205 status_change=status_lbl,
1207 status_change=status_lbl,
1206 status_change_type=status,
1208 status_change_type=status,
1207 closing_pr=True,
1209 closing_pr=True,
1208 auth_user=auth_user,
1210 auth_user=auth_user,
1209 )
1211 )
1210
1212
1211 # calculate old status before we change it
1213 # calculate old status before we change it
1212 old_calculated_status = pull_request.calculated_review_status()
1214 old_calculated_status = pull_request.calculated_review_status()
1213 ChangesetStatusModel().set_status(
1215 ChangesetStatusModel().set_status(
1214 repo.repo_id,
1216 repo.repo_id,
1215 status,
1217 status,
1216 user.user_id,
1218 user.user_id,
1217 comment=comment,
1219 comment=comment,
1218 pull_request=pull_request.pull_request_id
1220 pull_request=pull_request.pull_request_id
1219 )
1221 )
1220
1222
1221 Session().flush()
1223 Session().flush()
1222 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1224 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1223 # we now calculate the status of pull request again, and based on that
1225 # we now calculate the status of pull request again, and based on that
1224 # calculation trigger status change. This might happen in cases
1226 # calculation trigger status change. This might happen in cases
1225 # that non-reviewer admin closes a pr, which means his vote doesn't
1227 # that non-reviewer admin closes a pr, which means his vote doesn't
1226 # change the status, while if he's a reviewer this might change it.
1228 # change the status, while if he's a reviewer this might change it.
1227 calculated_status = pull_request.calculated_review_status()
1229 calculated_status = pull_request.calculated_review_status()
1228 if old_calculated_status != calculated_status:
1230 if old_calculated_status != calculated_status:
1229 self._trigger_pull_request_hook(
1231 self._trigger_pull_request_hook(
1230 pull_request, user, 'review_status_change')
1232 pull_request, user, 'review_status_change')
1231
1233
1232 # finally close the PR
1234 # finally close the PR
1233 PullRequestModel().close_pull_request(
1235 PullRequestModel().close_pull_request(
1234 pull_request.pull_request_id, user)
1236 pull_request.pull_request_id, user)
1235
1237
1236 return comment, status
1238 return comment, status
1237
1239
1238 def merge_status(self, pull_request, translator=None,
1240 def merge_status(self, pull_request, translator=None,
1239 force_shadow_repo_refresh=False):
1241 force_shadow_repo_refresh=False):
1240 _ = translator or get_current_request().translate
1242 _ = translator or get_current_request().translate
1241
1243
1242 if not self._is_merge_enabled(pull_request):
1244 if not self._is_merge_enabled(pull_request):
1243 return False, _('Server-side pull request merging is disabled.')
1245 return False, _('Server-side pull request merging is disabled.')
1244 if pull_request.is_closed():
1246 if pull_request.is_closed():
1245 return False, _('This pull request is closed.')
1247 return False, _('This pull request is closed.')
1246 merge_possible, msg = self._check_repo_requirements(
1248 merge_possible, msg = self._check_repo_requirements(
1247 target=pull_request.target_repo, source=pull_request.source_repo,
1249 target=pull_request.target_repo, source=pull_request.source_repo,
1248 translator=_)
1250 translator=_)
1249 if not merge_possible:
1251 if not merge_possible:
1250 return merge_possible, msg
1252 return merge_possible, msg
1251
1253
1252 try:
1254 try:
1253 resp = self._try_merge(
1255 resp = self._try_merge(
1254 pull_request,
1256 pull_request,
1255 force_shadow_repo_refresh=force_shadow_repo_refresh)
1257 force_shadow_repo_refresh=force_shadow_repo_refresh)
1256 log.debug("Merge response: %s", resp)
1258 log.debug("Merge response: %s", resp)
1257 status = resp.possible, self.merge_status_message(
1259 status = resp.possible, self.merge_status_message(
1258 resp.failure_reason)
1260 resp.failure_reason)
1259 except NotImplementedError:
1261 except NotImplementedError:
1260 status = False, _('Pull request merging is not supported.')
1262 status = False, _('Pull request merging is not supported.')
1261
1263
1262 return status
1264 return status
1263
1265
1264 def _check_repo_requirements(self, target, source, translator):
1266 def _check_repo_requirements(self, target, source, translator):
1265 """
1267 """
1266 Check if `target` and `source` have compatible requirements.
1268 Check if `target` and `source` have compatible requirements.
1267
1269
1268 Currently this is just checking for largefiles.
1270 Currently this is just checking for largefiles.
1269 """
1271 """
1270 _ = translator
1272 _ = translator
1271 target_has_largefiles = self._has_largefiles(target)
1273 target_has_largefiles = self._has_largefiles(target)
1272 source_has_largefiles = self._has_largefiles(source)
1274 source_has_largefiles = self._has_largefiles(source)
1273 merge_possible = True
1275 merge_possible = True
1274 message = u''
1276 message = u''
1275
1277
1276 if target_has_largefiles != source_has_largefiles:
1278 if target_has_largefiles != source_has_largefiles:
1277 merge_possible = False
1279 merge_possible = False
1278 if source_has_largefiles:
1280 if source_has_largefiles:
1279 message = _(
1281 message = _(
1280 'Target repository large files support is disabled.')
1282 'Target repository large files support is disabled.')
1281 else:
1283 else:
1282 message = _(
1284 message = _(
1283 'Source repository large files support is disabled.')
1285 'Source repository large files support is disabled.')
1284
1286
1285 return merge_possible, message
1287 return merge_possible, message
1286
1288
1287 def _has_largefiles(self, repo):
1289 def _has_largefiles(self, repo):
1288 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1290 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1289 'extensions', 'largefiles')
1291 'extensions', 'largefiles')
1290 return largefiles_ui and largefiles_ui[0].active
1292 return largefiles_ui and largefiles_ui[0].active
1291
1293
1292 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1294 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1293 """
1295 """
1294 Try to merge the pull request and return the merge status.
1296 Try to merge the pull request and return the merge status.
1295 """
1297 """
1296 log.debug(
1298 log.debug(
1297 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1299 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1298 pull_request.pull_request_id, force_shadow_repo_refresh)
1300 pull_request.pull_request_id, force_shadow_repo_refresh)
1299 target_vcs = pull_request.target_repo.scm_instance()
1301 target_vcs = pull_request.target_repo.scm_instance()
1300
1302
1301 # Refresh the target reference.
1303 # Refresh the target reference.
1302 try:
1304 try:
1303 target_ref = self._refresh_reference(
1305 target_ref = self._refresh_reference(
1304 pull_request.target_ref_parts, target_vcs)
1306 pull_request.target_ref_parts, target_vcs)
1305 except CommitDoesNotExistError:
1307 except CommitDoesNotExistError:
1306 merge_state = MergeResponse(
1308 merge_state = MergeResponse(
1307 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1309 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1308 return merge_state
1310 return merge_state
1309
1311
1310 target_locked = pull_request.target_repo.locked
1312 target_locked = pull_request.target_repo.locked
1311 if target_locked and target_locked[0]:
1313 if target_locked and target_locked[0]:
1312 log.debug("The target repository is locked.")
1314 log.debug("The target repository is locked.")
1313 merge_state = MergeResponse(
1315 merge_state = MergeResponse(
1314 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1316 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1315 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1317 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1316 pull_request, target_ref):
1318 pull_request, target_ref):
1317 log.debug("Refreshing the merge status of the repository.")
1319 log.debug("Refreshing the merge status of the repository.")
1318 merge_state = self._refresh_merge_state(
1320 merge_state = self._refresh_merge_state(
1319 pull_request, target_vcs, target_ref)
1321 pull_request, target_vcs, target_ref)
1320 else:
1322 else:
1321 possible = pull_request.\
1323 possible = pull_request.\
1322 last_merge_status == MergeFailureReason.NONE
1324 last_merge_status == MergeFailureReason.NONE
1323 merge_state = MergeResponse(
1325 merge_state = MergeResponse(
1324 possible, False, None, pull_request.last_merge_status)
1326 possible, False, None, pull_request.last_merge_status)
1325
1327
1326 return merge_state
1328 return merge_state
1327
1329
1328 def _refresh_reference(self, reference, vcs_repository):
1330 def _refresh_reference(self, reference, vcs_repository):
1329 if reference.type in ('branch', 'book'):
1331 if reference.type in self.UPDATABLE_REF_TYPES:
1330 name_or_id = reference.name
1332 name_or_id = reference.name
1331 else:
1333 else:
1332 name_or_id = reference.commit_id
1334 name_or_id = reference.commit_id
1333 refreshed_commit = vcs_repository.get_commit(name_or_id)
1335 refreshed_commit = vcs_repository.get_commit(name_or_id)
1334 refreshed_reference = Reference(
1336 refreshed_reference = Reference(
1335 reference.type, reference.name, refreshed_commit.raw_id)
1337 reference.type, reference.name, refreshed_commit.raw_id)
1336 return refreshed_reference
1338 return refreshed_reference
1337
1339
1338 def _needs_merge_state_refresh(self, pull_request, target_reference):
1340 def _needs_merge_state_refresh(self, pull_request, target_reference):
1339 return not(
1341 return not(
1340 pull_request.revisions and
1342 pull_request.revisions and
1341 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1343 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1342 target_reference.commit_id == pull_request._last_merge_target_rev)
1344 target_reference.commit_id == pull_request._last_merge_target_rev)
1343
1345
1344 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1346 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1345 workspace_id = self._workspace_id(pull_request)
1347 workspace_id = self._workspace_id(pull_request)
1346 source_vcs = pull_request.source_repo.scm_instance()
1348 source_vcs = pull_request.source_repo.scm_instance()
1347 repo_id = pull_request.target_repo.repo_id
1349 repo_id = pull_request.target_repo.repo_id
1348 use_rebase = self._use_rebase_for_merging(pull_request)
1350 use_rebase = self._use_rebase_for_merging(pull_request)
1349 close_branch = self._close_branch_before_merging(pull_request)
1351 close_branch = self._close_branch_before_merging(pull_request)
1350 merge_state = target_vcs.merge(
1352 merge_state = target_vcs.merge(
1351 repo_id, workspace_id,
1353 repo_id, workspace_id,
1352 target_reference, source_vcs, pull_request.source_ref_parts,
1354 target_reference, source_vcs, pull_request.source_ref_parts,
1353 dry_run=True, use_rebase=use_rebase,
1355 dry_run=True, use_rebase=use_rebase,
1354 close_branch=close_branch)
1356 close_branch=close_branch)
1355
1357
1356 # Do not store the response if there was an unknown error.
1358 # Do not store the response if there was an unknown error.
1357 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1359 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1358 pull_request._last_merge_source_rev = \
1360 pull_request._last_merge_source_rev = \
1359 pull_request.source_ref_parts.commit_id
1361 pull_request.source_ref_parts.commit_id
1360 pull_request._last_merge_target_rev = target_reference.commit_id
1362 pull_request._last_merge_target_rev = target_reference.commit_id
1361 pull_request.last_merge_status = merge_state.failure_reason
1363 pull_request.last_merge_status = merge_state.failure_reason
1362 pull_request.shadow_merge_ref = merge_state.merge_ref
1364 pull_request.shadow_merge_ref = merge_state.merge_ref
1363 Session().add(pull_request)
1365 Session().add(pull_request)
1364 Session().commit()
1366 Session().commit()
1365
1367
1366 return merge_state
1368 return merge_state
1367
1369
1368 def _workspace_id(self, pull_request):
1370 def _workspace_id(self, pull_request):
1369 workspace_id = 'pr-%s' % pull_request.pull_request_id
1371 workspace_id = 'pr-%s' % pull_request.pull_request_id
1370 return workspace_id
1372 return workspace_id
1371
1373
1372 def merge_status_message(self, status_code):
1374 def merge_status_message(self, status_code):
1373 """
1375 """
1374 Return a human friendly error message for the given merge status code.
1376 Return a human friendly error message for the given merge status code.
1375 """
1377 """
1376 return self.MERGE_STATUS_MESSAGES[status_code]
1378 return self.MERGE_STATUS_MESSAGES[status_code]
1377
1379
1378 def generate_repo_data(self, repo, commit_id=None, branch=None,
1380 def generate_repo_data(self, repo, commit_id=None, branch=None,
1379 bookmark=None, translator=None):
1381 bookmark=None, translator=None):
1380 from rhodecode.model.repo import RepoModel
1382 from rhodecode.model.repo import RepoModel
1381
1383
1382 all_refs, selected_ref = \
1384 all_refs, selected_ref = \
1383 self._get_repo_pullrequest_sources(
1385 self._get_repo_pullrequest_sources(
1384 repo.scm_instance(), commit_id=commit_id,
1386 repo.scm_instance(), commit_id=commit_id,
1385 branch=branch, bookmark=bookmark, translator=translator)
1387 branch=branch, bookmark=bookmark, translator=translator)
1386
1388
1387 refs_select2 = []
1389 refs_select2 = []
1388 for element in all_refs:
1390 for element in all_refs:
1389 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1391 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1390 refs_select2.append({'text': element[1], 'children': children})
1392 refs_select2.append({'text': element[1], 'children': children})
1391
1393
1392 return {
1394 return {
1393 'user': {
1395 'user': {
1394 'user_id': repo.user.user_id,
1396 'user_id': repo.user.user_id,
1395 'username': repo.user.username,
1397 'username': repo.user.username,
1396 'firstname': repo.user.first_name,
1398 'firstname': repo.user.first_name,
1397 'lastname': repo.user.last_name,
1399 'lastname': repo.user.last_name,
1398 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1400 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1399 },
1401 },
1400 'name': repo.repo_name,
1402 'name': repo.repo_name,
1401 'link': RepoModel().get_url(repo),
1403 'link': RepoModel().get_url(repo),
1402 'description': h.chop_at_smart(repo.description_safe, '\n'),
1404 'description': h.chop_at_smart(repo.description_safe, '\n'),
1403 'refs': {
1405 'refs': {
1404 'all_refs': all_refs,
1406 'all_refs': all_refs,
1405 'selected_ref': selected_ref,
1407 'selected_ref': selected_ref,
1406 'select2_refs': refs_select2
1408 'select2_refs': refs_select2
1407 }
1409 }
1408 }
1410 }
1409
1411
1410 def generate_pullrequest_title(self, source, source_ref, target):
1412 def generate_pullrequest_title(self, source, source_ref, target):
1411 return u'{source}#{at_ref} to {target}'.format(
1413 return u'{source}#{at_ref} to {target}'.format(
1412 source=source,
1414 source=source,
1413 at_ref=source_ref,
1415 at_ref=source_ref,
1414 target=target,
1416 target=target,
1415 )
1417 )
1416
1418
1417 def _cleanup_merge_workspace(self, pull_request):
1419 def _cleanup_merge_workspace(self, pull_request):
1418 # Merging related cleanup
1420 # Merging related cleanup
1419 repo_id = pull_request.target_repo.repo_id
1421 repo_id = pull_request.target_repo.repo_id
1420 target_scm = pull_request.target_repo.scm_instance()
1422 target_scm = pull_request.target_repo.scm_instance()
1421 workspace_id = self._workspace_id(pull_request)
1423 workspace_id = self._workspace_id(pull_request)
1422
1424
1423 try:
1425 try:
1424 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1426 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1425 except NotImplementedError:
1427 except NotImplementedError:
1426 pass
1428 pass
1427
1429
1428 def _get_repo_pullrequest_sources(
1430 def _get_repo_pullrequest_sources(
1429 self, repo, commit_id=None, branch=None, bookmark=None,
1431 self, repo, commit_id=None, branch=None, bookmark=None,
1430 translator=None):
1432 translator=None):
1431 """
1433 """
1432 Return a structure with repo's interesting commits, suitable for
1434 Return a structure with repo's interesting commits, suitable for
1433 the selectors in pullrequest controller
1435 the selectors in pullrequest controller
1434
1436
1435 :param commit_id: a commit that must be in the list somehow
1437 :param commit_id: a commit that must be in the list somehow
1436 and selected by default
1438 and selected by default
1437 :param branch: a branch that must be in the list and selected
1439 :param branch: a branch that must be in the list and selected
1438 by default - even if closed
1440 by default - even if closed
1439 :param bookmark: a bookmark that must be in the list and selected
1441 :param bookmark: a bookmark that must be in the list and selected
1440 """
1442 """
1441 _ = translator or get_current_request().translate
1443 _ = translator or get_current_request().translate
1442
1444
1443 commit_id = safe_str(commit_id) if commit_id else None
1445 commit_id = safe_str(commit_id) if commit_id else None
1444 branch = safe_str(branch) if branch else None
1446 branch = safe_str(branch) if branch else None
1445 bookmark = safe_str(bookmark) if bookmark else None
1447 bookmark = safe_str(bookmark) if bookmark else None
1446
1448
1447 selected = None
1449 selected = None
1448
1450
1449 # order matters: first source that has commit_id in it will be selected
1451 # order matters: first source that has commit_id in it will be selected
1450 sources = []
1452 sources = []
1451 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1453 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1452 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1454 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1453
1455
1454 if commit_id:
1456 if commit_id:
1455 ref_commit = (h.short_id(commit_id), commit_id)
1457 ref_commit = (h.short_id(commit_id), commit_id)
1456 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1458 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1457
1459
1458 sources.append(
1460 sources.append(
1459 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1461 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1460 )
1462 )
1461
1463
1462 groups = []
1464 groups = []
1463 for group_key, ref_list, group_name, match in sources:
1465 for group_key, ref_list, group_name, match in sources:
1464 group_refs = []
1466 group_refs = []
1465 for ref_name, ref_id in ref_list:
1467 for ref_name, ref_id in ref_list:
1466 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1468 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1467 group_refs.append((ref_key, ref_name))
1469 group_refs.append((ref_key, ref_name))
1468
1470
1469 if not selected:
1471 if not selected:
1470 if set([commit_id, match]) & set([ref_id, ref_name]):
1472 if set([commit_id, match]) & set([ref_id, ref_name]):
1471 selected = ref_key
1473 selected = ref_key
1472
1474
1473 if group_refs:
1475 if group_refs:
1474 groups.append((group_refs, group_name))
1476 groups.append((group_refs, group_name))
1475
1477
1476 if not selected:
1478 if not selected:
1477 ref = commit_id or branch or bookmark
1479 ref = commit_id or branch or bookmark
1478 if ref:
1480 if ref:
1479 raise CommitDoesNotExistError(
1481 raise CommitDoesNotExistError(
1480 'No commit refs could be found matching: %s' % ref)
1482 'No commit refs could be found matching: %s' % ref)
1481 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1483 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1482 selected = 'branch:%s:%s' % (
1484 selected = 'branch:%s:%s' % (
1483 repo.DEFAULT_BRANCH_NAME,
1485 repo.DEFAULT_BRANCH_NAME,
1484 repo.branches[repo.DEFAULT_BRANCH_NAME]
1486 repo.branches[repo.DEFAULT_BRANCH_NAME]
1485 )
1487 )
1486 elif repo.commit_ids:
1488 elif repo.commit_ids:
1487 # make the user select in this case
1489 # make the user select in this case
1488 selected = None
1490 selected = None
1489 else:
1491 else:
1490 raise EmptyRepositoryError()
1492 raise EmptyRepositoryError()
1491 return groups, selected
1493 return groups, selected
1492
1494
1493 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1495 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1494 hide_whitespace_changes, diff_context):
1496 hide_whitespace_changes, diff_context):
1495
1497
1496 return self._get_diff_from_pr_or_version(
1498 return self._get_diff_from_pr_or_version(
1497 source_repo, source_ref_id, target_ref_id,
1499 source_repo, source_ref_id, target_ref_id,
1498 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1500 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1499
1501
1500 def _get_diff_from_pr_or_version(
1502 def _get_diff_from_pr_or_version(
1501 self, source_repo, source_ref_id, target_ref_id,
1503 self, source_repo, source_ref_id, target_ref_id,
1502 hide_whitespace_changes, diff_context):
1504 hide_whitespace_changes, diff_context):
1503
1505
1504 target_commit = source_repo.get_commit(
1506 target_commit = source_repo.get_commit(
1505 commit_id=safe_str(target_ref_id))
1507 commit_id=safe_str(target_ref_id))
1506 source_commit = source_repo.get_commit(
1508 source_commit = source_repo.get_commit(
1507 commit_id=safe_str(source_ref_id))
1509 commit_id=safe_str(source_ref_id))
1508 if isinstance(source_repo, Repository):
1510 if isinstance(source_repo, Repository):
1509 vcs_repo = source_repo.scm_instance()
1511 vcs_repo = source_repo.scm_instance()
1510 else:
1512 else:
1511 vcs_repo = source_repo
1513 vcs_repo = source_repo
1512
1514
1513 # TODO: johbo: In the context of an update, we cannot reach
1515 # TODO: johbo: In the context of an update, we cannot reach
1514 # the old commit anymore with our normal mechanisms. It needs
1516 # the old commit anymore with our normal mechanisms. It needs
1515 # some sort of special support in the vcs layer to avoid this
1517 # some sort of special support in the vcs layer to avoid this
1516 # workaround.
1518 # workaround.
1517 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1519 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1518 vcs_repo.alias == 'git'):
1520 vcs_repo.alias == 'git'):
1519 source_commit.raw_id = safe_str(source_ref_id)
1521 source_commit.raw_id = safe_str(source_ref_id)
1520
1522
1521 log.debug('calculating diff between '
1523 log.debug('calculating diff between '
1522 'source_ref:%s and target_ref:%s for repo `%s`',
1524 'source_ref:%s and target_ref:%s for repo `%s`',
1523 target_ref_id, source_ref_id,
1525 target_ref_id, source_ref_id,
1524 safe_unicode(vcs_repo.path))
1526 safe_unicode(vcs_repo.path))
1525
1527
1526 vcs_diff = vcs_repo.get_diff(
1528 vcs_diff = vcs_repo.get_diff(
1527 commit1=target_commit, commit2=source_commit,
1529 commit1=target_commit, commit2=source_commit,
1528 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1530 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1529 return vcs_diff
1531 return vcs_diff
1530
1532
1531 def _is_merge_enabled(self, pull_request):
1533 def _is_merge_enabled(self, pull_request):
1532 return self._get_general_setting(
1534 return self._get_general_setting(
1533 pull_request, 'rhodecode_pr_merge_enabled')
1535 pull_request, 'rhodecode_pr_merge_enabled')
1534
1536
1535 def _use_rebase_for_merging(self, pull_request):
1537 def _use_rebase_for_merging(self, pull_request):
1536 repo_type = pull_request.target_repo.repo_type
1538 repo_type = pull_request.target_repo.repo_type
1537 if repo_type == 'hg':
1539 if repo_type == 'hg':
1538 return self._get_general_setting(
1540 return self._get_general_setting(
1539 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1541 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1540 elif repo_type == 'git':
1542 elif repo_type == 'git':
1541 return self._get_general_setting(
1543 return self._get_general_setting(
1542 pull_request, 'rhodecode_git_use_rebase_for_merging')
1544 pull_request, 'rhodecode_git_use_rebase_for_merging')
1543
1545
1544 return False
1546 return False
1545
1547
1546 def _close_branch_before_merging(self, pull_request):
1548 def _close_branch_before_merging(self, pull_request):
1547 repo_type = pull_request.target_repo.repo_type
1549 repo_type = pull_request.target_repo.repo_type
1548 if repo_type == 'hg':
1550 if repo_type == 'hg':
1549 return self._get_general_setting(
1551 return self._get_general_setting(
1550 pull_request, 'rhodecode_hg_close_branch_before_merging')
1552 pull_request, 'rhodecode_hg_close_branch_before_merging')
1551 elif repo_type == 'git':
1553 elif repo_type == 'git':
1552 return self._get_general_setting(
1554 return self._get_general_setting(
1553 pull_request, 'rhodecode_git_close_branch_before_merging')
1555 pull_request, 'rhodecode_git_close_branch_before_merging')
1554
1556
1555 return False
1557 return False
1556
1558
1557 def _get_general_setting(self, pull_request, settings_key, default=False):
1559 def _get_general_setting(self, pull_request, settings_key, default=False):
1558 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1560 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1559 settings = settings_model.get_general_settings()
1561 settings = settings_model.get_general_settings()
1560 return settings.get(settings_key, default)
1562 return settings.get(settings_key, default)
1561
1563
1562 def _log_audit_action(self, action, action_data, user, pull_request):
1564 def _log_audit_action(self, action, action_data, user, pull_request):
1563 audit_logger.store(
1565 audit_logger.store(
1564 action=action,
1566 action=action,
1565 action_data=action_data,
1567 action_data=action_data,
1566 user=user,
1568 user=user,
1567 repo=pull_request.target_repo)
1569 repo=pull_request.target_repo)
1568
1570
1569 def get_reviewer_functions(self):
1571 def get_reviewer_functions(self):
1570 """
1572 """
1571 Fetches functions for validation and fetching default reviewers.
1573 Fetches functions for validation and fetching default reviewers.
1572 If available we use the EE package, else we fallback to CE
1574 If available we use the EE package, else we fallback to CE
1573 package functions
1575 package functions
1574 """
1576 """
1575 try:
1577 try:
1576 from rc_reviewers.utils import get_default_reviewers_data
1578 from rc_reviewers.utils import get_default_reviewers_data
1577 from rc_reviewers.utils import validate_default_reviewers
1579 from rc_reviewers.utils import validate_default_reviewers
1578 except ImportError:
1580 except ImportError:
1579 from rhodecode.apps.repository.utils import get_default_reviewers_data
1581 from rhodecode.apps.repository.utils import get_default_reviewers_data
1580 from rhodecode.apps.repository.utils import validate_default_reviewers
1582 from rhodecode.apps.repository.utils import validate_default_reviewers
1581
1583
1582 return get_default_reviewers_data, validate_default_reviewers
1584 return get_default_reviewers_data, validate_default_reviewers
1583
1585
1584
1586
1585 class MergeCheck(object):
1587 class MergeCheck(object):
1586 """
1588 """
1587 Perform Merge Checks and returns a check object which stores information
1589 Perform Merge Checks and returns a check object which stores information
1588 about merge errors, and merge conditions
1590 about merge errors, and merge conditions
1589 """
1591 """
1590 TODO_CHECK = 'todo'
1592 TODO_CHECK = 'todo'
1591 PERM_CHECK = 'perm'
1593 PERM_CHECK = 'perm'
1592 REVIEW_CHECK = 'review'
1594 REVIEW_CHECK = 'review'
1593 MERGE_CHECK = 'merge'
1595 MERGE_CHECK = 'merge'
1594
1596
1595 def __init__(self):
1597 def __init__(self):
1596 self.review_status = None
1598 self.review_status = None
1597 self.merge_possible = None
1599 self.merge_possible = None
1598 self.merge_msg = ''
1600 self.merge_msg = ''
1599 self.failed = None
1601 self.failed = None
1600 self.errors = []
1602 self.errors = []
1601 self.error_details = OrderedDict()
1603 self.error_details = OrderedDict()
1602
1604
1603 def push_error(self, error_type, message, error_key, details):
1605 def push_error(self, error_type, message, error_key, details):
1604 self.failed = True
1606 self.failed = True
1605 self.errors.append([error_type, message])
1607 self.errors.append([error_type, message])
1606 self.error_details[error_key] = dict(
1608 self.error_details[error_key] = dict(
1607 details=details,
1609 details=details,
1608 error_type=error_type,
1610 error_type=error_type,
1609 message=message
1611 message=message
1610 )
1612 )
1611
1613
1612 @classmethod
1614 @classmethod
1613 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1615 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1614 force_shadow_repo_refresh=False):
1616 force_shadow_repo_refresh=False):
1615 _ = translator
1617 _ = translator
1616 merge_check = cls()
1618 merge_check = cls()
1617
1619
1618 # permissions to merge
1620 # permissions to merge
1619 user_allowed_to_merge = PullRequestModel().check_user_merge(
1621 user_allowed_to_merge = PullRequestModel().check_user_merge(
1620 pull_request, auth_user)
1622 pull_request, auth_user)
1621 if not user_allowed_to_merge:
1623 if not user_allowed_to_merge:
1622 log.debug("MergeCheck: cannot merge, approval is pending.")
1624 log.debug("MergeCheck: cannot merge, approval is pending.")
1623
1625
1624 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1626 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1625 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1627 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1626 if fail_early:
1628 if fail_early:
1627 return merge_check
1629 return merge_check
1628
1630
1629 # permission to merge into the target branch
1631 # permission to merge into the target branch
1630 target_commit_id = pull_request.target_ref_parts.commit_id
1632 target_commit_id = pull_request.target_ref_parts.commit_id
1631 if pull_request.target_ref_parts.type == 'branch':
1633 if pull_request.target_ref_parts.type == 'branch':
1632 branch_name = pull_request.target_ref_parts.name
1634 branch_name = pull_request.target_ref_parts.name
1633 else:
1635 else:
1634 # for mercurial we can always figure out the branch from the commit
1636 # for mercurial we can always figure out the branch from the commit
1635 # in case of bookmark
1637 # in case of bookmark
1636 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1638 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1637 branch_name = target_commit.branch
1639 branch_name = target_commit.branch
1638
1640
1639 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1641 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1640 pull_request.target_repo.repo_name, branch_name)
1642 pull_request.target_repo.repo_name, branch_name)
1641 if branch_perm and branch_perm == 'branch.none':
1643 if branch_perm and branch_perm == 'branch.none':
1642 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1644 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1643 branch_name, rule)
1645 branch_name, rule)
1644 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1646 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1645 if fail_early:
1647 if fail_early:
1646 return merge_check
1648 return merge_check
1647
1649
1648 # review status, must be always present
1650 # review status, must be always present
1649 review_status = pull_request.calculated_review_status()
1651 review_status = pull_request.calculated_review_status()
1650 merge_check.review_status = review_status
1652 merge_check.review_status = review_status
1651
1653
1652 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1654 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1653 if not status_approved:
1655 if not status_approved:
1654 log.debug("MergeCheck: cannot merge, approval is pending.")
1656 log.debug("MergeCheck: cannot merge, approval is pending.")
1655
1657
1656 msg = _('Pull request reviewer approval is pending.')
1658 msg = _('Pull request reviewer approval is pending.')
1657
1659
1658 merge_check.push_error(
1660 merge_check.push_error(
1659 'warning', msg, cls.REVIEW_CHECK, review_status)
1661 'warning', msg, cls.REVIEW_CHECK, review_status)
1660
1662
1661 if fail_early:
1663 if fail_early:
1662 return merge_check
1664 return merge_check
1663
1665
1664 # left over TODOs
1666 # left over TODOs
1665 todos = CommentsModel().get_unresolved_todos(pull_request)
1667 todos = CommentsModel().get_unresolved_todos(pull_request)
1666 if todos:
1668 if todos:
1667 log.debug("MergeCheck: cannot merge, {} "
1669 log.debug("MergeCheck: cannot merge, {} "
1668 "unresolved todos left.".format(len(todos)))
1670 "unresolved todos left.".format(len(todos)))
1669
1671
1670 if len(todos) == 1:
1672 if len(todos) == 1:
1671 msg = _('Cannot merge, {} TODO still not resolved.').format(
1673 msg = _('Cannot merge, {} TODO still not resolved.').format(
1672 len(todos))
1674 len(todos))
1673 else:
1675 else:
1674 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1676 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1675 len(todos))
1677 len(todos))
1676
1678
1677 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1679 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1678
1680
1679 if fail_early:
1681 if fail_early:
1680 return merge_check
1682 return merge_check
1681
1683
1682 # merge possible, here is the filesystem simulation + shadow repo
1684 # merge possible, here is the filesystem simulation + shadow repo
1683 merge_status, msg = PullRequestModel().merge_status(
1685 merge_status, msg = PullRequestModel().merge_status(
1684 pull_request, translator=translator,
1686 pull_request, translator=translator,
1685 force_shadow_repo_refresh=force_shadow_repo_refresh)
1687 force_shadow_repo_refresh=force_shadow_repo_refresh)
1686 merge_check.merge_possible = merge_status
1688 merge_check.merge_possible = merge_status
1687 merge_check.merge_msg = msg
1689 merge_check.merge_msg = msg
1688 if not merge_status:
1690 if not merge_status:
1689 log.debug(
1691 log.debug(
1690 "MergeCheck: cannot merge, pull request merge not possible.")
1692 "MergeCheck: cannot merge, pull request merge not possible.")
1691 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1693 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1692
1694
1693 if fail_early:
1695 if fail_early:
1694 return merge_check
1696 return merge_check
1695
1697
1696 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1698 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1697 return merge_check
1699 return merge_check
1698
1700
1699 @classmethod
1701 @classmethod
1700 def get_merge_conditions(cls, pull_request, translator):
1702 def get_merge_conditions(cls, pull_request, translator):
1701 _ = translator
1703 _ = translator
1702 merge_details = {}
1704 merge_details = {}
1703
1705
1704 model = PullRequestModel()
1706 model = PullRequestModel()
1705 use_rebase = model._use_rebase_for_merging(pull_request)
1707 use_rebase = model._use_rebase_for_merging(pull_request)
1706
1708
1707 if use_rebase:
1709 if use_rebase:
1708 merge_details['merge_strategy'] = dict(
1710 merge_details['merge_strategy'] = dict(
1709 details={},
1711 details={},
1710 message=_('Merge strategy: rebase')
1712 message=_('Merge strategy: rebase')
1711 )
1713 )
1712 else:
1714 else:
1713 merge_details['merge_strategy'] = dict(
1715 merge_details['merge_strategy'] = dict(
1714 details={},
1716 details={},
1715 message=_('Merge strategy: explicit merge commit')
1717 message=_('Merge strategy: explicit merge commit')
1716 )
1718 )
1717
1719
1718 close_branch = model._close_branch_before_merging(pull_request)
1720 close_branch = model._close_branch_before_merging(pull_request)
1719 if close_branch:
1721 if close_branch:
1720 repo_type = pull_request.target_repo.repo_type
1722 repo_type = pull_request.target_repo.repo_type
1721 if repo_type == 'hg':
1723 if repo_type == 'hg':
1722 close_msg = _('Source branch will be closed after merge.')
1724 close_msg = _('Source branch will be closed after merge.')
1723 elif repo_type == 'git':
1725 elif repo_type == 'git':
1724 close_msg = _('Source branch will be deleted after merge.')
1726 close_msg = _('Source branch will be deleted after merge.')
1725
1727
1726 merge_details['close_branch'] = dict(
1728 merge_details['close_branch'] = dict(
1727 details={},
1729 details={},
1728 message=close_msg
1730 message=close_msg
1729 )
1731 )
1730
1732
1731 return merge_details
1733 return merge_details
1732
1734
1733 ChangeTuple = collections.namedtuple(
1735 ChangeTuple = collections.namedtuple(
1734 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1736 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1735
1737
1736 FileChangeTuple = collections.namedtuple(
1738 FileChangeTuple = collections.namedtuple(
1737 'FileChangeTuple', ['added', 'modified', 'removed'])
1739 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now