##// END OF EJS Templates
observers: code cleanups and fixed tests.
marcink -
r4519:ea50ffa9 stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,368 +1,368 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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', [
59 @pytest.mark.parametrize('source_ref', [
60 'bookmarg:default:initial'
60 'bookmarg:default:initial'
61 ])
61 ])
62 def test_create_with_wrong_refs_data(self, backend, source_ref):
62 def test_create_with_wrong_refs_data(self, backend, source_ref):
63
63
64 data = self._prepare_data(backend)
64 data = self._prepare_data(backend)
65 data['source_ref'] = source_ref
65 data['source_ref'] = source_ref
66
66
67 id_, params = build_data(
67 id_, params = build_data(
68 self.apikey_regular, 'create_pull_request', **data)
68 self.apikey_regular, 'create_pull_request', **data)
69
69
70 response = api_call(self.app, params)
70 response = api_call(self.app, params)
71
71
72 expected = "Ref `{}` type is not allowed. " \
72 expected = "Ref `{}` type is not allowed. " \
73 "Only:['bookmark', 'book', 'tag', 'branch'] " \
73 "Only:['bookmark', 'book', 'tag', 'branch'] " \
74 "are possible.".format(source_ref)
74 "are possible.".format(source_ref)
75 assert_error(id_, expected, given=response.body)
75 assert_error(id_, expected, given=response.body)
76
76
77 @pytest.mark.backends("git", "hg")
77 @pytest.mark.backends("git", "hg")
78 def test_create_with_correct_data(self, backend):
78 def test_create_with_correct_data(self, backend):
79 data = self._prepare_data(backend)
79 data = self._prepare_data(backend)
80 RepoModel().revoke_user_permission(
80 RepoModel().revoke_user_permission(
81 self.source.repo_name, User.DEFAULT_USER)
81 self.source.repo_name, User.DEFAULT_USER)
82 id_, params = build_data(
82 id_, params = build_data(
83 self.apikey_regular, 'create_pull_request', **data)
83 self.apikey_regular, 'create_pull_request', **data)
84 response = api_call(self.app, params)
84 response = api_call(self.app, params)
85 expected_message = "Created new pull request `{title}`".format(
85 expected_message = "Created new pull request `{title}`".format(
86 title=data['title'])
86 title=data['title'])
87 result = response.json
87 result = response.json
88 assert result['error'] is None
88 assert result['error'] is None
89 assert result['result']['msg'] == expected_message
89 assert result['result']['msg'] == expected_message
90 pull_request_id = result['result']['pull_request_id']
90 pull_request_id = result['result']['pull_request_id']
91 pull_request = PullRequestModel().get(pull_request_id)
91 pull_request = PullRequestModel().get(pull_request_id)
92 assert pull_request.title == data['title']
92 assert pull_request.title == data['title']
93 assert pull_request.description == data['description']
93 assert pull_request.description == data['description']
94 assert pull_request.source_ref == data['source_ref']
94 assert pull_request.source_ref == data['source_ref']
95 assert pull_request.target_ref == data['target_ref']
95 assert pull_request.target_ref == data['target_ref']
96 assert pull_request.source_repo.repo_name == data['source_repo']
96 assert pull_request.source_repo.repo_name == data['source_repo']
97 assert pull_request.target_repo.repo_name == data['target_repo']
97 assert pull_request.target_repo.repo_name == data['target_repo']
98 assert pull_request.revisions == [self.commit_ids['change']]
98 assert pull_request.revisions == [self.commit_ids['change']]
99 assert len(pull_request.reviewers) == 1
99 assert len(pull_request.reviewers) == 1
100
100
101 @pytest.mark.backends("git", "hg")
101 @pytest.mark.backends("git", "hg")
102 def test_create_with_empty_description(self, backend):
102 def test_create_with_empty_description(self, backend):
103 data = self._prepare_data(backend)
103 data = self._prepare_data(backend)
104 data.pop('description')
104 data.pop('description')
105 id_, params = build_data(
105 id_, params = build_data(
106 self.apikey_regular, 'create_pull_request', **data)
106 self.apikey_regular, 'create_pull_request', **data)
107 response = api_call(self.app, params)
107 response = api_call(self.app, params)
108 expected_message = "Created new pull request `{title}`".format(
108 expected_message = "Created new pull request `{title}`".format(
109 title=data['title'])
109 title=data['title'])
110 result = response.json
110 result = response.json
111 assert result['error'] is None
111 assert result['error'] is None
112 assert result['result']['msg'] == expected_message
112 assert result['result']['msg'] == expected_message
113 pull_request_id = result['result']['pull_request_id']
113 pull_request_id = result['result']['pull_request_id']
114 pull_request = PullRequestModel().get(pull_request_id)
114 pull_request = PullRequestModel().get(pull_request_id)
115 assert pull_request.description == ''
115 assert pull_request.description == ''
116
116
117 @pytest.mark.backends("git", "hg")
117 @pytest.mark.backends("git", "hg")
118 def test_create_with_empty_title(self, backend):
118 def test_create_with_empty_title(self, backend):
119 data = self._prepare_data(backend)
119 data = self._prepare_data(backend)
120 data.pop('title')
120 data.pop('title')
121 id_, params = build_data(
121 id_, params = build_data(
122 self.apikey_regular, 'create_pull_request', **data)
122 self.apikey_regular, 'create_pull_request', **data)
123 response = api_call(self.app, params)
123 response = api_call(self.app, params)
124 result = response.json
124 result = response.json
125 pull_request_id = result['result']['pull_request_id']
125 pull_request_id = result['result']['pull_request_id']
126 pull_request = PullRequestModel().get(pull_request_id)
126 pull_request = PullRequestModel().get(pull_request_id)
127 data['ref'] = backend.default_branch_name
127 data['ref'] = backend.default_branch_name
128 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
128 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
129 assert pull_request.title == title
129 assert pull_request.title == title
130
130
131 @pytest.mark.backends("git", "hg")
131 @pytest.mark.backends("git", "hg")
132 def test_create_with_reviewers_specified_by_names(
132 def test_create_with_reviewers_specified_by_names(
133 self, backend, no_notifications):
133 self, backend, no_notifications):
134 data = self._prepare_data(backend)
134 data = self._prepare_data(backend)
135 reviewers = [
135 reviewers = [
136 {'username': TEST_USER_REGULAR_LOGIN,
136 {'username': TEST_USER_REGULAR_LOGIN,
137 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
137 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
138 {'username': TEST_USER_ADMIN_LOGIN,
138 {'username': TEST_USER_ADMIN_LOGIN,
139 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
139 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
140 'mandatory': True},
140 'mandatory': True},
141 ]
141 ]
142 data['reviewers'] = reviewers
142 data['reviewers'] = reviewers
143
143
144 id_, params = build_data(
144 id_, params = build_data(
145 self.apikey_regular, 'create_pull_request', **data)
145 self.apikey_regular, 'create_pull_request', **data)
146 response = api_call(self.app, params)
146 response = api_call(self.app, params)
147
147
148 expected_message = "Created new pull request `{title}`".format(
148 expected_message = "Created new pull request `{title}`".format(
149 title=data['title'])
149 title=data['title'])
150 result = response.json
150 result = response.json
151 assert result['error'] is None
151 assert result['error'] is None
152 assert result['result']['msg'] == expected_message
152 assert result['result']['msg'] == expected_message
153 pull_request_id = result['result']['pull_request_id']
153 pull_request_id = result['result']['pull_request_id']
154 pull_request = PullRequestModel().get(pull_request_id)
154 pull_request = PullRequestModel().get(pull_request_id)
155
155
156 actual_reviewers = []
156 actual_reviewers = []
157 for rev in pull_request.reviewers:
157 for rev in pull_request.reviewers:
158 entry = {
158 entry = {
159 'username': rev.user.username,
159 'username': rev.user.username,
160 'reasons': rev.reasons,
160 'reasons': rev.reasons,
161 }
161 }
162 if rev.mandatory:
162 if rev.mandatory:
163 entry['mandatory'] = rev.mandatory
163 entry['mandatory'] = rev.mandatory
164 actual_reviewers.append(entry)
164 actual_reviewers.append(entry)
165
165
166 owner_username = pull_request.target_repo.user.username
166 owner_username = pull_request.target_repo.user.username
167 for spec_reviewer in reviewers[::]:
167 for spec_reviewer in reviewers[::]:
168 # 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
169 # this get's overridden by a add owner to reviewers rule
169 # this get's overridden by a add owner to reviewers rule
170 if spec_reviewer['username'] == owner_username:
170 if spec_reviewer['username'] == owner_username:
171 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
171 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
172 # since owner is more important, we don't inherit mandatory flag
172 # since owner is more important, we don't inherit mandatory flag
173 del spec_reviewer['mandatory']
173 del spec_reviewer['mandatory']
174
174
175 assert sorted(actual_reviewers, key=lambda e: e['username']) \
175 assert sorted(actual_reviewers, key=lambda e: e['username']) \
176 == sorted(reviewers, key=lambda e: e['username'])
176 == sorted(reviewers, key=lambda e: e['username'])
177
177
178 @pytest.mark.backends("git", "hg")
178 @pytest.mark.backends("git", "hg")
179 def test_create_with_reviewers_specified_by_ids(
179 def test_create_with_reviewers_specified_by_ids(
180 self, backend, no_notifications):
180 self, backend, no_notifications):
181 data = self._prepare_data(backend)
181 data = self._prepare_data(backend)
182 reviewers = [
182 reviewers = [
183 {'username': UserModel().get_by_username(
183 {'username': UserModel().get_by_username(
184 TEST_USER_REGULAR_LOGIN).user_id,
184 TEST_USER_REGULAR_LOGIN).user_id,
185 'reasons': ['added manually']},
185 'reasons': ['added manually']},
186 {'username': UserModel().get_by_username(
186 {'username': UserModel().get_by_username(
187 TEST_USER_ADMIN_LOGIN).user_id,
187 TEST_USER_ADMIN_LOGIN).user_id,
188 'reasons': ['added manually']},
188 'reasons': ['added manually']},
189 ]
189 ]
190
190
191 data['reviewers'] = reviewers
191 data['reviewers'] = reviewers
192 id_, params = build_data(
192 id_, params = build_data(
193 self.apikey_regular, 'create_pull_request', **data)
193 self.apikey_regular, 'create_pull_request', **data)
194 response = api_call(self.app, params)
194 response = api_call(self.app, params)
195
195
196 expected_message = "Created new pull request `{title}`".format(
196 expected_message = "Created new pull request `{title}`".format(
197 title=data['title'])
197 title=data['title'])
198 result = response.json
198 result = response.json
199 assert result['error'] is None
199 assert result['error'] is None
200 assert result['result']['msg'] == expected_message
200 assert result['result']['msg'] == expected_message
201 pull_request_id = result['result']['pull_request_id']
201 pull_request_id = result['result']['pull_request_id']
202 pull_request = PullRequestModel().get(pull_request_id)
202 pull_request = PullRequestModel().get(pull_request_id)
203
203
204 actual_reviewers = []
204 actual_reviewers = []
205 for rev in pull_request.reviewers:
205 for rev in pull_request.reviewers:
206 entry = {
206 entry = {
207 'username': rev.user.user_id,
207 'username': rev.user.user_id,
208 'reasons': rev.reasons,
208 'reasons': rev.reasons,
209 }
209 }
210 if rev.mandatory:
210 if rev.mandatory:
211 entry['mandatory'] = rev.mandatory
211 entry['mandatory'] = rev.mandatory
212 actual_reviewers.append(entry)
212 actual_reviewers.append(entry)
213
213
214 owner_user_id = pull_request.target_repo.user.user_id
214 owner_user_id = pull_request.target_repo.user.user_id
215 for spec_reviewer in reviewers[::]:
215 for spec_reviewer in reviewers[::]:
216 # 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
217 # this get's overridden by a add owner to reviewers rule
217 # this get's overridden by a add owner to reviewers rule
218 if spec_reviewer['username'] == owner_user_id:
218 if spec_reviewer['username'] == owner_user_id:
219 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
219 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
220
220
221 assert sorted(actual_reviewers, key=lambda e: e['username']) \
221 assert sorted(actual_reviewers, key=lambda e: e['username']) \
222 == sorted(reviewers, key=lambda e: e['username'])
222 == sorted(reviewers, key=lambda e: e['username'])
223
223
224 @pytest.mark.backends("git", "hg")
224 @pytest.mark.backends("git", "hg")
225 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):
226 data = self._prepare_data(backend)
226 data = self._prepare_data(backend)
227 data['reviewers'] = [{'username': 'somebody'}]
227 data['reviewers'] = [{'username': 'somebody'}]
228 id_, params = build_data(
228 id_, params = build_data(
229 self.apikey_regular, 'create_pull_request', **data)
229 self.apikey_regular, 'create_pull_request', **data)
230 response = api_call(self.app, params)
230 response = api_call(self.app, params)
231 expected_message = 'user `somebody` does not exist'
231 expected_message = 'user `somebody` does not exist'
232 assert_error(id_, expected_message, given=response.body)
232 assert_error(id_, expected_message, given=response.body)
233
233
234 @pytest.mark.backends("git", "hg")
234 @pytest.mark.backends("git", "hg")
235 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
235 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
236 data = self._prepare_data(backend)
236 data = self._prepare_data(backend)
237 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
237 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
238 data['reviewers'] = reviewers
238 data['reviewers'] = reviewers
239 id_, params = build_data(
239 id_, params = build_data(
240 self.apikey_regular, 'create_pull_request', **data)
240 self.apikey_regular, 'create_pull_request', **data)
241 response = api_call(self.app, params)
241 response = api_call(self.app, params)
242 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
242 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
243 assert_error(id_, expected_message, given=response.body)
243 assert_error(id_, expected_message, given=response.body)
244
244
245 @pytest.mark.backends("git", "hg")
245 @pytest.mark.backends("git", "hg")
246 def test_create_with_no_commit_hashes(self, backend):
246 def test_create_with_no_commit_hashes(self, backend):
247 data = self._prepare_data(backend)
247 data = self._prepare_data(backend)
248 expected_source_ref = data['source_ref']
248 expected_source_ref = data['source_ref']
249 expected_target_ref = data['target_ref']
249 expected_target_ref = data['target_ref']
250 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
250 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
251 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
251 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
252 id_, params = build_data(
252 id_, params = build_data(
253 self.apikey_regular, 'create_pull_request', **data)
253 self.apikey_regular, 'create_pull_request', **data)
254 response = api_call(self.app, params)
254 response = api_call(self.app, params)
255 expected_message = "Created new pull request `{title}`".format(
255 expected_message = "Created new pull request `{title}`".format(
256 title=data['title'])
256 title=data['title'])
257 result = response.json
257 result = response.json
258 assert result['result']['msg'] == expected_message
258 assert result['result']['msg'] == expected_message
259 pull_request_id = result['result']['pull_request_id']
259 pull_request_id = result['result']['pull_request_id']
260 pull_request = PullRequestModel().get(pull_request_id)
260 pull_request = PullRequestModel().get(pull_request_id)
261 assert pull_request.source_ref == expected_source_ref
261 assert pull_request.source_ref == expected_source_ref
262 assert pull_request.target_ref == expected_target_ref
262 assert pull_request.target_ref == expected_target_ref
263
263
264 @pytest.mark.backends("git", "hg")
264 @pytest.mark.backends("git", "hg")
265 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
265 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
266 def test_create_fails_with_wrong_repo(self, backend, data_key):
266 def test_create_fails_with_wrong_repo(self, backend, data_key):
267 repo_name = 'fake-repo'
267 repo_name = 'fake-repo'
268 data = self._prepare_data(backend)
268 data = self._prepare_data(backend)
269 data[data_key] = repo_name
269 data[data_key] = repo_name
270 id_, params = build_data(
270 id_, params = build_data(
271 self.apikey_regular, 'create_pull_request', **data)
271 self.apikey_regular, 'create_pull_request', **data)
272 response = api_call(self.app, params)
272 response = api_call(self.app, params)
273 expected_message = 'repository `{}` does not exist'.format(repo_name)
273 expected_message = 'repository `{}` does not exist'.format(repo_name)
274 assert_error(id_, expected_message, given=response.body)
274 assert_error(id_, expected_message, given=response.body)
275
275
276 @pytest.mark.backends("git", "hg")
276 @pytest.mark.backends("git", "hg")
277 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
277 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
278 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):
279 branch_name = 'test-branch'
279 branch_name = 'test-branch'
280 data = self._prepare_data(backend)
280 data = self._prepare_data(backend)
281 data[data_key] = "branch:{}".format(branch_name)
281 data[data_key] = "branch:{}".format(branch_name)
282 id_, params = build_data(
282 id_, params = build_data(
283 self.apikey_regular, 'create_pull_request', **data)
283 self.apikey_regular, 'create_pull_request', **data)
284 response = api_call(self.app, params)
284 response = api_call(self.app, params)
285 expected_message = 'The specified value:{type}:`{name}` ' \
285 expected_message = 'The specified value:{type}:`{name}` ' \
286 'does not exist, or is not allowed.'.format(type='branch',
286 'does not exist, or is not allowed.'.format(type='branch',
287 name=branch_name)
287 name=branch_name)
288 assert_error(id_, expected_message, given=response.body)
288 assert_error(id_, expected_message, given=response.body)
289
289
290 @pytest.mark.backends("git", "hg")
290 @pytest.mark.backends("git", "hg")
291 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
291 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
292 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):
293 data = self._prepare_data(backend)
293 data = self._prepare_data(backend)
294 ref = 'stange-ref'
294 ref = 'stange-ref'
295 data[data_key] = ref
295 data[data_key] = ref
296 id_, params = build_data(
296 id_, params = build_data(
297 self.apikey_regular, 'create_pull_request', **data)
297 self.apikey_regular, 'create_pull_request', **data)
298 response = api_call(self.app, params)
298 response = api_call(self.app, params)
299 expected_message = (
299 expected_message = (
300 'Ref `{ref}` given in a wrong format. Please check the API'
300 'Ref `{ref}` given in a wrong format. Please check the API'
301 ' documentation for more details'.format(ref=ref))
301 ' documentation for more details'.format(ref=ref))
302 assert_error(id_, expected_message, given=response.body)
302 assert_error(id_, expected_message, given=response.body)
303
303
304 @pytest.mark.backends("git", "hg")
304 @pytest.mark.backends("git", "hg")
305 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
305 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
306 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):
307 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
307 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
308 ref = self._get_full_ref(backend, commit_id)
308 ref = self._get_full_ref(backend, commit_id)
309 data = self._prepare_data(backend)
309 data = self._prepare_data(backend)
310 data[data_key] = ref
310 data[data_key] = ref
311 id_, params = build_data(
311 id_, params = build_data(
312 self.apikey_regular, 'create_pull_request', **data)
312 self.apikey_regular, 'create_pull_request', **data)
313 response = api_call(self.app, params)
313 response = api_call(self.app, params)
314 expected_message = 'Ref `{}` does not exist'.format(ref)
314 expected_message = 'Ref `{}` does not exist'.format(ref)
315 assert_error(id_, expected_message, given=response.body)
315 assert_error(id_, expected_message, given=response.body)
316
316
317 @pytest.mark.backends("git", "hg")
317 @pytest.mark.backends("git", "hg")
318 def test_create_fails_when_no_revisions(self, backend):
318 def test_create_fails_when_no_revisions(self, backend):
319 data = self._prepare_data(backend, source_head='initial')
319 data = self._prepare_data(backend, source_head='initial')
320 id_, params = build_data(
320 id_, params = build_data(
321 self.apikey_regular, 'create_pull_request', **data)
321 self.apikey_regular, 'create_pull_request', **data)
322 response = api_call(self.app, params)
322 response = api_call(self.app, params)
323 expected_message = 'no commits found'
323 expected_message = 'no commits found for merge between specified references'
324 assert_error(id_, expected_message, given=response.body)
324 assert_error(id_, expected_message, given=response.body)
325
325
326 @pytest.mark.backends("git", "hg")
326 @pytest.mark.backends("git", "hg")
327 def test_create_fails_when_no_permissions(self, backend):
327 def test_create_fails_when_no_permissions(self, backend):
328 data = self._prepare_data(backend)
328 data = self._prepare_data(backend)
329 RepoModel().revoke_user_permission(
329 RepoModel().revoke_user_permission(
330 self.source.repo_name, self.test_user)
330 self.source.repo_name, self.test_user)
331 RepoModel().revoke_user_permission(
331 RepoModel().revoke_user_permission(
332 self.source.repo_name, User.DEFAULT_USER)
332 self.source.repo_name, User.DEFAULT_USER)
333
333
334 id_, params = build_data(
334 id_, params = build_data(
335 self.apikey_regular, 'create_pull_request', **data)
335 self.apikey_regular, 'create_pull_request', **data)
336 response = api_call(self.app, params)
336 response = api_call(self.app, params)
337 expected_message = 'repository `{}` does not exist'.format(
337 expected_message = 'repository `{}` does not exist'.format(
338 self.source.repo_name)
338 self.source.repo_name)
339 assert_error(id_, expected_message, given=response.body)
339 assert_error(id_, expected_message, given=response.body)
340
340
341 def _prepare_data(
341 def _prepare_data(
342 self, backend, source_head='change', target_head='initial'):
342 self, backend, source_head='change', target_head='initial'):
343 commits = [
343 commits = [
344 {'message': 'initial'},
344 {'message': 'initial'},
345 {'message': 'change'},
345 {'message': 'change'},
346 {'message': 'new-feature', 'parents': ['initial']},
346 {'message': 'new-feature', 'parents': ['initial']},
347 ]
347 ]
348 self.commit_ids = backend.create_master_repo(commits)
348 self.commit_ids = backend.create_master_repo(commits)
349 self.source = backend.create_repo(heads=[source_head])
349 self.source = backend.create_repo(heads=[source_head])
350 self.target = backend.create_repo(heads=[target_head])
350 self.target = backend.create_repo(heads=[target_head])
351
351
352 data = {
352 data = {
353 'source_repo': self.source.repo_name,
353 'source_repo': self.source.repo_name,
354 'target_repo': self.target.repo_name,
354 'target_repo': self.target.repo_name,
355 'source_ref': self._get_full_ref(
355 'source_ref': self._get_full_ref(
356 backend, self.commit_ids[source_head]),
356 backend, self.commit_ids[source_head]),
357 'target_ref': self._get_full_ref(
357 'target_ref': self._get_full_ref(
358 backend, self.commit_ids[target_head]),
358 backend, self.commit_ids[target_head]),
359 'title': 'Test PR 1',
359 'title': 'Test PR 1',
360 'description': 'Test'
360 'description': 'Test'
361 }
361 }
362 RepoModel().grant_user_permission(
362 RepoModel().grant_user_permission(
363 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
363 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
364 return data
364 return data
365
365
366 def _get_full_ref(self, backend, commit_id):
366 def _get_full_ref(self, backend, commit_id):
367 return 'branch:{branch}:{commit_id}'.format(
367 return 'branch:{branch}:{commit_id}'.format(
368 branch=backend.default_branch_name, commit_id=commit_id)
368 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,80 +1,82 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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
23
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error)
27 build_data, api_call, assert_error)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestGetPullRequest(object):
31 class TestGetPullRequest(object):
32
32 @pytest.mark.backends("git", "hg")
33 @pytest.mark.backends("git", "hg")
33 def test_api_get_pull_requests(self, pr_util):
34 def test_api_get_pull_requests(self, pr_util):
34 pull_request = pr_util.create_pull_request()
35 pull_request = pr_util.create_pull_request()
35 pull_request_2 = PullRequestModel().create(
36 pull_request_2 = PullRequestModel().create(
36 created_by=pull_request.author,
37 created_by=pull_request.author,
37 source_repo=pull_request.source_repo,
38 source_repo=pull_request.source_repo,
38 source_ref=pull_request.source_ref,
39 source_ref=pull_request.source_ref,
39 target_repo=pull_request.target_repo,
40 target_repo=pull_request.target_repo,
40 target_ref=pull_request.target_ref,
41 target_ref=pull_request.target_ref,
41 revisions=pull_request.revisions,
42 revisions=pull_request.revisions,
42 reviewers=(),
43 reviewers=(),
44 observers=(),
43 title=pull_request.title,
45 title=pull_request.title,
44 description=pull_request.description,
46 description=pull_request.description,
45 )
47 )
46 Session().commit()
48 Session().commit()
47 id_, params = build_data(
49 id_, params = build_data(
48 self.apikey, 'get_pull_requests',
50 self.apikey, 'get_pull_requests',
49 repoid=pull_request.target_repo.repo_name)
51 repoid=pull_request.target_repo.repo_name)
50 response = api_call(self.app, params)
52 response = api_call(self.app, params)
51 assert response.status == '200 OK'
53 assert response.status == '200 OK'
52 assert len(response.json['result']) == 2
54 assert len(response.json['result']) == 2
53
55
54 PullRequestModel().close_pull_request(
56 PullRequestModel().close_pull_request(
55 pull_request_2, pull_request_2.author)
57 pull_request_2, pull_request_2.author)
56 Session().commit()
58 Session().commit()
57
59
58 id_, params = build_data(
60 id_, params = build_data(
59 self.apikey, 'get_pull_requests',
61 self.apikey, 'get_pull_requests',
60 repoid=pull_request.target_repo.repo_name,
62 repoid=pull_request.target_repo.repo_name,
61 status='new')
63 status='new')
62 response = api_call(self.app, params)
64 response = api_call(self.app, params)
63 assert response.status == '200 OK'
65 assert response.status == '200 OK'
64 assert len(response.json['result']) == 1
66 assert len(response.json['result']) == 1
65
67
66 id_, params = build_data(
68 id_, params = build_data(
67 self.apikey, 'get_pull_requests',
69 self.apikey, 'get_pull_requests',
68 repoid=pull_request.target_repo.repo_name,
70 repoid=pull_request.target_repo.repo_name,
69 status='closed')
71 status='closed')
70 response = api_call(self.app, params)
72 response = api_call(self.app, params)
71 assert response.status == '200 OK'
73 assert response.status == '200 OK'
72 assert len(response.json['result']) == 1
74 assert len(response.json['result']) == 1
73
75
74 @pytest.mark.backends("git", "hg")
76 @pytest.mark.backends("git", "hg")
75 def test_api_get_pull_requests_repo_error(self):
77 def test_api_get_pull_requests_repo_error(self):
76 id_, params = build_data(self.apikey, 'get_pull_requests', repoid=666)
78 id_, params = build_data(self.apikey, 'get_pull_requests', repoid=666)
77 response = api_call(self.app, params)
79 response = api_call(self.app, params)
78
80
79 expected = 'repository `666` does not exist'
81 expected = 'repository `666` does not exist'
80 assert_error(id_, expected, given=response.body)
82 assert_error(id_, expected, given=response.body)
@@ -1,212 +1,215 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.lib.vcs.nodes import FileNode
23 from rhodecode.lib.vcs.nodes import FileNode
24 from rhodecode.model.db import User
24 from rhodecode.model.db import User
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_ok, assert_error)
28 build_data, api_call, assert_ok, assert_error)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestUpdatePullRequest(object):
32 class TestUpdatePullRequest(object):
33
33
34 @pytest.mark.backends("git", "hg")
34 @pytest.mark.backends("git", "hg")
35 def test_api_update_pull_request_title_or_description(
35 def test_api_update_pull_request_title_or_description(
36 self, pr_util, no_notifications):
36 self, pr_util, no_notifications):
37 pull_request = pr_util.create_pull_request()
37 pull_request = pr_util.create_pull_request()
38
38
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'update_pull_request',
40 self.apikey, 'update_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id,
42 pullrequestid=pull_request.pull_request_id,
43 title='New TITLE OF A PR',
43 title='New TITLE OF A PR',
44 description='New DESC OF A PR',
44 description='New DESC OF A PR',
45 )
45 )
46 response = api_call(self.app, params)
46 response = api_call(self.app, params)
47
47
48 expected = {
48 expected = {
49 "msg": "Updated pull request `{}`".format(
49 "msg": "Updated pull request `{}`".format(
50 pull_request.pull_request_id),
50 pull_request.pull_request_id),
51 "pull_request": response.json['result']['pull_request'],
51 "pull_request": response.json['result']['pull_request'],
52 "updated_commits": {"added": [], "common": [], "removed": []},
52 "updated_commits": {"added": [], "common": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
54 "updated_observers": {"added": [], "removed": []},
54 }
55 }
55
56
56 response_json = response.json['result']
57 response_json = response.json['result']
57 assert response_json == expected
58 assert response_json == expected
58 pr = response_json['pull_request']
59 pr = response_json['pull_request']
59 assert pr['title'] == 'New TITLE OF A PR'
60 assert pr['title'] == 'New TITLE OF A PR'
60 assert pr['description'] == 'New DESC OF A PR'
61 assert pr['description'] == 'New DESC OF A PR'
61
62
62 @pytest.mark.backends("git", "hg")
63 @pytest.mark.backends("git", "hg")
63 def test_api_try_update_closed_pull_request(
64 def test_api_try_update_closed_pull_request(
64 self, pr_util, no_notifications):
65 self, pr_util, no_notifications):
65 pull_request = pr_util.create_pull_request()
66 pull_request = pr_util.create_pull_request()
66 PullRequestModel().close_pull_request(
67 PullRequestModel().close_pull_request(
67 pull_request, TEST_USER_ADMIN_LOGIN)
68 pull_request, TEST_USER_ADMIN_LOGIN)
68
69
69 id_, params = build_data(
70 id_, params = build_data(
70 self.apikey, 'update_pull_request',
71 self.apikey, 'update_pull_request',
71 repoid=pull_request.target_repo.repo_name,
72 repoid=pull_request.target_repo.repo_name,
72 pullrequestid=pull_request.pull_request_id)
73 pullrequestid=pull_request.pull_request_id)
73 response = api_call(self.app, params)
74 response = api_call(self.app, params)
74
75
75 expected = 'pull request `{}` update failed, pull request ' \
76 expected = 'pull request `{}` update failed, pull request ' \
76 'is closed'.format(pull_request.pull_request_id)
77 'is closed'.format(pull_request.pull_request_id)
77
78
78 assert_error(id_, expected, response.body)
79 assert_error(id_, expected, response.body)
79
80
80 @pytest.mark.backends("git", "hg")
81 @pytest.mark.backends("git", "hg")
81 def test_api_update_update_commits(self, pr_util, no_notifications):
82 def test_api_update_update_commits(self, pr_util, no_notifications):
82 commits = [
83 commits = [
83 {'message': 'a'},
84 {'message': 'a'},
84 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
85 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
86 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
86 ]
87 ]
87 pull_request = pr_util.create_pull_request(
88 pull_request = pr_util.create_pull_request(
88 commits=commits, target_head='a', source_head='b', revisions=['b'])
89 commits=commits, target_head='a', source_head='b', revisions=['b'])
89 pr_util.update_source_repository(head='c')
90 pr_util.update_source_repository(head='c')
90 repo = pull_request.source_repo.scm_instance()
91 repo = pull_request.source_repo.scm_instance()
91 commits = [x for x in repo.get_commits()]
92 commits = [x for x in repo.get_commits()]
92
93
93 added_commit_id = commits[-1].raw_id # c commit
94 added_commit_id = commits[-1].raw_id # c commit
94 common_commit_id = commits[1].raw_id # b commit is common ancestor
95 common_commit_id = commits[1].raw_id # b commit is common ancestor
95 total_commits = [added_commit_id, common_commit_id]
96 total_commits = [added_commit_id, common_commit_id]
96
97
97 id_, params = build_data(
98 id_, params = build_data(
98 self.apikey, 'update_pull_request',
99 self.apikey, 'update_pull_request',
99 repoid=pull_request.target_repo.repo_name,
100 repoid=pull_request.target_repo.repo_name,
100 pullrequestid=pull_request.pull_request_id,
101 pullrequestid=pull_request.pull_request_id,
101 update_commits=True
102 update_commits=True
102 )
103 )
103 response = api_call(self.app, params)
104 response = api_call(self.app, params)
104
105
105 expected = {
106 expected = {
106 "msg": "Updated pull request `{}`".format(
107 "msg": "Updated pull request `{}`".format(
107 pull_request.pull_request_id),
108 pull_request.pull_request_id),
108 "pull_request": response.json['result']['pull_request'],
109 "pull_request": response.json['result']['pull_request'],
109 "updated_commits": {"added": [added_commit_id],
110 "updated_commits": {"added": [added_commit_id],
110 "common": [common_commit_id],
111 "common": [common_commit_id],
111 "total": total_commits,
112 "total": total_commits,
112 "removed": []},
113 "removed": []},
113 "updated_reviewers": {"added": [], "removed": []},
114 "updated_reviewers": {"added": [], "removed": []},
115 "updated_observers": {"added": [], "removed": []},
114 }
116 }
115
117
116 assert_ok(id_, expected, response.body)
118 assert_ok(id_, expected, response.body)
117
119
118 @pytest.mark.backends("git", "hg")
120 @pytest.mark.backends("git", "hg")
119 def test_api_update_change_reviewers(
121 def test_api_update_change_reviewers(
120 self, user_util, pr_util, no_notifications):
122 self, user_util, pr_util, no_notifications):
121 a = user_util.create_user()
123 a = user_util.create_user()
122 b = user_util.create_user()
124 b = user_util.create_user()
123 c = user_util.create_user()
125 c = user_util.create_user()
124 new_reviewers = [
126 new_reviewers = [
125 {'username': b.username,'reasons': ['updated via API'],
127 {'username': b.username, 'reasons': ['updated via API'],
126 'mandatory':False},
128 'mandatory':False},
127 {'username': c.username, 'reasons': ['updated via API'],
129 {'username': c.username, 'reasons': ['updated via API'],
128 'mandatory':False},
130 'mandatory':False},
129 ]
131 ]
130
132
131 added = [b.username, c.username]
133 added = [b.username, c.username]
132 removed = [a.username]
134 removed = [a.username]
133
135
134 pull_request = pr_util.create_pull_request(
136 pull_request = pr_util.create_pull_request(
135 reviewers=[(a.username, ['added via API'], False, [])])
137 reviewers=[(a.username, ['added via API'], False, 'reviewer', [])])
136
138
137 id_, params = build_data(
139 id_, params = build_data(
138 self.apikey, 'update_pull_request',
140 self.apikey, 'update_pull_request',
139 repoid=pull_request.target_repo.repo_name,
141 repoid=pull_request.target_repo.repo_name,
140 pullrequestid=pull_request.pull_request_id,
142 pullrequestid=pull_request.pull_request_id,
141 reviewers=new_reviewers)
143 reviewers=new_reviewers)
142 response = api_call(self.app, params)
144 response = api_call(self.app, params)
143 expected = {
145 expected = {
144 "msg": "Updated pull request `{}`".format(
146 "msg": "Updated pull request `{}`".format(
145 pull_request.pull_request_id),
147 pull_request.pull_request_id),
146 "pull_request": response.json['result']['pull_request'],
148 "pull_request": response.json['result']['pull_request'],
147 "updated_commits": {"added": [], "common": [], "removed": []},
149 "updated_commits": {"added": [], "common": [], "removed": []},
148 "updated_reviewers": {"added": added, "removed": removed},
150 "updated_reviewers": {"added": added, "removed": removed},
151 "updated_observers": {"added": [], "removed": []},
149 }
152 }
150
153
151 assert_ok(id_, expected, response.body)
154 assert_ok(id_, expected, response.body)
152
155
153 @pytest.mark.backends("git", "hg")
156 @pytest.mark.backends("git", "hg")
154 def test_api_update_bad_user_in_reviewers(self, pr_util):
157 def test_api_update_bad_user_in_reviewers(self, pr_util):
155 pull_request = pr_util.create_pull_request()
158 pull_request = pr_util.create_pull_request()
156
159
157 id_, params = build_data(
160 id_, params = build_data(
158 self.apikey, 'update_pull_request',
161 self.apikey, 'update_pull_request',
159 repoid=pull_request.target_repo.repo_name,
162 repoid=pull_request.target_repo.repo_name,
160 pullrequestid=pull_request.pull_request_id,
163 pullrequestid=pull_request.pull_request_id,
161 reviewers=[{'username': 'bad_name'}])
164 reviewers=[{'username': 'bad_name'}])
162 response = api_call(self.app, params)
165 response = api_call(self.app, params)
163
166
164 expected = 'user `bad_name` does not exist'
167 expected = 'user `bad_name` does not exist'
165
168
166 assert_error(id_, expected, response.body)
169 assert_error(id_, expected, response.body)
167
170
168 @pytest.mark.backends("git", "hg")
171 @pytest.mark.backends("git", "hg")
169 def test_api_update_repo_error(self, pr_util):
172 def test_api_update_repo_error(self, pr_util):
170 pull_request = pr_util.create_pull_request()
173 pull_request = pr_util.create_pull_request()
171 id_, params = build_data(
174 id_, params = build_data(
172 self.apikey, 'update_pull_request',
175 self.apikey, 'update_pull_request',
173 repoid='fake',
176 repoid='fake',
174 pullrequestid=pull_request.pull_request_id,
177 pullrequestid=pull_request.pull_request_id,
175 reviewers=[{'username': 'bad_name'}])
178 reviewers=[{'username': 'bad_name'}])
176 response = api_call(self.app, params)
179 response = api_call(self.app, params)
177
180
178 expected = 'repository `fake` does not exist'
181 expected = 'repository `fake` does not exist'
179
182
180 response_json = response.json['error']
183 response_json = response.json['error']
181 assert response_json == expected
184 assert response_json == expected
182
185
183 @pytest.mark.backends("git", "hg")
186 @pytest.mark.backends("git", "hg")
184 def test_api_update_pull_request_error(self, pr_util):
187 def test_api_update_pull_request_error(self, pr_util):
185 pull_request = pr_util.create_pull_request()
188 pull_request = pr_util.create_pull_request()
186
189
187 id_, params = build_data(
190 id_, params = build_data(
188 self.apikey, 'update_pull_request',
191 self.apikey, 'update_pull_request',
189 repoid=pull_request.target_repo.repo_name,
192 repoid=pull_request.target_repo.repo_name,
190 pullrequestid=999999,
193 pullrequestid=999999,
191 reviewers=[{'username': 'bad_name'}])
194 reviewers=[{'username': 'bad_name'}])
192 response = api_call(self.app, params)
195 response = api_call(self.app, params)
193
196
194 expected = 'pull request `999999` does not exist'
197 expected = 'pull request `999999` does not exist'
195 assert_error(id_, expected, response.body)
198 assert_error(id_, expected, response.body)
196
199
197 @pytest.mark.backends("git", "hg")
200 @pytest.mark.backends("git", "hg")
198 def test_api_update_pull_request_no_perms_to_update(
201 def test_api_update_pull_request_no_perms_to_update(
199 self, user_util, pr_util):
202 self, user_util, pr_util):
200 user = user_util.create_user()
203 user = user_util.create_user()
201 pull_request = pr_util.create_pull_request()
204 pull_request = pr_util.create_pull_request()
202
205
203 id_, params = build_data(
206 id_, params = build_data(
204 user.api_key, 'update_pull_request',
207 user.api_key, 'update_pull_request',
205 repoid=pull_request.target_repo.repo_name,
208 repoid=pull_request.target_repo.repo_name,
206 pullrequestid=pull_request.pull_request_id,)
209 pullrequestid=pull_request.pull_request_id,)
207 response = api_call(self.app, params)
210 response = api_call(self.app, params)
208
211
209 expected = ('pull request `%s` update failed, '
212 expected = ('pull request `%s` update failed, '
210 'no permission to update.') % pull_request.pull_request_id
213 'no permission to update.') % pull_request.pull_request_id
211
214
212 assert_error(id_, expected, response.body)
215 assert_error(id_, expected, response.body)
@@ -1,1056 +1,1118 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib import channelstream
29 from rhodecode.lib import channelstream
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.lib.vcs.backends.base import unicode_to_reference
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.db import (
37 Session, ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers)
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
38 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
39 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
40 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
41 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
40
42
41 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
42
44
43
45
44 @jsonrpc_method()
46 @jsonrpc_method()
45 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
47 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
46 merge_state=Optional(False)):
48 merge_state=Optional(False)):
47 """
49 """
48 Get a pull request based on the given ID.
50 Get a pull request based on the given ID.
49
51
50 :param apiuser: This is filled automatically from the |authtoken|.
52 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
53 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
54 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
55 the pull request was opened.
54 :type repoid: str or int
56 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
57 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
58 :type pullrequestid: int
57 :param merge_state: Optional calculate merge state for each repository.
59 :param merge_state: Optional calculate merge state for each repository.
58 This could result in longer time to fetch the data
60 This could result in longer time to fetch the data
59 :type merge_state: bool
61 :type merge_state: bool
60
62
61 Example output:
63 Example output:
62
64
63 .. code-block:: bash
65 .. code-block:: bash
64
66
65 "id": <id_given_in_input>,
67 "id": <id_given_in_input>,
66 "result":
68 "result":
67 {
69 {
68 "pull_request_id": "<pull_request_id>",
70 "pull_request_id": "<pull_request_id>",
69 "url": "<url>",
71 "url": "<url>",
70 "title": "<title>",
72 "title": "<title>",
71 "description": "<description>",
73 "description": "<description>",
72 "status" : "<status>",
74 "status" : "<status>",
73 "created_on": "<date_time_created>",
75 "created_on": "<date_time_created>",
74 "updated_on": "<date_time_updated>",
76 "updated_on": "<date_time_updated>",
75 "versions": "<number_or_versions_of_pr>",
77 "versions": "<number_or_versions_of_pr>",
76 "commit_ids": [
78 "commit_ids": [
77 ...
79 ...
78 "<commit_id>",
80 "<commit_id>",
79 "<commit_id>",
81 "<commit_id>",
80 ...
82 ...
81 ],
83 ],
82 "review_status": "<review_status>",
84 "review_status": "<review_status>",
83 "mergeable": {
85 "mergeable": {
84 "status": "<bool>",
86 "status": "<bool>",
85 "message": "<message>",
87 "message": "<message>",
86 },
88 },
87 "source": {
89 "source": {
88 "clone_url": "<clone_url>",
90 "clone_url": "<clone_url>",
89 "repository": "<repository_name>",
91 "repository": "<repository_name>",
90 "reference":
92 "reference":
91 {
93 {
92 "name": "<name>",
94 "name": "<name>",
93 "type": "<type>",
95 "type": "<type>",
94 "commit_id": "<commit_id>",
96 "commit_id": "<commit_id>",
95 }
97 }
96 },
98 },
97 "target": {
99 "target": {
98 "clone_url": "<clone_url>",
100 "clone_url": "<clone_url>",
99 "repository": "<repository_name>",
101 "repository": "<repository_name>",
100 "reference":
102 "reference":
101 {
103 {
102 "name": "<name>",
104 "name": "<name>",
103 "type": "<type>",
105 "type": "<type>",
104 "commit_id": "<commit_id>",
106 "commit_id": "<commit_id>",
105 }
107 }
106 },
108 },
107 "merge": {
109 "merge": {
108 "clone_url": "<clone_url>",
110 "clone_url": "<clone_url>",
109 "reference":
111 "reference":
110 {
112 {
111 "name": "<name>",
113 "name": "<name>",
112 "type": "<type>",
114 "type": "<type>",
113 "commit_id": "<commit_id>",
115 "commit_id": "<commit_id>",
114 }
116 }
115 },
117 },
116 "author": <user_obj>,
118 "author": <user_obj>,
117 "reviewers": [
119 "reviewers": [
118 ...
120 ...
119 {
121 {
120 "user": "<user_obj>",
122 "user": "<user_obj>",
121 "review_status": "<review_status>",
123 "review_status": "<review_status>",
122 }
124 }
123 ...
125 ...
124 ]
126 ]
125 },
127 },
126 "error": null
128 "error": null
127 """
129 """
128
130
129 pull_request = get_pull_request_or_error(pullrequestid)
131 pull_request = get_pull_request_or_error(pullrequestid)
130 if Optional.extract(repoid):
132 if Optional.extract(repoid):
131 repo = get_repo_or_error(repoid)
133 repo = get_repo_or_error(repoid)
132 else:
134 else:
133 repo = pull_request.target_repo
135 repo = pull_request.target_repo
134
136
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
137 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 raise JSONRPCError('repository `%s` or pull request `%s` '
138 raise JSONRPCError('repository `%s` or pull request `%s` '
137 'does not exist' % (repoid, pullrequestid))
139 'does not exist' % (repoid, pullrequestid))
138
140
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
141 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 # otherwise we can lock the repo on calculation of merge state while update/merge
142 # otherwise we can lock the repo on calculation of merge state while update/merge
141 # is happening.
143 # is happening.
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
144 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
145 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 data = pull_request.get_api_data(with_merge_state=merge_state)
146 data = pull_request.get_api_data(with_merge_state=merge_state)
145 return data
147 return data
146
148
147
149
148 @jsonrpc_method()
150 @jsonrpc_method()
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
151 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 merge_state=Optional(False)):
152 merge_state=Optional(False)):
151 """
153 """
152 Get all pull requests from the repository specified in `repoid`.
154 Get all pull requests from the repository specified in `repoid`.
153
155
154 :param apiuser: This is filled automatically from the |authtoken|.
156 :param apiuser: This is filled automatically from the |authtoken|.
155 :type apiuser: AuthUser
157 :type apiuser: AuthUser
156 :param repoid: Optional repository name or repository ID.
158 :param repoid: Optional repository name or repository ID.
157 :type repoid: str or int
159 :type repoid: str or int
158 :param status: Only return pull requests with the specified status.
160 :param status: Only return pull requests with the specified status.
159 Valid options are.
161 Valid options are.
160 * ``new`` (default)
162 * ``new`` (default)
161 * ``open``
163 * ``open``
162 * ``closed``
164 * ``closed``
163 :type status: str
165 :type status: str
164 :param merge_state: Optional calculate merge state for each repository.
166 :param merge_state: Optional calculate merge state for each repository.
165 This could result in longer time to fetch the data
167 This could result in longer time to fetch the data
166 :type merge_state: bool
168 :type merge_state: bool
167
169
168 Example output:
170 Example output:
169
171
170 .. code-block:: bash
172 .. code-block:: bash
171
173
172 "id": <id_given_in_input>,
174 "id": <id_given_in_input>,
173 "result":
175 "result":
174 [
176 [
175 ...
177 ...
176 {
178 {
177 "pull_request_id": "<pull_request_id>",
179 "pull_request_id": "<pull_request_id>",
178 "url": "<url>",
180 "url": "<url>",
179 "title" : "<title>",
181 "title" : "<title>",
180 "description": "<description>",
182 "description": "<description>",
181 "status": "<status>",
183 "status": "<status>",
182 "created_on": "<date_time_created>",
184 "created_on": "<date_time_created>",
183 "updated_on": "<date_time_updated>",
185 "updated_on": "<date_time_updated>",
184 "commit_ids": [
186 "commit_ids": [
185 ...
187 ...
186 "<commit_id>",
188 "<commit_id>",
187 "<commit_id>",
189 "<commit_id>",
188 ...
190 ...
189 ],
191 ],
190 "review_status": "<review_status>",
192 "review_status": "<review_status>",
191 "mergeable": {
193 "mergeable": {
192 "status": "<bool>",
194 "status": "<bool>",
193 "message: "<message>",
195 "message: "<message>",
194 },
196 },
195 "source": {
197 "source": {
196 "clone_url": "<clone_url>",
198 "clone_url": "<clone_url>",
197 "reference":
199 "reference":
198 {
200 {
199 "name": "<name>",
201 "name": "<name>",
200 "type": "<type>",
202 "type": "<type>",
201 "commit_id": "<commit_id>",
203 "commit_id": "<commit_id>",
202 }
204 }
203 },
205 },
204 "target": {
206 "target": {
205 "clone_url": "<clone_url>",
207 "clone_url": "<clone_url>",
206 "reference":
208 "reference":
207 {
209 {
208 "name": "<name>",
210 "name": "<name>",
209 "type": "<type>",
211 "type": "<type>",
210 "commit_id": "<commit_id>",
212 "commit_id": "<commit_id>",
211 }
213 }
212 },
214 },
213 "merge": {
215 "merge": {
214 "clone_url": "<clone_url>",
216 "clone_url": "<clone_url>",
215 "reference":
217 "reference":
216 {
218 {
217 "name": "<name>",
219 "name": "<name>",
218 "type": "<type>",
220 "type": "<type>",
219 "commit_id": "<commit_id>",
221 "commit_id": "<commit_id>",
220 }
222 }
221 },
223 },
222 "author": <user_obj>,
224 "author": <user_obj>,
223 "reviewers": [
225 "reviewers": [
224 ...
226 ...
225 {
227 {
226 "user": "<user_obj>",
228 "user": "<user_obj>",
227 "review_status": "<review_status>",
229 "review_status": "<review_status>",
228 }
230 }
229 ...
231 ...
230 ]
232 ]
231 }
233 }
232 ...
234 ...
233 ],
235 ],
234 "error": null
236 "error": null
235
237
236 """
238 """
237 repo = get_repo_or_error(repoid)
239 repo = get_repo_or_error(repoid)
238 if not has_superadmin_permission(apiuser):
240 if not has_superadmin_permission(apiuser):
239 _perms = (
241 _perms = (
240 'repository.admin', 'repository.write', 'repository.read',)
242 'repository.admin', 'repository.write', 'repository.read',)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
243 validate_repo_permissions(apiuser, repoid, repo, _perms)
242
244
243 status = Optional.extract(status)
245 status = Optional.extract(status)
244 merge_state = Optional.extract(merge_state, binary=True)
246 merge_state = Optional.extract(merge_state, binary=True)
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
247 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 order_by='id', order_dir='desc')
248 order_by='id', order_dir='desc')
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
249 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 return data
250 return data
249
251
250
252
251 @jsonrpc_method()
253 @jsonrpc_method()
252 def merge_pull_request(
254 def merge_pull_request(
253 request, apiuser, pullrequestid, repoid=Optional(None),
255 request, apiuser, pullrequestid, repoid=Optional(None),
254 userid=Optional(OAttr('apiuser'))):
256 userid=Optional(OAttr('apiuser'))):
255 """
257 """
256 Merge the pull request specified by `pullrequestid` into its target
258 Merge the pull request specified by `pullrequestid` into its target
257 repository.
259 repository.
258
260
259 :param apiuser: This is filled automatically from the |authtoken|.
261 :param apiuser: This is filled automatically from the |authtoken|.
260 :type apiuser: AuthUser
262 :type apiuser: AuthUser
261 :param repoid: Optional, repository name or repository ID of the
263 :param repoid: Optional, repository name or repository ID of the
262 target repository to which the |pr| is to be merged.
264 target repository to which the |pr| is to be merged.
263 :type repoid: str or int
265 :type repoid: str or int
264 :param pullrequestid: ID of the pull request which shall be merged.
266 :param pullrequestid: ID of the pull request which shall be merged.
265 :type pullrequestid: int
267 :type pullrequestid: int
266 :param userid: Merge the pull request as this user.
268 :param userid: Merge the pull request as this user.
267 :type userid: Optional(str or int)
269 :type userid: Optional(str or int)
268
270
269 Example output:
271 Example output:
270
272
271 .. code-block:: bash
273 .. code-block:: bash
272
274
273 "id": <id_given_in_input>,
275 "id": <id_given_in_input>,
274 "result": {
276 "result": {
275 "executed": "<bool>",
277 "executed": "<bool>",
276 "failure_reason": "<int>",
278 "failure_reason": "<int>",
277 "merge_status_message": "<str>",
279 "merge_status_message": "<str>",
278 "merge_commit_id": "<merge_commit_id>",
280 "merge_commit_id": "<merge_commit_id>",
279 "possible": "<bool>",
281 "possible": "<bool>",
280 "merge_ref": {
282 "merge_ref": {
281 "commit_id": "<commit_id>",
283 "commit_id": "<commit_id>",
282 "type": "<type>",
284 "type": "<type>",
283 "name": "<name>"
285 "name": "<name>"
284 }
286 }
285 },
287 },
286 "error": null
288 "error": null
287 """
289 """
288 pull_request = get_pull_request_or_error(pullrequestid)
290 pull_request = get_pull_request_or_error(pullrequestid)
289 if Optional.extract(repoid):
291 if Optional.extract(repoid):
290 repo = get_repo_or_error(repoid)
292 repo = get_repo_or_error(repoid)
291 else:
293 else:
292 repo = pull_request.target_repo
294 repo = pull_request.target_repo
293 auth_user = apiuser
295 auth_user = apiuser
294
296
295 if not isinstance(userid, Optional):
297 if not isinstance(userid, Optional):
296 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
298 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
297 user=apiuser, repo_name=repo.repo_name)
299 user=apiuser, repo_name=repo.repo_name)
298 if has_superadmin_permission(apiuser) or is_repo_admin:
300 if has_superadmin_permission(apiuser) or is_repo_admin:
299 apiuser = get_user_or_error(userid)
301 apiuser = get_user_or_error(userid)
300 auth_user = apiuser.AuthUser()
302 auth_user = apiuser.AuthUser()
301 else:
303 else:
302 raise JSONRPCError('userid is not the same as your user')
304 raise JSONRPCError('userid is not the same as your user')
303
305
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
306 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
305 raise JSONRPCError(
307 raise JSONRPCError(
306 'Operation forbidden because pull request is in state {}, '
308 'Operation forbidden because pull request is in state {}, '
307 'only state {} is allowed.'.format(
309 'only state {} is allowed.'.format(
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
310 pull_request.pull_request_state, PullRequest.STATE_CREATED))
309
311
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
312 with pull_request.set_state(PullRequest.STATE_UPDATING):
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
313 check = MergeCheck.validate(pull_request, auth_user=auth_user,
312 translator=request.translate)
314 translator=request.translate)
313 merge_possible = not check.failed
315 merge_possible = not check.failed
314
316
315 if not merge_possible:
317 if not merge_possible:
316 error_messages = []
318 error_messages = []
317 for err_type, error_msg in check.errors:
319 for err_type, error_msg in check.errors:
318 error_msg = request.translate(error_msg)
320 error_msg = request.translate(error_msg)
319 error_messages.append(error_msg)
321 error_messages.append(error_msg)
320
322
321 reasons = ','.join(error_messages)
323 reasons = ','.join(error_messages)
322 raise JSONRPCError(
324 raise JSONRPCError(
323 'merge not possible for following reasons: {}'.format(reasons))
325 'merge not possible for following reasons: {}'.format(reasons))
324
326
325 target_repo = pull_request.target_repo
327 target_repo = pull_request.target_repo
326 extras = vcs_operation_context(
328 extras = vcs_operation_context(
327 request.environ, repo_name=target_repo.repo_name,
329 request.environ, repo_name=target_repo.repo_name,
328 username=auth_user.username, action='push',
330 username=auth_user.username, action='push',
329 scm=target_repo.repo_type)
331 scm=target_repo.repo_type)
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
332 with pull_request.set_state(PullRequest.STATE_UPDATING):
331 merge_response = PullRequestModel().merge_repo(
333 merge_response = PullRequestModel().merge_repo(
332 pull_request, apiuser, extras=extras)
334 pull_request, apiuser, extras=extras)
333 if merge_response.executed:
335 if merge_response.executed:
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
336 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
335
337
336 Session().commit()
338 Session().commit()
337
339
338 # In previous versions the merge response directly contained the merge
340 # In previous versions the merge response directly contained the merge
339 # commit id. It is now contained in the merge reference object. To be
341 # commit id. It is now contained in the merge reference object. To be
340 # backwards compatible we have to extract it again.
342 # backwards compatible we have to extract it again.
341 merge_response = merge_response.asdict()
343 merge_response = merge_response.asdict()
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
344 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
343
345
344 return merge_response
346 return merge_response
345
347
346
348
347 @jsonrpc_method()
349 @jsonrpc_method()
348 def get_pull_request_comments(
350 def get_pull_request_comments(
349 request, apiuser, pullrequestid, repoid=Optional(None)):
351 request, apiuser, pullrequestid, repoid=Optional(None)):
350 """
352 """
351 Get all comments of pull request specified with the `pullrequestid`
353 Get all comments of pull request specified with the `pullrequestid`
352
354
353 :param apiuser: This is filled automatically from the |authtoken|.
355 :param apiuser: This is filled automatically from the |authtoken|.
354 :type apiuser: AuthUser
356 :type apiuser: AuthUser
355 :param repoid: Optional repository name or repository ID.
357 :param repoid: Optional repository name or repository ID.
356 :type repoid: str or int
358 :type repoid: str or int
357 :param pullrequestid: The pull request ID.
359 :param pullrequestid: The pull request ID.
358 :type pullrequestid: int
360 :type pullrequestid: int
359
361
360 Example output:
362 Example output:
361
363
362 .. code-block:: bash
364 .. code-block:: bash
363
365
364 id : <id_given_in_input>
366 id : <id_given_in_input>
365 result : [
367 result : [
366 {
368 {
367 "comment_author": {
369 "comment_author": {
368 "active": true,
370 "active": true,
369 "full_name_or_username": "Tom Gore",
371 "full_name_or_username": "Tom Gore",
370 "username": "admin"
372 "username": "admin"
371 },
373 },
372 "comment_created_on": "2017-01-02T18:43:45.533",
374 "comment_created_on": "2017-01-02T18:43:45.533",
373 "comment_f_path": null,
375 "comment_f_path": null,
374 "comment_id": 25,
376 "comment_id": 25,
375 "comment_lineno": null,
377 "comment_lineno": null,
376 "comment_status": {
378 "comment_status": {
377 "status": "under_review",
379 "status": "under_review",
378 "status_lbl": "Under Review"
380 "status_lbl": "Under Review"
379 },
381 },
380 "comment_text": "Example text",
382 "comment_text": "Example text",
381 "comment_type": null,
383 "comment_type": null,
382 "comment_last_version: 0,
384 "comment_last_version: 0,
383 "pull_request_version": null,
385 "pull_request_version": null,
384 "comment_commit_id": None,
386 "comment_commit_id": None,
385 "comment_pull_request_id": <pull_request_id>
387 "comment_pull_request_id": <pull_request_id>
386 }
388 }
387 ],
389 ],
388 error : null
390 error : null
389 """
391 """
390
392
391 pull_request = get_pull_request_or_error(pullrequestid)
393 pull_request = get_pull_request_or_error(pullrequestid)
392 if Optional.extract(repoid):
394 if Optional.extract(repoid):
393 repo = get_repo_or_error(repoid)
395 repo = get_repo_or_error(repoid)
394 else:
396 else:
395 repo = pull_request.target_repo
397 repo = pull_request.target_repo
396
398
397 if not PullRequestModel().check_user_read(
399 if not PullRequestModel().check_user_read(
398 pull_request, apiuser, api=True):
400 pull_request, apiuser, api=True):
399 raise JSONRPCError('repository `%s` or pull request `%s` '
401 raise JSONRPCError('repository `%s` or pull request `%s` '
400 'does not exist' % (repoid, pullrequestid))
402 'does not exist' % (repoid, pullrequestid))
401
403
402 (pull_request_latest,
404 (pull_request_latest,
403 pull_request_at_ver,
405 pull_request_at_ver,
404 pull_request_display_obj,
406 pull_request_display_obj,
405 at_version) = PullRequestModel().get_pr_version(
407 at_version) = PullRequestModel().get_pr_version(
406 pull_request.pull_request_id, version=None)
408 pull_request.pull_request_id, version=None)
407
409
408 versions = pull_request_display_obj.versions()
410 versions = pull_request_display_obj.versions()
409 ver_map = {
411 ver_map = {
410 ver.pull_request_version_id: cnt
412 ver.pull_request_version_id: cnt
411 for cnt, ver in enumerate(versions, 1)
413 for cnt, ver in enumerate(versions, 1)
412 }
414 }
413
415
414 # GENERAL COMMENTS with versions #
416 # GENERAL COMMENTS with versions #
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
417 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
416 q = q.order_by(ChangesetComment.comment_id.asc())
418 q = q.order_by(ChangesetComment.comment_id.asc())
417 general_comments = q.all()
419 general_comments = q.all()
418
420
419 # INLINE COMMENTS with versions #
421 # INLINE COMMENTS with versions #
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
422 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
421 q = q.order_by(ChangesetComment.comment_id.asc())
423 q = q.order_by(ChangesetComment.comment_id.asc())
422 inline_comments = q.all()
424 inline_comments = q.all()
423
425
424 data = []
426 data = []
425 for comment in inline_comments + general_comments:
427 for comment in inline_comments + general_comments:
426 full_data = comment.get_api_data()
428 full_data = comment.get_api_data()
427 pr_version_id = None
429 pr_version_id = None
428 if comment.pull_request_version_id:
430 if comment.pull_request_version_id:
429 pr_version_id = 'v{}'.format(
431 pr_version_id = 'v{}'.format(
430 ver_map[comment.pull_request_version_id])
432 ver_map[comment.pull_request_version_id])
431
433
432 # sanitize some entries
434 # sanitize some entries
433
435
434 full_data['pull_request_version'] = pr_version_id
436 full_data['pull_request_version'] = pr_version_id
435 full_data['comment_author'] = {
437 full_data['comment_author'] = {
436 'username': full_data['comment_author'].username,
438 'username': full_data['comment_author'].username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
439 'full_name_or_username': full_data['comment_author'].full_name_or_username,
438 'active': full_data['comment_author'].active,
440 'active': full_data['comment_author'].active,
439 }
441 }
440
442
441 if full_data['comment_status']:
443 if full_data['comment_status']:
442 full_data['comment_status'] = {
444 full_data['comment_status'] = {
443 'status': full_data['comment_status'][0].status,
445 'status': full_data['comment_status'][0].status,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
446 'status_lbl': full_data['comment_status'][0].status_lbl,
445 }
447 }
446 else:
448 else:
447 full_data['comment_status'] = {}
449 full_data['comment_status'] = {}
448
450
449 data.append(full_data)
451 data.append(full_data)
450 return data
452 return data
451
453
452
454
453 @jsonrpc_method()
455 @jsonrpc_method()
454 def comment_pull_request(
456 def comment_pull_request(
455 request, apiuser, pullrequestid, repoid=Optional(None),
457 request, apiuser, pullrequestid, repoid=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
458 message=Optional(None), commit_id=Optional(None), status=Optional(None),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
459 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
460 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
461 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
460 """
462 """
461 Comment on the pull request specified with the `pullrequestid`,
463 Comment on the pull request specified with the `pullrequestid`,
462 in the |repo| specified by the `repoid`, and optionally change the
464 in the |repo| specified by the `repoid`, and optionally change the
463 review status.
465 review status.
464
466
465 :param apiuser: This is filled automatically from the |authtoken|.
467 :param apiuser: This is filled automatically from the |authtoken|.
466 :type apiuser: AuthUser
468 :type apiuser: AuthUser
467 :param repoid: Optional repository name or repository ID.
469 :param repoid: Optional repository name or repository ID.
468 :type repoid: str or int
470 :type repoid: str or int
469 :param pullrequestid: The pull request ID.
471 :param pullrequestid: The pull request ID.
470 :type pullrequestid: int
472 :type pullrequestid: int
471 :param commit_id: Specify the commit_id for which to set a comment. If
473 :param commit_id: Specify the commit_id for which to set a comment. If
472 given commit_id is different than latest in the PR status
474 given commit_id is different than latest in the PR status
473 change won't be performed.
475 change won't be performed.
474 :type commit_id: str
476 :type commit_id: str
475 :param message: The text content of the comment.
477 :param message: The text content of the comment.
476 :type message: str
478 :type message: str
477 :param status: (**Optional**) Set the approval status of the pull
479 :param status: (**Optional**) Set the approval status of the pull
478 request. One of: 'not_reviewed', 'approved', 'rejected',
480 request. One of: 'not_reviewed', 'approved', 'rejected',
479 'under_review'
481 'under_review'
480 :type status: str
482 :type status: str
481 :param comment_type: Comment type, one of: 'note', 'todo'
483 :param comment_type: Comment type, one of: 'note', 'todo'
482 :type comment_type: Optional(str), default: 'note'
484 :type comment_type: Optional(str), default: 'note'
483 :param resolves_comment_id: id of comment which this one will resolve
485 :param resolves_comment_id: id of comment which this one will resolve
484 :type resolves_comment_id: Optional(int)
486 :type resolves_comment_id: Optional(int)
485 :param extra_recipients: list of user ids or usernames to add
487 :param extra_recipients: list of user ids or usernames to add
486 notifications for this comment. Acts like a CC for notification
488 notifications for this comment. Acts like a CC for notification
487 :type extra_recipients: Optional(list)
489 :type extra_recipients: Optional(list)
488 :param userid: Comment on the pull request as this user
490 :param userid: Comment on the pull request as this user
489 :type userid: Optional(str or int)
491 :type userid: Optional(str or int)
490 :param send_email: Define if this comment should also send email notification
492 :param send_email: Define if this comment should also send email notification
491 :type send_email: Optional(bool)
493 :type send_email: Optional(bool)
492
494
493 Example output:
495 Example output:
494
496
495 .. code-block:: bash
497 .. code-block:: bash
496
498
497 id : <id_given_in_input>
499 id : <id_given_in_input>
498 result : {
500 result : {
499 "pull_request_id": "<Integer>",
501 "pull_request_id": "<Integer>",
500 "comment_id": "<Integer>",
502 "comment_id": "<Integer>",
501 "status": {"given": <given_status>,
503 "status": {"given": <given_status>,
502 "was_changed": <bool status_was_actually_changed> },
504 "was_changed": <bool status_was_actually_changed> },
503 },
505 },
504 error : null
506 error : null
505 """
507 """
506 _ = request.translate
508 _ = request.translate
507
509
508 pull_request = get_pull_request_or_error(pullrequestid)
510 pull_request = get_pull_request_or_error(pullrequestid)
509 if Optional.extract(repoid):
511 if Optional.extract(repoid):
510 repo = get_repo_or_error(repoid)
512 repo = get_repo_or_error(repoid)
511 else:
513 else:
512 repo = pull_request.target_repo
514 repo = pull_request.target_repo
513
515
514 db_repo_name = repo.repo_name
516 db_repo_name = repo.repo_name
515 auth_user = apiuser
517 auth_user = apiuser
516 if not isinstance(userid, Optional):
518 if not isinstance(userid, Optional):
517 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
519 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
518 user=apiuser, repo_name=db_repo_name)
520 user=apiuser, repo_name=db_repo_name)
519 if has_superadmin_permission(apiuser) or is_repo_admin:
521 if has_superadmin_permission(apiuser) or is_repo_admin:
520 apiuser = get_user_or_error(userid)
522 apiuser = get_user_or_error(userid)
521 auth_user = apiuser.AuthUser()
523 auth_user = apiuser.AuthUser()
522 else:
524 else:
523 raise JSONRPCError('userid is not the same as your user')
525 raise JSONRPCError('userid is not the same as your user')
524
526
525 if pull_request.is_closed():
527 if pull_request.is_closed():
526 raise JSONRPCError(
528 raise JSONRPCError(
527 'pull request `%s` comment failed, pull request is closed' % (
529 'pull request `%s` comment failed, pull request is closed' % (
528 pullrequestid,))
530 pullrequestid,))
529
531
530 if not PullRequestModel().check_user_read(
532 if not PullRequestModel().check_user_read(
531 pull_request, apiuser, api=True):
533 pull_request, apiuser, api=True):
532 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
534 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
533 message = Optional.extract(message)
535 message = Optional.extract(message)
534 status = Optional.extract(status)
536 status = Optional.extract(status)
535 commit_id = Optional.extract(commit_id)
537 commit_id = Optional.extract(commit_id)
536 comment_type = Optional.extract(comment_type)
538 comment_type = Optional.extract(comment_type)
537 resolves_comment_id = Optional.extract(resolves_comment_id)
539 resolves_comment_id = Optional.extract(resolves_comment_id)
538 extra_recipients = Optional.extract(extra_recipients)
540 extra_recipients = Optional.extract(extra_recipients)
539 send_email = Optional.extract(send_email, binary=True)
541 send_email = Optional.extract(send_email, binary=True)
540
542
541 if not message and not status:
543 if not message and not status:
542 raise JSONRPCError(
544 raise JSONRPCError(
543 'Both message and status parameters are missing. '
545 'Both message and status parameters are missing. '
544 'At least one is required.')
546 'At least one is required.')
545
547
546 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
548 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
547 status is not None):
549 status is not None):
548 raise JSONRPCError('Unknown comment status: `%s`' % status)
550 raise JSONRPCError('Unknown comment status: `%s`' % status)
549
551
550 if commit_id and commit_id not in pull_request.revisions:
552 if commit_id and commit_id not in pull_request.revisions:
551 raise JSONRPCError(
553 raise JSONRPCError(
552 'Invalid commit_id `%s` for this pull request.' % commit_id)
554 'Invalid commit_id `%s` for this pull request.' % commit_id)
553
555
554 allowed_to_change_status = PullRequestModel().check_user_change_status(
556 allowed_to_change_status = PullRequestModel().check_user_change_status(
555 pull_request, apiuser)
557 pull_request, apiuser)
556
558
557 # if commit_id is passed re-validated if user is allowed to change status
559 # if commit_id is passed re-validated if user is allowed to change status
558 # based on latest commit_id from the PR
560 # based on latest commit_id from the PR
559 if commit_id:
561 if commit_id:
560 commit_idx = pull_request.revisions.index(commit_id)
562 commit_idx = pull_request.revisions.index(commit_id)
561 if commit_idx != 0:
563 if commit_idx != 0:
562 allowed_to_change_status = False
564 allowed_to_change_status = False
563
565
564 if resolves_comment_id:
566 if resolves_comment_id:
565 comment = ChangesetComment.get(resolves_comment_id)
567 comment = ChangesetComment.get(resolves_comment_id)
566 if not comment:
568 if not comment:
567 raise JSONRPCError(
569 raise JSONRPCError(
568 'Invalid resolves_comment_id `%s` for this pull request.'
570 'Invalid resolves_comment_id `%s` for this pull request.'
569 % resolves_comment_id)
571 % resolves_comment_id)
570 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
572 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
571 raise JSONRPCError(
573 raise JSONRPCError(
572 'Comment `%s` is wrong type for setting status to resolved.'
574 'Comment `%s` is wrong type for setting status to resolved.'
573 % resolves_comment_id)
575 % resolves_comment_id)
574
576
575 text = message
577 text = message
576 status_label = ChangesetStatus.get_status_lbl(status)
578 status_label = ChangesetStatus.get_status_lbl(status)
577 if status and allowed_to_change_status:
579 if status and allowed_to_change_status:
578 st_message = ('Status change %(transition_icon)s %(status)s'
580 st_message = ('Status change %(transition_icon)s %(status)s'
579 % {'transition_icon': '>', 'status': status_label})
581 % {'transition_icon': '>', 'status': status_label})
580 text = message or st_message
582 text = message or st_message
581
583
582 rc_config = SettingsModel().get_all_settings()
584 rc_config = SettingsModel().get_all_settings()
583 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
585 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
584
586
585 status_change = status and allowed_to_change_status
587 status_change = status and allowed_to_change_status
586 comment = CommentsModel().create(
588 comment = CommentsModel().create(
587 text=text,
589 text=text,
588 repo=pull_request.target_repo.repo_id,
590 repo=pull_request.target_repo.repo_id,
589 user=apiuser.user_id,
591 user=apiuser.user_id,
590 pull_request=pull_request.pull_request_id,
592 pull_request=pull_request.pull_request_id,
591 f_path=None,
593 f_path=None,
592 line_no=None,
594 line_no=None,
593 status_change=(status_label if status_change else None),
595 status_change=(status_label if status_change else None),
594 status_change_type=(status if status_change else None),
596 status_change_type=(status if status_change else None),
595 closing_pr=False,
597 closing_pr=False,
596 renderer=renderer,
598 renderer=renderer,
597 comment_type=comment_type,
599 comment_type=comment_type,
598 resolves_comment_id=resolves_comment_id,
600 resolves_comment_id=resolves_comment_id,
599 auth_user=auth_user,
601 auth_user=auth_user,
600 extra_recipients=extra_recipients,
602 extra_recipients=extra_recipients,
601 send_email=send_email
603 send_email=send_email
602 )
604 )
603 is_inline = bool(comment.f_path and comment.line_no)
605 is_inline = comment.is_inline
604
606
605 if allowed_to_change_status and status:
607 if allowed_to_change_status and status:
606 old_calculated_status = pull_request.calculated_review_status()
608 old_calculated_status = pull_request.calculated_review_status()
607 ChangesetStatusModel().set_status(
609 ChangesetStatusModel().set_status(
608 pull_request.target_repo.repo_id,
610 pull_request.target_repo.repo_id,
609 status,
611 status,
610 apiuser.user_id,
612 apiuser.user_id,
611 comment,
613 comment,
612 pull_request=pull_request.pull_request_id
614 pull_request=pull_request.pull_request_id
613 )
615 )
614 Session().flush()
616 Session().flush()
615
617
616 Session().commit()
618 Session().commit()
617
619
618 PullRequestModel().trigger_pull_request_hook(
620 PullRequestModel().trigger_pull_request_hook(
619 pull_request, apiuser, 'comment',
621 pull_request, apiuser, 'comment',
620 data={'comment': comment})
622 data={'comment': comment})
621
623
622 if allowed_to_change_status and status:
624 if allowed_to_change_status and status:
623 # we now calculate the status of pull request, and based on that
625 # we now calculate the status of pull request, and based on that
624 # calculation we set the commits status
626 # calculation we set the commits status
625 calculated_status = pull_request.calculated_review_status()
627 calculated_status = pull_request.calculated_review_status()
626 if old_calculated_status != calculated_status:
628 if old_calculated_status != calculated_status:
627 PullRequestModel().trigger_pull_request_hook(
629 PullRequestModel().trigger_pull_request_hook(
628 pull_request, apiuser, 'review_status_change',
630 pull_request, apiuser, 'review_status_change',
629 data={'status': calculated_status})
631 data={'status': calculated_status})
630
632
631 data = {
633 data = {
632 'pull_request_id': pull_request.pull_request_id,
634 'pull_request_id': pull_request.pull_request_id,
633 'comment_id': comment.comment_id if comment else None,
635 'comment_id': comment.comment_id if comment else None,
634 'status': {'given': status, 'was_changed': status_change},
636 'status': {'given': status, 'was_changed': status_change},
635 }
637 }
636
638
637 comment_broadcast_channel = channelstream.comment_channel(
639 comment_broadcast_channel = channelstream.comment_channel(
638 db_repo_name, pull_request_obj=pull_request)
640 db_repo_name, pull_request_obj=pull_request)
639
641
640 comment_data = data
642 comment_data = data
641 comment_type = 'inline' if is_inline else 'general'
643 comment_type = 'inline' if is_inline else 'general'
642 channelstream.comment_channelstream_push(
644 channelstream.comment_channelstream_push(
643 request, comment_broadcast_channel, apiuser,
645 request, comment_broadcast_channel, apiuser,
644 _('posted a new {} comment').format(comment_type),
646 _('posted a new {} comment').format(comment_type),
645 comment_data=comment_data)
647 comment_data=comment_data)
646
648
647 return data
649 return data
648
650
651 def _reviewers_validation(obj_list):
652 schema = ReviewerListSchema()
653 try:
654 reviewer_objects = schema.deserialize(obj_list)
655 except Invalid as err:
656 raise JSONRPCValidationError(colander_exc=err)
657
658 # validate users
659 for reviewer_object in reviewer_objects:
660 user = get_user_or_error(reviewer_object['username'])
661 reviewer_object['user_id'] = user.user_id
662 return reviewer_objects
663
649
664
650 @jsonrpc_method()
665 @jsonrpc_method()
651 def create_pull_request(
666 def create_pull_request(
652 request, apiuser, source_repo, target_repo, source_ref, target_ref,
667 request, apiuser, source_repo, target_repo, source_ref, target_ref,
653 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
668 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
654 description_renderer=Optional(''), reviewers=Optional(None)):
669 description_renderer=Optional(''),
670 reviewers=Optional(None), observers=Optional(None)):
655 """
671 """
656 Creates a new pull request.
672 Creates a new pull request.
657
673
658 Accepts refs in the following formats:
674 Accepts refs in the following formats:
659
675
660 * branch:<branch_name>:<sha>
676 * branch:<branch_name>:<sha>
661 * branch:<branch_name>
677 * branch:<branch_name>
662 * bookmark:<bookmark_name>:<sha> (Mercurial only)
678 * bookmark:<bookmark_name>:<sha> (Mercurial only)
663 * bookmark:<bookmark_name> (Mercurial only)
679 * bookmark:<bookmark_name> (Mercurial only)
664
680
665 :param apiuser: This is filled automatically from the |authtoken|.
681 :param apiuser: This is filled automatically from the |authtoken|.
666 :type apiuser: AuthUser
682 :type apiuser: AuthUser
667 :param source_repo: Set the source repository name.
683 :param source_repo: Set the source repository name.
668 :type source_repo: str
684 :type source_repo: str
669 :param target_repo: Set the target repository name.
685 :param target_repo: Set the target repository name.
670 :type target_repo: str
686 :type target_repo: str
671 :param source_ref: Set the source ref name.
687 :param source_ref: Set the source ref name.
672 :type source_ref: str
688 :type source_ref: str
673 :param target_ref: Set the target ref name.
689 :param target_ref: Set the target ref name.
674 :type target_ref: str
690 :type target_ref: str
675 :param owner: user_id or username
691 :param owner: user_id or username
676 :type owner: Optional(str)
692 :type owner: Optional(str)
677 :param title: Optionally Set the pull request title, it's generated otherwise
693 :param title: Optionally Set the pull request title, it's generated otherwise
678 :type title: str
694 :type title: str
679 :param description: Set the pull request description.
695 :param description: Set the pull request description.
680 :type description: Optional(str)
696 :type description: Optional(str)
681 :type description_renderer: Optional(str)
697 :type description_renderer: Optional(str)
682 :param description_renderer: Set pull request renderer for the description.
698 :param description_renderer: Set pull request renderer for the description.
683 It should be 'rst', 'markdown' or 'plain'. If not give default
699 It should be 'rst', 'markdown' or 'plain'. If not give default
684 system renderer will be used
700 system renderer will be used
685 :param reviewers: Set the new pull request reviewers list.
701 :param reviewers: Set the new pull request reviewers list.
686 Reviewer defined by review rules will be added automatically to the
702 Reviewer defined by review rules will be added automatically to the
687 defined list.
703 defined list.
688 :type reviewers: Optional(list)
704 :type reviewers: Optional(list)
689 Accepts username strings or objects of the format:
705 Accepts username strings or objects of the format:
690
706
691 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
707 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
708 :param observers: Set the new pull request observers list.
709 Reviewer defined by review rules will be added automatically to the
710 defined list. This feature is only available in RhodeCode EE
711 :type observers: Optional(list)
712 Accepts username strings or objects of the format:
713
714 [{'username': 'nick', 'reasons': ['original author']}]
692 """
715 """
693
716
694 source_db_repo = get_repo_or_error(source_repo)
717 source_db_repo = get_repo_or_error(source_repo)
695 target_db_repo = get_repo_or_error(target_repo)
718 target_db_repo = get_repo_or_error(target_repo)
696 if not has_superadmin_permission(apiuser):
719 if not has_superadmin_permission(apiuser):
697 _perms = ('repository.admin', 'repository.write', 'repository.read',)
720 _perms = ('repository.admin', 'repository.write', 'repository.read',)
698 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
721 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
699
722
700 owner = validate_set_owner_permissions(apiuser, owner)
723 owner = validate_set_owner_permissions(apiuser, owner)
701
724
702 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
725 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
703 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
726 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
704
727
705 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
728 get_commit_or_error(full_source_ref, source_db_repo)
706 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
729 get_commit_or_error(full_target_ref, target_db_repo)
707
730
708 reviewer_objects = Optional.extract(reviewers) or []
731 reviewer_objects = Optional.extract(reviewers) or []
732 observer_objects = Optional.extract(observers) or []
709
733
710 # serialize and validate passed in given reviewers
734 # serialize and validate passed in given reviewers
711 if reviewer_objects:
735 if reviewer_objects:
712 schema = ReviewerListSchema()
736 reviewer_objects = _reviewers_validation(reviewer_objects)
713 try:
714 reviewer_objects = schema.deserialize(reviewer_objects)
715 except Invalid as err:
716 raise JSONRPCValidationError(colander_exc=err)
717
737
718 # validate users
738 if observer_objects:
719 for reviewer_object in reviewer_objects:
739 observer_objects = _reviewers_validation(reviewer_objects)
720 user = get_user_or_error(reviewer_object['username'])
721 reviewer_object['user_id'] = user.user_id
722
740
723 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
741 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
724 PullRequestModel().get_reviewer_functions()
742 PullRequestModel().get_reviewer_functions()
725
743
744 source_ref_obj = unicode_to_reference(full_source_ref)
745 target_ref_obj = unicode_to_reference(full_target_ref)
746
726 # recalculate reviewers logic, to make sure we can validate this
747 # recalculate reviewers logic, to make sure we can validate this
727 default_reviewers_data = get_default_reviewers_data(
748 default_reviewers_data = get_default_reviewers_data(
728 owner,
749 owner,
729 source_repo,
750 source_db_repo,
730 Reference(source_type, source_name, source_commit_id),
751 source_ref_obj,
731 target_repo,
752 target_db_repo,
732 Reference(target_type, target_name, target_commit_id)
753 target_ref_obj,
733 )
754 )
734
755
735 # now MERGE our given with the calculated
756 # now MERGE our given with the calculated from the default rules
736 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
757 just_reviewers = [
758 x for x in default_reviewers_data['reviewers']
759 if x['role'] == PullRequestReviewers.ROLE_REVIEWER]
760 reviewer_objects = just_reviewers + reviewer_objects
737
761
738 try:
762 try:
739 reviewers = validate_default_reviewers(
763 reviewers = validate_default_reviewers(
740 reviewer_objects, default_reviewers_data)
764 reviewer_objects, default_reviewers_data)
741 except ValueError as e:
765 except ValueError as e:
742 raise JSONRPCError('Reviewers Validation: {}'.format(e))
766 raise JSONRPCError('Reviewers Validation: {}'.format(e))
743
767
768 # now MERGE our given with the calculated from the default rules
769 just_observers = [
770 x for x in default_reviewers_data['reviewers']
771 if x['role'] == PullRequestReviewers.ROLE_OBSERVER]
772 observer_objects = just_observers + observer_objects
773
774 try:
775 observers = validate_observers(
776 observer_objects, default_reviewers_data)
777 except ValueError as e:
778 raise JSONRPCError('Observer Validation: {}'.format(e))
779
744 title = Optional.extract(title)
780 title = Optional.extract(title)
745 if not title:
781 if not title:
746 title_source_ref = source_ref.split(':', 2)[1]
782 title_source_ref = source_ref_obj.name
747 title = PullRequestModel().generate_pullrequest_title(
783 title = PullRequestModel().generate_pullrequest_title(
748 source=source_repo,
784 source=source_repo,
749 source_ref=title_source_ref,
785 source_ref=title_source_ref,
750 target=target_repo
786 target=target_repo
751 )
787 )
752
788
753 diff_info = default_reviewers_data['diff_info']
789 diff_info = default_reviewers_data['diff_info']
754 common_ancestor_id = diff_info['ancestor']
790 common_ancestor_id = diff_info['ancestor']
755 commits = diff_info['commits']
791 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
792 commits = [commit['commit_id'] for commit in reversed(diff_info['commits'])]
756
793
757 if not common_ancestor_id:
794 if not common_ancestor_id:
758 raise JSONRPCError('no common ancestor found')
795 raise JSONRPCError('no common ancestor found between specified references')
759
796
760 if not commits:
797 if not commits:
761 raise JSONRPCError('no commits found')
798 raise JSONRPCError('no commits found for merge between specified references')
762
763 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
764 revisions = [commit.raw_id for commit in reversed(commits)]
765
799
766 # recalculate target ref based on ancestor
800 # recalculate target ref based on ancestor
767 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
801 full_target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, common_ancestor_id))
768 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
769
802
770 # fetch renderer, if set fallback to plain in case of PR
803 # fetch renderer, if set fallback to plain in case of PR
771 rc_config = SettingsModel().get_all_settings()
804 rc_config = SettingsModel().get_all_settings()
772 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
805 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
773 description = Optional.extract(description)
806 description = Optional.extract(description)
774 description_renderer = Optional.extract(description_renderer) or default_system_renderer
807 description_renderer = Optional.extract(description_renderer) or default_system_renderer
775
808
776 pull_request = PullRequestModel().create(
809 pull_request = PullRequestModel().create(
777 created_by=owner.user_id,
810 created_by=owner.user_id,
778 source_repo=source_repo,
811 source_repo=source_repo,
779 source_ref=full_source_ref,
812 source_ref=full_source_ref,
780 target_repo=target_repo,
813 target_repo=target_repo,
781 target_ref=full_target_ref,
814 target_ref=full_target_ref,
782 common_ancestor_id=common_ancestor_id,
815 common_ancestor_id=common_ancestor_id,
783 revisions=revisions,
816 revisions=commits,
784 reviewers=reviewers,
817 reviewers=reviewers,
818 observers=observers,
785 title=title,
819 title=title,
786 description=description,
820 description=description,
787 description_renderer=description_renderer,
821 description_renderer=description_renderer,
788 reviewer_data=default_reviewers_data,
822 reviewer_data=default_reviewers_data,
789 auth_user=apiuser
823 auth_user=apiuser
790 )
824 )
791
825
792 Session().commit()
826 Session().commit()
793 data = {
827 data = {
794 'msg': 'Created new pull request `{}`'.format(title),
828 'msg': 'Created new pull request `{}`'.format(title),
795 'pull_request_id': pull_request.pull_request_id,
829 'pull_request_id': pull_request.pull_request_id,
796 }
830 }
797 return data
831 return data
798
832
799
833
800 @jsonrpc_method()
834 @jsonrpc_method()
801 def update_pull_request(
835 def update_pull_request(
802 request, apiuser, pullrequestid, repoid=Optional(None),
836 request, apiuser, pullrequestid, repoid=Optional(None),
803 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
837 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
804 reviewers=Optional(None), update_commits=Optional(None)):
838 reviewers=Optional(None), observers=Optional(None), update_commits=Optional(None)):
805 """
839 """
806 Updates a pull request.
840 Updates a pull request.
807
841
808 :param apiuser: This is filled automatically from the |authtoken|.
842 :param apiuser: This is filled automatically from the |authtoken|.
809 :type apiuser: AuthUser
843 :type apiuser: AuthUser
810 :param repoid: Optional repository name or repository ID.
844 :param repoid: Optional repository name or repository ID.
811 :type repoid: str or int
845 :type repoid: str or int
812 :param pullrequestid: The pull request ID.
846 :param pullrequestid: The pull request ID.
813 :type pullrequestid: int
847 :type pullrequestid: int
814 :param title: Set the pull request title.
848 :param title: Set the pull request title.
815 :type title: str
849 :type title: str
816 :param description: Update pull request description.
850 :param description: Update pull request description.
817 :type description: Optional(str)
851 :type description: Optional(str)
818 :type description_renderer: Optional(str)
852 :type description_renderer: Optional(str)
819 :param description_renderer: Update pull request renderer for the description.
853 :param description_renderer: Update pull request renderer for the description.
820 It should be 'rst', 'markdown' or 'plain'
854 It should be 'rst', 'markdown' or 'plain'
821 :param reviewers: Update pull request reviewers list with new value.
855 :param reviewers: Update pull request reviewers list with new value.
822 :type reviewers: Optional(list)
856 :type reviewers: Optional(list)
823 Accepts username strings or objects of the format:
857 Accepts username strings or objects of the format:
824
858
825 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
859 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
860 :param observers: Update pull request observers list with new value.
861 :type observers: Optional(list)
862 Accepts username strings or objects of the format:
826
863
864 [{'username': 'nick', 'reasons': ['should be aware about this PR']}]
827 :param update_commits: Trigger update of commits for this pull request
865 :param update_commits: Trigger update of commits for this pull request
828 :type: update_commits: Optional(bool)
866 :type: update_commits: Optional(bool)
829
867
830 Example output:
868 Example output:
831
869
832 .. code-block:: bash
870 .. code-block:: bash
833
871
834 id : <id_given_in_input>
872 id : <id_given_in_input>
835 result : {
873 result : {
836 "msg": "Updated pull request `63`",
874 "msg": "Updated pull request `63`",
837 "pull_request": <pull_request_object>,
875 "pull_request": <pull_request_object>,
838 "updated_reviewers": {
876 "updated_reviewers": {
839 "added": [
877 "added": [
840 "username"
878 "username"
841 ],
879 ],
842 "removed": []
880 "removed": []
843 },
881 },
882 "updated_observers": {
883 "added": [
884 "username"
885 ],
886 "removed": []
887 },
844 "updated_commits": {
888 "updated_commits": {
845 "added": [
889 "added": [
846 "<sha1_hash>"
890 "<sha1_hash>"
847 ],
891 ],
848 "common": [
892 "common": [
849 "<sha1_hash>",
893 "<sha1_hash>",
850 "<sha1_hash>",
894 "<sha1_hash>",
851 ],
895 ],
852 "removed": []
896 "removed": []
853 }
897 }
854 }
898 }
855 error : null
899 error : null
856 """
900 """
857
901
858 pull_request = get_pull_request_or_error(pullrequestid)
902 pull_request = get_pull_request_or_error(pullrequestid)
859 if Optional.extract(repoid):
903 if Optional.extract(repoid):
860 repo = get_repo_or_error(repoid)
904 repo = get_repo_or_error(repoid)
861 else:
905 else:
862 repo = pull_request.target_repo
906 repo = pull_request.target_repo
863
907
864 if not PullRequestModel().check_user_update(
908 if not PullRequestModel().check_user_update(
865 pull_request, apiuser, api=True):
909 pull_request, apiuser, api=True):
866 raise JSONRPCError(
910 raise JSONRPCError(
867 'pull request `%s` update failed, no permission to update.' % (
911 'pull request `%s` update failed, no permission to update.' % (
868 pullrequestid,))
912 pullrequestid,))
869 if pull_request.is_closed():
913 if pull_request.is_closed():
870 raise JSONRPCError(
914 raise JSONRPCError(
871 'pull request `%s` update failed, pull request is closed' % (
915 'pull request `%s` update failed, pull request is closed' % (
872 pullrequestid,))
916 pullrequestid,))
873
917
874 reviewer_objects = Optional.extract(reviewers) or []
918 reviewer_objects = Optional.extract(reviewers) or []
875
919 observer_objects = Optional.extract(observers) or []
876 if reviewer_objects:
877 schema = ReviewerListSchema()
878 try:
879 reviewer_objects = schema.deserialize(reviewer_objects)
880 except Invalid as err:
881 raise JSONRPCValidationError(colander_exc=err)
882
883 # validate users
884 for reviewer_object in reviewer_objects:
885 user = get_user_or_error(reviewer_object['username'])
886 reviewer_object['user_id'] = user.user_id
887
888 get_default_reviewers_data, get_validated_reviewers, validate_observers = \
889 PullRequestModel().get_reviewer_functions()
890
891 # re-use stored rules
892 reviewer_rules = pull_request.reviewer_data
893 try:
894 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
895 except ValueError as e:
896 raise JSONRPCError('Reviewers Validation: {}'.format(e))
897 else:
898 reviewers = []
899
920
900 title = Optional.extract(title)
921 title = Optional.extract(title)
901 description = Optional.extract(description)
922 description = Optional.extract(description)
902 description_renderer = Optional.extract(description_renderer)
923 description_renderer = Optional.extract(description_renderer)
903
924
904 # Update title/description
925 # Update title/description
905 title_changed = False
926 title_changed = False
906 if title or description:
927 if title or description:
907 PullRequestModel().edit(
928 PullRequestModel().edit(
908 pull_request,
929 pull_request,
909 title or pull_request.title,
930 title or pull_request.title,
910 description or pull_request.description,
931 description or pull_request.description,
911 description_renderer or pull_request.description_renderer,
932 description_renderer or pull_request.description_renderer,
912 apiuser)
933 apiuser)
913 Session().commit()
934 Session().commit()
914 title_changed = True
935 title_changed = True
915
936
916 commit_changes = {"added": [], "common": [], "removed": []}
937 commit_changes = {"added": [], "common": [], "removed": []}
917
938
918 # Update commits
939 # Update commits
919 commits_changed = False
940 commits_changed = False
920 if str2bool(Optional.extract(update_commits)):
941 if str2bool(Optional.extract(update_commits)):
921
942
922 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
943 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
923 raise JSONRPCError(
944 raise JSONRPCError(
924 'Operation forbidden because pull request is in state {}, '
945 'Operation forbidden because pull request is in state {}, '
925 'only state {} is allowed.'.format(
946 'only state {} is allowed.'.format(
926 pull_request.pull_request_state, PullRequest.STATE_CREATED))
947 pull_request.pull_request_state, PullRequest.STATE_CREATED))
927
948
928 with pull_request.set_state(PullRequest.STATE_UPDATING):
949 with pull_request.set_state(PullRequest.STATE_UPDATING):
929 if PullRequestModel().has_valid_update_type(pull_request):
950 if PullRequestModel().has_valid_update_type(pull_request):
930 db_user = apiuser.get_instance()
951 db_user = apiuser.get_instance()
931 update_response = PullRequestModel().update_commits(
952 update_response = PullRequestModel().update_commits(
932 pull_request, db_user)
953 pull_request, db_user)
933 commit_changes = update_response.changes or commit_changes
954 commit_changes = update_response.changes or commit_changes
934 Session().commit()
955 Session().commit()
935 commits_changed = True
956 commits_changed = True
936
957
937 # Update reviewers
958 # Update reviewers
959 # serialize and validate passed in given reviewers
960 if reviewer_objects:
961 reviewer_objects = _reviewers_validation(reviewer_objects)
962
963 if observer_objects:
964 observer_objects = _reviewers_validation(reviewer_objects)
965
966 # re-use stored rules
967 default_reviewers_data = pull_request.reviewer_data
968
969 __, validate_default_reviewers, validate_observers = \
970 PullRequestModel().get_reviewer_functions()
971
972 if reviewer_objects:
973 try:
974 reviewers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
975 except ValueError as e:
976 raise JSONRPCError('Reviewers Validation: {}'.format(e))
977 else:
978 reviewers = []
979
980 if observer_objects:
981 try:
982 observers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
983 except ValueError as e:
984 raise JSONRPCError('Observer Validation: {}'.format(e))
985 else:
986 observers = []
987
938 reviewers_changed = False
988 reviewers_changed = False
939 reviewers_changes = {"added": [], "removed": []}
989 reviewers_changes = {"added": [], "removed": []}
940 if reviewers:
990 if reviewers:
941 old_calculated_status = pull_request.calculated_review_status()
991 old_calculated_status = pull_request.calculated_review_status()
942 added_reviewers, removed_reviewers = \
992 added_reviewers, removed_reviewers = \
943 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
993 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser.get_instance())
944
994
945 reviewers_changes['added'] = sorted(
995 reviewers_changes['added'] = sorted(
946 [get_user_or_error(n).username for n in added_reviewers])
996 [get_user_or_error(n).username for n in added_reviewers])
947 reviewers_changes['removed'] = sorted(
997 reviewers_changes['removed'] = sorted(
948 [get_user_or_error(n).username for n in removed_reviewers])
998 [get_user_or_error(n).username for n in removed_reviewers])
949 Session().commit()
999 Session().commit()
950
1000
951 # trigger status changed if change in reviewers changes the status
1001 # trigger status changed if change in reviewers changes the status
952 calculated_status = pull_request.calculated_review_status()
1002 calculated_status = pull_request.calculated_review_status()
953 if old_calculated_status != calculated_status:
1003 if old_calculated_status != calculated_status:
954 PullRequestModel().trigger_pull_request_hook(
1004 PullRequestModel().trigger_pull_request_hook(
955 pull_request, apiuser, 'review_status_change',
1005 pull_request, apiuser, 'review_status_change',
956 data={'status': calculated_status})
1006 data={'status': calculated_status})
957 reviewers_changed = True
1007 reviewers_changed = True
958
1008
959 observers_changed = False
1009 observers_changed = False
1010 observers_changes = {"added": [], "removed": []}
1011 if observers:
1012 added_observers, removed_observers = \
1013 PullRequestModel().update_observers(pull_request, observers, apiuser.get_instance())
1014
1015 observers_changes['added'] = sorted(
1016 [get_user_or_error(n).username for n in added_observers])
1017 observers_changes['removed'] = sorted(
1018 [get_user_or_error(n).username for n in removed_observers])
1019 Session().commit()
1020
1021 reviewers_changed = True
960
1022
961 # push changed to channelstream
1023 # push changed to channelstream
962 if commits_changed or reviewers_changed or observers_changed:
1024 if commits_changed or reviewers_changed or observers_changed:
963 pr_broadcast_channel = channelstream.pr_channel(pull_request)
1025 pr_broadcast_channel = channelstream.pr_channel(pull_request)
964 msg = 'Pull request was updated.'
1026 msg = 'Pull request was updated.'
965 channelstream.pr_update_channelstream_push(
1027 channelstream.pr_update_channelstream_push(
966 request, pr_broadcast_channel, apiuser, msg)
1028 request, pr_broadcast_channel, apiuser, msg)
967
1029
968 data = {
1030 data = {
969 'msg': 'Updated pull request `{}`'.format(
1031 'msg': 'Updated pull request `{}`'.format(pull_request.pull_request_id),
970 pull_request.pull_request_id),
971 'pull_request': pull_request.get_api_data(),
1032 'pull_request': pull_request.get_api_data(),
972 'updated_commits': commit_changes,
1033 'updated_commits': commit_changes,
973 'updated_reviewers': reviewers_changes
1034 'updated_reviewers': reviewers_changes,
1035 'updated_observers': observers_changes,
974 }
1036 }
975
1037
976 return data
1038 return data
977
1039
978
1040
979 @jsonrpc_method()
1041 @jsonrpc_method()
980 def close_pull_request(
1042 def close_pull_request(
981 request, apiuser, pullrequestid, repoid=Optional(None),
1043 request, apiuser, pullrequestid, repoid=Optional(None),
982 userid=Optional(OAttr('apiuser')), message=Optional('')):
1044 userid=Optional(OAttr('apiuser')), message=Optional('')):
983 """
1045 """
984 Close the pull request specified by `pullrequestid`.
1046 Close the pull request specified by `pullrequestid`.
985
1047
986 :param apiuser: This is filled automatically from the |authtoken|.
1048 :param apiuser: This is filled automatically from the |authtoken|.
987 :type apiuser: AuthUser
1049 :type apiuser: AuthUser
988 :param repoid: Repository name or repository ID to which the pull
1050 :param repoid: Repository name or repository ID to which the pull
989 request belongs.
1051 request belongs.
990 :type repoid: str or int
1052 :type repoid: str or int
991 :param pullrequestid: ID of the pull request to be closed.
1053 :param pullrequestid: ID of the pull request to be closed.
992 :type pullrequestid: int
1054 :type pullrequestid: int
993 :param userid: Close the pull request as this user.
1055 :param userid: Close the pull request as this user.
994 :type userid: Optional(str or int)
1056 :type userid: Optional(str or int)
995 :param message: Optional message to close the Pull Request with. If not
1057 :param message: Optional message to close the Pull Request with. If not
996 specified it will be generated automatically.
1058 specified it will be generated automatically.
997 :type message: Optional(str)
1059 :type message: Optional(str)
998
1060
999 Example output:
1061 Example output:
1000
1062
1001 .. code-block:: bash
1063 .. code-block:: bash
1002
1064
1003 "id": <id_given_in_input>,
1065 "id": <id_given_in_input>,
1004 "result": {
1066 "result": {
1005 "pull_request_id": "<int>",
1067 "pull_request_id": "<int>",
1006 "close_status": "<str:status_lbl>,
1068 "close_status": "<str:status_lbl>,
1007 "closed": "<bool>"
1069 "closed": "<bool>"
1008 },
1070 },
1009 "error": null
1071 "error": null
1010
1072
1011 """
1073 """
1012 _ = request.translate
1074 _ = request.translate
1013
1075
1014 pull_request = get_pull_request_or_error(pullrequestid)
1076 pull_request = get_pull_request_or_error(pullrequestid)
1015 if Optional.extract(repoid):
1077 if Optional.extract(repoid):
1016 repo = get_repo_or_error(repoid)
1078 repo = get_repo_or_error(repoid)
1017 else:
1079 else:
1018 repo = pull_request.target_repo
1080 repo = pull_request.target_repo
1019
1081
1020 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1082 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1021 user=apiuser, repo_name=repo.repo_name)
1083 user=apiuser, repo_name=repo.repo_name)
1022 if not isinstance(userid, Optional):
1084 if not isinstance(userid, Optional):
1023 if has_superadmin_permission(apiuser) or is_repo_admin:
1085 if has_superadmin_permission(apiuser) or is_repo_admin:
1024 apiuser = get_user_or_error(userid)
1086 apiuser = get_user_or_error(userid)
1025 else:
1087 else:
1026 raise JSONRPCError('userid is not the same as your user')
1088 raise JSONRPCError('userid is not the same as your user')
1027
1089
1028 if pull_request.is_closed():
1090 if pull_request.is_closed():
1029 raise JSONRPCError(
1091 raise JSONRPCError(
1030 'pull request `%s` is already closed' % (pullrequestid,))
1092 'pull request `%s` is already closed' % (pullrequestid,))
1031
1093
1032 # only owner or admin or person with write permissions
1094 # only owner or admin or person with write permissions
1033 allowed_to_close = PullRequestModel().check_user_update(
1095 allowed_to_close = PullRequestModel().check_user_update(
1034 pull_request, apiuser, api=True)
1096 pull_request, apiuser, api=True)
1035
1097
1036 if not allowed_to_close:
1098 if not allowed_to_close:
1037 raise JSONRPCError(
1099 raise JSONRPCError(
1038 'pull request `%s` close failed, no permission to close.' % (
1100 'pull request `%s` close failed, no permission to close.' % (
1039 pullrequestid,))
1101 pullrequestid,))
1040
1102
1041 # message we're using to close the PR, else it's automatically generated
1103 # message we're using to close the PR, else it's automatically generated
1042 message = Optional.extract(message)
1104 message = Optional.extract(message)
1043
1105
1044 # finally close the PR, with proper message comment
1106 # finally close the PR, with proper message comment
1045 comment, status = PullRequestModel().close_pull_request_with_comment(
1107 comment, status = PullRequestModel().close_pull_request_with_comment(
1046 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1108 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1047 status_lbl = ChangesetStatus.get_status_lbl(status)
1109 status_lbl = ChangesetStatus.get_status_lbl(status)
1048
1110
1049 Session().commit()
1111 Session().commit()
1050
1112
1051 data = {
1113 data = {
1052 'pull_request_id': pull_request.pull_request_id,
1114 'pull_request_id': pull_request.pull_request_id,
1053 'close_status': status_lbl,
1115 'close_status': status_lbl,
1054 'closed': True,
1116 'closed': True,
1055 }
1117 }
1056 return data
1118 return data
@@ -1,2523 +1,2523 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger, rc_cache, channelstream
32 from rhodecode.lib import audit_logger, rc_cache, channelstream
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 HasRepoPermissionAnyApi)
36 HasRepoPermissionAnyApi)
37 from rhodecode.lib.celerylib.utils import get_task_id
37 from rhodecode.lib.celerylib.utils import get_task_id
38 from rhodecode.lib.utils2 import (
38 from rhodecode.lib.utils2 import (
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
40 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.exceptions import (
41 from rhodecode.lib.exceptions import (
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
43 from rhodecode.lib.vcs import RepositoryError
43 from rhodecode.lib.vcs import RepositoryError
44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (
47 from rhodecode.model.db import (
48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
49 ChangesetComment)
49 ChangesetComment)
50 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.pull_request import PullRequestModel
51 from rhodecode.model.pull_request import PullRequestModel
52 from rhodecode.model.repo import RepoModel
52 from rhodecode.model.repo import RepoModel
53 from rhodecode.model.scm import ScmModel, RepoList
53 from rhodecode.model.scm import ScmModel, RepoList
54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
55 from rhodecode.model import validation_schema
55 from rhodecode.model import validation_schema
56 from rhodecode.model.validation_schema.schemas import repo_schema
56 from rhodecode.model.validation_schema.schemas import repo_schema
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 @jsonrpc_method()
61 @jsonrpc_method()
62 def get_repo(request, apiuser, repoid, cache=Optional(True)):
62 def get_repo(request, apiuser, repoid, cache=Optional(True)):
63 """
63 """
64 Gets an existing repository by its name or repository_id.
64 Gets an existing repository by its name or repository_id.
65
65
66 The members section so the output returns users groups or users
66 The members section so the output returns users groups or users
67 associated with that repository.
67 associated with that repository.
68
68
69 This command can only be run using an |authtoken| with admin rights,
69 This command can only be run using an |authtoken| with admin rights,
70 or users with at least read rights to the |repo|.
70 or users with at least read rights to the |repo|.
71
71
72 :param apiuser: This is filled automatically from the |authtoken|.
72 :param apiuser: This is filled automatically from the |authtoken|.
73 :type apiuser: AuthUser
73 :type apiuser: AuthUser
74 :param repoid: The repository name or repository id.
74 :param repoid: The repository name or repository id.
75 :type repoid: str or int
75 :type repoid: str or int
76 :param cache: use the cached value for last changeset
76 :param cache: use the cached value for last changeset
77 :type: cache: Optional(bool)
77 :type: cache: Optional(bool)
78
78
79 Example output:
79 Example output:
80
80
81 .. code-block:: bash
81 .. code-block:: bash
82
82
83 {
83 {
84 "error": null,
84 "error": null,
85 "id": <repo_id>,
85 "id": <repo_id>,
86 "result": {
86 "result": {
87 "clone_uri": null,
87 "clone_uri": null,
88 "created_on": "timestamp",
88 "created_on": "timestamp",
89 "description": "repo description",
89 "description": "repo description",
90 "enable_downloads": false,
90 "enable_downloads": false,
91 "enable_locking": false,
91 "enable_locking": false,
92 "enable_statistics": false,
92 "enable_statistics": false,
93 "followers": [
93 "followers": [
94 {
94 {
95 "active": true,
95 "active": true,
96 "admin": false,
96 "admin": false,
97 "api_key": "****************************************",
97 "api_key": "****************************************",
98 "api_keys": [
98 "api_keys": [
99 "****************************************"
99 "****************************************"
100 ],
100 ],
101 "email": "user@example.com",
101 "email": "user@example.com",
102 "emails": [
102 "emails": [
103 "user@example.com"
103 "user@example.com"
104 ],
104 ],
105 "extern_name": "rhodecode",
105 "extern_name": "rhodecode",
106 "extern_type": "rhodecode",
106 "extern_type": "rhodecode",
107 "firstname": "username",
107 "firstname": "username",
108 "ip_addresses": [],
108 "ip_addresses": [],
109 "language": null,
109 "language": null,
110 "last_login": "2015-09-16T17:16:35.854",
110 "last_login": "2015-09-16T17:16:35.854",
111 "lastname": "surname",
111 "lastname": "surname",
112 "user_id": <user_id>,
112 "user_id": <user_id>,
113 "username": "name"
113 "username": "name"
114 }
114 }
115 ],
115 ],
116 "fork_of": "parent-repo",
116 "fork_of": "parent-repo",
117 "landing_rev": [
117 "landing_rev": [
118 "rev",
118 "rev",
119 "tip"
119 "tip"
120 ],
120 ],
121 "last_changeset": {
121 "last_changeset": {
122 "author": "User <user@example.com>",
122 "author": "User <user@example.com>",
123 "branch": "default",
123 "branch": "default",
124 "date": "timestamp",
124 "date": "timestamp",
125 "message": "last commit message",
125 "message": "last commit message",
126 "parents": [
126 "parents": [
127 {
127 {
128 "raw_id": "commit-id"
128 "raw_id": "commit-id"
129 }
129 }
130 ],
130 ],
131 "raw_id": "commit-id",
131 "raw_id": "commit-id",
132 "revision": <revision number>,
132 "revision": <revision number>,
133 "short_id": "short id"
133 "short_id": "short id"
134 },
134 },
135 "lock_reason": null,
135 "lock_reason": null,
136 "locked_by": null,
136 "locked_by": null,
137 "locked_date": null,
137 "locked_date": null,
138 "owner": "owner-name",
138 "owner": "owner-name",
139 "permissions": [
139 "permissions": [
140 {
140 {
141 "name": "super-admin-name",
141 "name": "super-admin-name",
142 "origin": "super-admin",
142 "origin": "super-admin",
143 "permission": "repository.admin",
143 "permission": "repository.admin",
144 "type": "user"
144 "type": "user"
145 },
145 },
146 {
146 {
147 "name": "owner-name",
147 "name": "owner-name",
148 "origin": "owner",
148 "origin": "owner",
149 "permission": "repository.admin",
149 "permission": "repository.admin",
150 "type": "user"
150 "type": "user"
151 },
151 },
152 {
152 {
153 "name": "user-group-name",
153 "name": "user-group-name",
154 "origin": "permission",
154 "origin": "permission",
155 "permission": "repository.write",
155 "permission": "repository.write",
156 "type": "user_group"
156 "type": "user_group"
157 }
157 }
158 ],
158 ],
159 "private": true,
159 "private": true,
160 "repo_id": 676,
160 "repo_id": 676,
161 "repo_name": "user-group/repo-name",
161 "repo_name": "user-group/repo-name",
162 "repo_type": "hg"
162 "repo_type": "hg"
163 }
163 }
164 }
164 }
165 """
165 """
166
166
167 repo = get_repo_or_error(repoid)
167 repo = get_repo_or_error(repoid)
168 cache = Optional.extract(cache)
168 cache = Optional.extract(cache)
169
169
170 include_secrets = False
170 include_secrets = False
171 if has_superadmin_permission(apiuser):
171 if has_superadmin_permission(apiuser):
172 include_secrets = True
172 include_secrets = True
173 else:
173 else:
174 # check if we have at least read permission for this repo !
174 # check if we have at least read permission for this repo !
175 _perms = (
175 _perms = (
176 'repository.admin', 'repository.write', 'repository.read',)
176 'repository.admin', 'repository.write', 'repository.read',)
177 validate_repo_permissions(apiuser, repoid, repo, _perms)
177 validate_repo_permissions(apiuser, repoid, repo, _perms)
178
178
179 permissions = []
179 permissions = []
180 for _user in repo.permissions():
180 for _user in repo.permissions():
181 user_data = {
181 user_data = {
182 'name': _user.username,
182 'name': _user.username,
183 'permission': _user.permission,
183 'permission': _user.permission,
184 'origin': get_origin(_user),
184 'origin': get_origin(_user),
185 'type': "user",
185 'type': "user",
186 }
186 }
187 permissions.append(user_data)
187 permissions.append(user_data)
188
188
189 for _user_group in repo.permission_user_groups():
189 for _user_group in repo.permission_user_groups():
190 user_group_data = {
190 user_group_data = {
191 'name': _user_group.users_group_name,
191 'name': _user_group.users_group_name,
192 'permission': _user_group.permission,
192 'permission': _user_group.permission,
193 'origin': get_origin(_user_group),
193 'origin': get_origin(_user_group),
194 'type': "user_group",
194 'type': "user_group",
195 }
195 }
196 permissions.append(user_group_data)
196 permissions.append(user_group_data)
197
197
198 following_users = [
198 following_users = [
199 user.user.get_api_data(include_secrets=include_secrets)
199 user.user.get_api_data(include_secrets=include_secrets)
200 for user in repo.followers]
200 for user in repo.followers]
201
201
202 if not cache:
202 if not cache:
203 repo.update_commit_cache()
203 repo.update_commit_cache()
204 data = repo.get_api_data(include_secrets=include_secrets)
204 data = repo.get_api_data(include_secrets=include_secrets)
205 data['permissions'] = permissions
205 data['permissions'] = permissions
206 data['followers'] = following_users
206 data['followers'] = following_users
207 return data
207 return data
208
208
209
209
210 @jsonrpc_method()
210 @jsonrpc_method()
211 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
211 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
212 """
212 """
213 Lists all existing repositories.
213 Lists all existing repositories.
214
214
215 This command can only be run using an |authtoken| with admin rights,
215 This command can only be run using an |authtoken| with admin rights,
216 or users with at least read rights to |repos|.
216 or users with at least read rights to |repos|.
217
217
218 :param apiuser: This is filled automatically from the |authtoken|.
218 :param apiuser: This is filled automatically from the |authtoken|.
219 :type apiuser: AuthUser
219 :type apiuser: AuthUser
220 :param root: specify root repository group to fetch repositories.
220 :param root: specify root repository group to fetch repositories.
221 filters the returned repositories to be members of given root group.
221 filters the returned repositories to be members of given root group.
222 :type root: Optional(None)
222 :type root: Optional(None)
223 :param traverse: traverse given root into subrepositories. With this flag
223 :param traverse: traverse given root into subrepositories. With this flag
224 set to False, it will only return top-level repositories from `root`.
224 set to False, it will only return top-level repositories from `root`.
225 if root is empty it will return just top-level repositories.
225 if root is empty it will return just top-level repositories.
226 :type traverse: Optional(True)
226 :type traverse: Optional(True)
227
227
228
228
229 Example output:
229 Example output:
230
230
231 .. code-block:: bash
231 .. code-block:: bash
232
232
233 id : <id_given_in_input>
233 id : <id_given_in_input>
234 result: [
234 result: [
235 {
235 {
236 "repo_id" : "<repo_id>",
236 "repo_id" : "<repo_id>",
237 "repo_name" : "<reponame>"
237 "repo_name" : "<reponame>"
238 "repo_type" : "<repo_type>",
238 "repo_type" : "<repo_type>",
239 "clone_uri" : "<clone_uri>",
239 "clone_uri" : "<clone_uri>",
240 "private": : "<bool>",
240 "private": : "<bool>",
241 "created_on" : "<datetimecreated>",
241 "created_on" : "<datetimecreated>",
242 "description" : "<description>",
242 "description" : "<description>",
243 "landing_rev": "<landing_rev>",
243 "landing_rev": "<landing_rev>",
244 "owner": "<repo_owner>",
244 "owner": "<repo_owner>",
245 "fork_of": "<name_of_fork_parent>",
245 "fork_of": "<name_of_fork_parent>",
246 "enable_downloads": "<bool>",
246 "enable_downloads": "<bool>",
247 "enable_locking": "<bool>",
247 "enable_locking": "<bool>",
248 "enable_statistics": "<bool>",
248 "enable_statistics": "<bool>",
249 },
249 },
250 ...
250 ...
251 ]
251 ]
252 error: null
252 error: null
253 """
253 """
254
254
255 include_secrets = has_superadmin_permission(apiuser)
255 include_secrets = has_superadmin_permission(apiuser)
256 _perms = ('repository.read', 'repository.write', 'repository.admin',)
256 _perms = ('repository.read', 'repository.write', 'repository.admin',)
257 extras = {'user': apiuser}
257 extras = {'user': apiuser}
258
258
259 root = Optional.extract(root)
259 root = Optional.extract(root)
260 traverse = Optional.extract(traverse, binary=True)
260 traverse = Optional.extract(traverse, binary=True)
261
261
262 if root:
262 if root:
263 # verify parent existance, if it's empty return an error
263 # verify parent existance, if it's empty return an error
264 parent = RepoGroup.get_by_group_name(root)
264 parent = RepoGroup.get_by_group_name(root)
265 if not parent:
265 if not parent:
266 raise JSONRPCError(
266 raise JSONRPCError(
267 'Root repository group `{}` does not exist'.format(root))
267 'Root repository group `{}` does not exist'.format(root))
268
268
269 if traverse:
269 if traverse:
270 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
270 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
271 else:
271 else:
272 repos = RepoModel().get_repos_for_root(root=parent)
272 repos = RepoModel().get_repos_for_root(root=parent)
273 else:
273 else:
274 if traverse:
274 if traverse:
275 repos = RepoModel().get_all()
275 repos = RepoModel().get_all()
276 else:
276 else:
277 # return just top-level
277 # return just top-level
278 repos = RepoModel().get_repos_for_root(root=None)
278 repos = RepoModel().get_repos_for_root(root=None)
279
279
280 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
280 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
281 return [repo.get_api_data(include_secrets=include_secrets)
281 return [repo.get_api_data(include_secrets=include_secrets)
282 for repo in repo_list]
282 for repo in repo_list]
283
283
284
284
285 @jsonrpc_method()
285 @jsonrpc_method()
286 def get_repo_changeset(request, apiuser, repoid, revision,
286 def get_repo_changeset(request, apiuser, repoid, revision,
287 details=Optional('basic')):
287 details=Optional('basic')):
288 """
288 """
289 Returns information about a changeset.
289 Returns information about a changeset.
290
290
291 Additionally parameters define the amount of details returned by
291 Additionally parameters define the amount of details returned by
292 this function.
292 this function.
293
293
294 This command can only be run using an |authtoken| with admin rights,
294 This command can only be run using an |authtoken| with admin rights,
295 or users with at least read rights to the |repo|.
295 or users with at least read rights to the |repo|.
296
296
297 :param apiuser: This is filled automatically from the |authtoken|.
297 :param apiuser: This is filled automatically from the |authtoken|.
298 :type apiuser: AuthUser
298 :type apiuser: AuthUser
299 :param repoid: The repository name or repository id
299 :param repoid: The repository name or repository id
300 :type repoid: str or int
300 :type repoid: str or int
301 :param revision: revision for which listing should be done
301 :param revision: revision for which listing should be done
302 :type revision: str
302 :type revision: str
303 :param details: details can be 'basic|extended|full' full gives diff
303 :param details: details can be 'basic|extended|full' full gives diff
304 info details like the diff itself, and number of changed files etc.
304 info details like the diff itself, and number of changed files etc.
305 :type details: Optional(str)
305 :type details: Optional(str)
306
306
307 """
307 """
308 repo = get_repo_or_error(repoid)
308 repo = get_repo_or_error(repoid)
309 if not has_superadmin_permission(apiuser):
309 if not has_superadmin_permission(apiuser):
310 _perms = ('repository.admin', 'repository.write', 'repository.read',)
310 _perms = ('repository.admin', 'repository.write', 'repository.read',)
311 validate_repo_permissions(apiuser, repoid, repo, _perms)
311 validate_repo_permissions(apiuser, repoid, repo, _perms)
312
312
313 changes_details = Optional.extract(details)
313 changes_details = Optional.extract(details)
314 _changes_details_types = ['basic', 'extended', 'full']
314 _changes_details_types = ['basic', 'extended', 'full']
315 if changes_details not in _changes_details_types:
315 if changes_details not in _changes_details_types:
316 raise JSONRPCError(
316 raise JSONRPCError(
317 'ret_type must be one of %s' % (
317 'ret_type must be one of %s' % (
318 ','.join(_changes_details_types)))
318 ','.join(_changes_details_types)))
319
319
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
321 'status', '_commit', '_file_paths']
321 'status', '_commit', '_file_paths']
322
322
323 try:
323 try:
324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
325 except TypeError as e:
325 except TypeError as e:
326 raise JSONRPCError(safe_str(e))
326 raise JSONRPCError(safe_str(e))
327 _cs_json = cs.__json__()
327 _cs_json = cs.__json__()
328 _cs_json['diff'] = build_commit_data(cs, changes_details)
328 _cs_json['diff'] = build_commit_data(cs, changes_details)
329 if changes_details == 'full':
329 if changes_details == 'full':
330 _cs_json['refs'] = cs._get_refs()
330 _cs_json['refs'] = cs._get_refs()
331 return _cs_json
331 return _cs_json
332
332
333
333
334 @jsonrpc_method()
334 @jsonrpc_method()
335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
336 details=Optional('basic')):
336 details=Optional('basic')):
337 """
337 """
338 Returns a set of commits limited by the number starting
338 Returns a set of commits limited by the number starting
339 from the `start_rev` option.
339 from the `start_rev` option.
340
340
341 Additional parameters define the amount of details returned by this
341 Additional parameters define the amount of details returned by this
342 function.
342 function.
343
343
344 This command can only be run using an |authtoken| with admin rights,
344 This command can only be run using an |authtoken| with admin rights,
345 or users with at least read rights to |repos|.
345 or users with at least read rights to |repos|.
346
346
347 :param apiuser: This is filled automatically from the |authtoken|.
347 :param apiuser: This is filled automatically from the |authtoken|.
348 :type apiuser: AuthUser
348 :type apiuser: AuthUser
349 :param repoid: The repository name or repository ID.
349 :param repoid: The repository name or repository ID.
350 :type repoid: str or int
350 :type repoid: str or int
351 :param start_rev: The starting revision from where to get changesets.
351 :param start_rev: The starting revision from where to get changesets.
352 :type start_rev: str
352 :type start_rev: str
353 :param limit: Limit the number of commits to this amount
353 :param limit: Limit the number of commits to this amount
354 :type limit: str or int
354 :type limit: str or int
355 :param details: Set the level of detail returned. Valid option are:
355 :param details: Set the level of detail returned. Valid option are:
356 ``basic``, ``extended`` and ``full``.
356 ``basic``, ``extended`` and ``full``.
357 :type details: Optional(str)
357 :type details: Optional(str)
358
358
359 .. note::
359 .. note::
360
360
361 Setting the parameter `details` to the value ``full`` is extensive
361 Setting the parameter `details` to the value ``full`` is extensive
362 and returns details like the diff itself, and the number
362 and returns details like the diff itself, and the number
363 of changed files.
363 of changed files.
364
364
365 """
365 """
366 repo = get_repo_or_error(repoid)
366 repo = get_repo_or_error(repoid)
367 if not has_superadmin_permission(apiuser):
367 if not has_superadmin_permission(apiuser):
368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
369 validate_repo_permissions(apiuser, repoid, repo, _perms)
369 validate_repo_permissions(apiuser, repoid, repo, _perms)
370
370
371 changes_details = Optional.extract(details)
371 changes_details = Optional.extract(details)
372 _changes_details_types = ['basic', 'extended', 'full']
372 _changes_details_types = ['basic', 'extended', 'full']
373 if changes_details not in _changes_details_types:
373 if changes_details not in _changes_details_types:
374 raise JSONRPCError(
374 raise JSONRPCError(
375 'ret_type must be one of %s' % (
375 'ret_type must be one of %s' % (
376 ','.join(_changes_details_types)))
376 ','.join(_changes_details_types)))
377
377
378 limit = int(limit)
378 limit = int(limit)
379 pre_load = ['author', 'branch', 'date', 'message', 'parents',
379 pre_load = ['author', 'branch', 'date', 'message', 'parents',
380 'status', '_commit', '_file_paths']
380 'status', '_commit', '_file_paths']
381
381
382 vcs_repo = repo.scm_instance()
382 vcs_repo = repo.scm_instance()
383 # SVN needs a special case to distinguish its index and commit id
383 # SVN needs a special case to distinguish its index and commit id
384 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
384 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
385 start_rev = vcs_repo.commit_ids[0]
385 start_rev = vcs_repo.commit_ids[0]
386
386
387 try:
387 try:
388 commits = vcs_repo.get_commits(
388 commits = vcs_repo.get_commits(
389 start_id=start_rev, pre_load=pre_load, translate_tags=False)
389 start_id=start_rev, pre_load=pre_load, translate_tags=False)
390 except TypeError as e:
390 except TypeError as e:
391 raise JSONRPCError(safe_str(e))
391 raise JSONRPCError(safe_str(e))
392 except Exception:
392 except Exception:
393 log.exception('Fetching of commits failed')
393 log.exception('Fetching of commits failed')
394 raise JSONRPCError('Error occurred during commit fetching')
394 raise JSONRPCError('Error occurred during commit fetching')
395
395
396 ret = []
396 ret = []
397 for cnt, commit in enumerate(commits):
397 for cnt, commit in enumerate(commits):
398 if cnt >= limit != -1:
398 if cnt >= limit != -1:
399 break
399 break
400 _cs_json = commit.__json__()
400 _cs_json = commit.__json__()
401 _cs_json['diff'] = build_commit_data(commit, changes_details)
401 _cs_json['diff'] = build_commit_data(commit, changes_details)
402 if changes_details == 'full':
402 if changes_details == 'full':
403 _cs_json['refs'] = {
403 _cs_json['refs'] = {
404 'branches': [commit.branch],
404 'branches': [commit.branch],
405 'bookmarks': getattr(commit, 'bookmarks', []),
405 'bookmarks': getattr(commit, 'bookmarks', []),
406 'tags': commit.tags
406 'tags': commit.tags
407 }
407 }
408 ret.append(_cs_json)
408 ret.append(_cs_json)
409 return ret
409 return ret
410
410
411
411
412 @jsonrpc_method()
412 @jsonrpc_method()
413 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
413 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
414 ret_type=Optional('all'), details=Optional('basic'),
414 ret_type=Optional('all'), details=Optional('basic'),
415 max_file_bytes=Optional(None)):
415 max_file_bytes=Optional(None)):
416 """
416 """
417 Returns a list of nodes and children in a flat list for a given
417 Returns a list of nodes and children in a flat list for a given
418 path at given revision.
418 path at given revision.
419
419
420 It's possible to specify ret_type to show only `files` or `dirs`.
420 It's possible to specify ret_type to show only `files` or `dirs`.
421
421
422 This command can only be run using an |authtoken| with admin rights,
422 This command can only be run using an |authtoken| with admin rights,
423 or users with at least read rights to |repos|.
423 or users with at least read rights to |repos|.
424
424
425 :param apiuser: This is filled automatically from the |authtoken|.
425 :param apiuser: This is filled automatically from the |authtoken|.
426 :type apiuser: AuthUser
426 :type apiuser: AuthUser
427 :param repoid: The repository name or repository ID.
427 :param repoid: The repository name or repository ID.
428 :type repoid: str or int
428 :type repoid: str or int
429 :param revision: The revision for which listing should be done.
429 :param revision: The revision for which listing should be done.
430 :type revision: str
430 :type revision: str
431 :param root_path: The path from which to start displaying.
431 :param root_path: The path from which to start displaying.
432 :type root_path: str
432 :type root_path: str
433 :param ret_type: Set the return type. Valid options are
433 :param ret_type: Set the return type. Valid options are
434 ``all`` (default), ``files`` and ``dirs``.
434 ``all`` (default), ``files`` and ``dirs``.
435 :type ret_type: Optional(str)
435 :type ret_type: Optional(str)
436 :param details: Returns extended information about nodes, such as
436 :param details: Returns extended information about nodes, such as
437 md5, binary, and or content.
437 md5, binary, and or content.
438 The valid options are ``basic`` and ``full``.
438 The valid options are ``basic`` and ``full``.
439 :type details: Optional(str)
439 :type details: Optional(str)
440 :param max_file_bytes: Only return file content under this file size bytes
440 :param max_file_bytes: Only return file content under this file size bytes
441 :type details: Optional(int)
441 :type details: Optional(int)
442
442
443 Example output:
443 Example output:
444
444
445 .. code-block:: bash
445 .. code-block:: bash
446
446
447 id : <id_given_in_input>
447 id : <id_given_in_input>
448 result: [
448 result: [
449 {
449 {
450 "binary": false,
450 "binary": false,
451 "content": "File line",
451 "content": "File line",
452 "extension": "md",
452 "extension": "md",
453 "lines": 2,
453 "lines": 2,
454 "md5": "059fa5d29b19c0657e384749480f6422",
454 "md5": "059fa5d29b19c0657e384749480f6422",
455 "mimetype": "text/x-minidsrc",
455 "mimetype": "text/x-minidsrc",
456 "name": "file.md",
456 "name": "file.md",
457 "size": 580,
457 "size": 580,
458 "type": "file"
458 "type": "file"
459 },
459 },
460 ...
460 ...
461 ]
461 ]
462 error: null
462 error: null
463 """
463 """
464
464
465 repo = get_repo_or_error(repoid)
465 repo = get_repo_or_error(repoid)
466 if not has_superadmin_permission(apiuser):
466 if not has_superadmin_permission(apiuser):
467 _perms = ('repository.admin', 'repository.write', 'repository.read',)
467 _perms = ('repository.admin', 'repository.write', 'repository.read',)
468 validate_repo_permissions(apiuser, repoid, repo, _perms)
468 validate_repo_permissions(apiuser, repoid, repo, _perms)
469
469
470 ret_type = Optional.extract(ret_type)
470 ret_type = Optional.extract(ret_type)
471 details = Optional.extract(details)
471 details = Optional.extract(details)
472 _extended_types = ['basic', 'full']
472 _extended_types = ['basic', 'full']
473 if details not in _extended_types:
473 if details not in _extended_types:
474 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
474 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
475 extended_info = False
475 extended_info = False
476 content = False
476 content = False
477 if details == 'basic':
477 if details == 'basic':
478 extended_info = True
478 extended_info = True
479
479
480 if details == 'full':
480 if details == 'full':
481 extended_info = content = True
481 extended_info = content = True
482
482
483 _map = {}
483 _map = {}
484 try:
484 try:
485 # check if repo is not empty by any chance, skip quicker if it is.
485 # check if repo is not empty by any chance, skip quicker if it is.
486 _scm = repo.scm_instance()
486 _scm = repo.scm_instance()
487 if _scm.is_empty():
487 if _scm.is_empty():
488 return []
488 return []
489
489
490 _d, _f = ScmModel().get_nodes(
490 _d, _f = ScmModel().get_nodes(
491 repo, revision, root_path, flat=False,
491 repo, revision, root_path, flat=False,
492 extended_info=extended_info, content=content,
492 extended_info=extended_info, content=content,
493 max_file_bytes=max_file_bytes)
493 max_file_bytes=max_file_bytes)
494 _map = {
494 _map = {
495 'all': _d + _f,
495 'all': _d + _f,
496 'files': _f,
496 'files': _f,
497 'dirs': _d,
497 'dirs': _d,
498 }
498 }
499 return _map[ret_type]
499 return _map[ret_type]
500 except KeyError:
500 except KeyError:
501 raise JSONRPCError(
501 raise JSONRPCError(
502 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
502 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
503 except Exception:
503 except Exception:
504 log.exception("Exception occurred while trying to get repo nodes")
504 log.exception("Exception occurred while trying to get repo nodes")
505 raise JSONRPCError(
505 raise JSONRPCError(
506 'failed to get repo: `%s` nodes' % repo.repo_name
506 'failed to get repo: `%s` nodes' % repo.repo_name
507 )
507 )
508
508
509
509
510 @jsonrpc_method()
510 @jsonrpc_method()
511 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
511 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
512 max_file_bytes=Optional(None), details=Optional('basic'),
512 max_file_bytes=Optional(None), details=Optional('basic'),
513 cache=Optional(True)):
513 cache=Optional(True)):
514 """
514 """
515 Returns a single file from repository at given revision.
515 Returns a single file from repository at given revision.
516
516
517 This command can only be run using an |authtoken| with admin rights,
517 This command can only be run using an |authtoken| with admin rights,
518 or users with at least read rights to |repos|.
518 or users with at least read rights to |repos|.
519
519
520 :param apiuser: This is filled automatically from the |authtoken|.
520 :param apiuser: This is filled automatically from the |authtoken|.
521 :type apiuser: AuthUser
521 :type apiuser: AuthUser
522 :param repoid: The repository name or repository ID.
522 :param repoid: The repository name or repository ID.
523 :type repoid: str or int
523 :type repoid: str or int
524 :param commit_id: The revision for which listing should be done.
524 :param commit_id: The revision for which listing should be done.
525 :type commit_id: str
525 :type commit_id: str
526 :param file_path: The path from which to start displaying.
526 :param file_path: The path from which to start displaying.
527 :type file_path: str
527 :type file_path: str
528 :param details: Returns different set of information about nodes.
528 :param details: Returns different set of information about nodes.
529 The valid options are ``minimal`` ``basic`` and ``full``.
529 The valid options are ``minimal`` ``basic`` and ``full``.
530 :type details: Optional(str)
530 :type details: Optional(str)
531 :param max_file_bytes: Only return file content under this file size bytes
531 :param max_file_bytes: Only return file content under this file size bytes
532 :type max_file_bytes: Optional(int)
532 :type max_file_bytes: Optional(int)
533 :param cache: Use internal caches for fetching files. If disabled fetching
533 :param cache: Use internal caches for fetching files. If disabled fetching
534 files is slower but more memory efficient
534 files is slower but more memory efficient
535 :type cache: Optional(bool)
535 :type cache: Optional(bool)
536
536
537 Example output:
537 Example output:
538
538
539 .. code-block:: bash
539 .. code-block:: bash
540
540
541 id : <id_given_in_input>
541 id : <id_given_in_input>
542 result: {
542 result: {
543 "binary": false,
543 "binary": false,
544 "extension": "py",
544 "extension": "py",
545 "lines": 35,
545 "lines": 35,
546 "content": "....",
546 "content": "....",
547 "md5": "76318336366b0f17ee249e11b0c99c41",
547 "md5": "76318336366b0f17ee249e11b0c99c41",
548 "mimetype": "text/x-python",
548 "mimetype": "text/x-python",
549 "name": "python.py",
549 "name": "python.py",
550 "size": 817,
550 "size": 817,
551 "type": "file",
551 "type": "file",
552 }
552 }
553 error: null
553 error: null
554 """
554 """
555
555
556 repo = get_repo_or_error(repoid)
556 repo = get_repo_or_error(repoid)
557 if not has_superadmin_permission(apiuser):
557 if not has_superadmin_permission(apiuser):
558 _perms = ('repository.admin', 'repository.write', 'repository.read',)
558 _perms = ('repository.admin', 'repository.write', 'repository.read',)
559 validate_repo_permissions(apiuser, repoid, repo, _perms)
559 validate_repo_permissions(apiuser, repoid, repo, _perms)
560
560
561 cache = Optional.extract(cache, binary=True)
561 cache = Optional.extract(cache, binary=True)
562 details = Optional.extract(details)
562 details = Optional.extract(details)
563 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
563 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
564 if details not in _extended_types:
564 if details not in _extended_types:
565 raise JSONRPCError(
565 raise JSONRPCError(
566 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
566 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
567 extended_info = False
567 extended_info = False
568 content = False
568 content = False
569
569
570 if details == 'minimal':
570 if details == 'minimal':
571 extended_info = False
571 extended_info = False
572
572
573 elif details == 'basic':
573 elif details == 'basic':
574 extended_info = True
574 extended_info = True
575
575
576 elif details == 'full':
576 elif details == 'full':
577 extended_info = content = True
577 extended_info = content = True
578
578
579 file_path = safe_unicode(file_path)
579 file_path = safe_unicode(file_path)
580 try:
580 try:
581 # check if repo is not empty by any chance, skip quicker if it is.
581 # check if repo is not empty by any chance, skip quicker if it is.
582 _scm = repo.scm_instance()
582 _scm = repo.scm_instance()
583 if _scm.is_empty():
583 if _scm.is_empty():
584 return None
584 return None
585
585
586 node = ScmModel().get_node(
586 node = ScmModel().get_node(
587 repo, commit_id, file_path, extended_info=extended_info,
587 repo, commit_id, file_path, extended_info=extended_info,
588 content=content, max_file_bytes=max_file_bytes, cache=cache)
588 content=content, max_file_bytes=max_file_bytes, cache=cache)
589 except NodeDoesNotExistError:
589 except NodeDoesNotExistError:
590 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
590 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
591 repo.repo_name, file_path, commit_id))
591 repo.repo_name, file_path, commit_id))
592 except Exception:
592 except Exception:
593 log.exception(u"Exception occurred while trying to get repo %s file",
593 log.exception(u"Exception occurred while trying to get repo %s file",
594 repo.repo_name)
594 repo.repo_name)
595 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
595 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
596 repo.repo_name, file_path))
596 repo.repo_name, file_path))
597
597
598 return node
598 return node
599
599
600
600
601 @jsonrpc_method()
601 @jsonrpc_method()
602 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
602 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
603 """
603 """
604 Returns a list of tree nodes for path at given revision. This api is built
604 Returns a list of tree nodes for path at given revision. This api is built
605 strictly for usage in full text search building, and shouldn't be consumed
605 strictly for usage in full text search building, and shouldn't be consumed
606
606
607 This command can only be run using an |authtoken| with admin rights,
607 This command can only be run using an |authtoken| with admin rights,
608 or users with at least read rights to |repos|.
608 or users with at least read rights to |repos|.
609
609
610 """
610 """
611
611
612 repo = get_repo_or_error(repoid)
612 repo = get_repo_or_error(repoid)
613 if not has_superadmin_permission(apiuser):
613 if not has_superadmin_permission(apiuser):
614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
615 validate_repo_permissions(apiuser, repoid, repo, _perms)
615 validate_repo_permissions(apiuser, repoid, repo, _perms)
616
616
617 repo_id = repo.repo_id
617 repo_id = repo.repo_id
618 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
618 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
619 cache_on = cache_seconds > 0
619 cache_on = cache_seconds > 0
620
620
621 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
621 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
622 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
622 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
623
623
624 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
624 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
625 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
625 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
626
626
627 try:
627 try:
628 # check if repo is not empty by any chance, skip quicker if it is.
628 # check if repo is not empty by any chance, skip quicker if it is.
629 _scm = repo.scm_instance()
629 _scm = repo.scm_instance()
630 if _scm.is_empty():
630 if _scm.is_empty():
631 return []
631 return []
632 except RepositoryError:
632 except RepositoryError:
633 log.exception("Exception occurred while trying to get repo nodes")
633 log.exception("Exception occurred while trying to get repo nodes")
634 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
634 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
635
635
636 try:
636 try:
637 # we need to resolve commit_id to a FULL sha for cache to work correctly.
637 # we need to resolve commit_id to a FULL sha for cache to work correctly.
638 # sending 'master' is a pointer that needs to be translated to current commit.
638 # sending 'master' is a pointer that needs to be translated to current commit.
639 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
639 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
640 log.debug(
640 log.debug(
641 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
641 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
642 'with caching: %s[TTL: %ss]' % (
642 'with caching: %s[TTL: %ss]' % (
643 repo_id, commit_id, cache_on, cache_seconds or 0))
643 repo_id, commit_id, cache_on, cache_seconds or 0))
644
644
645 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
645 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
646 return tree_files
646 return tree_files
647
647
648 except Exception:
648 except Exception:
649 log.exception("Exception occurred while trying to get repo nodes")
649 log.exception("Exception occurred while trying to get repo nodes")
650 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
650 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
651
651
652
652
653 @jsonrpc_method()
653 @jsonrpc_method()
654 def get_repo_refs(request, apiuser, repoid):
654 def get_repo_refs(request, apiuser, repoid):
655 """
655 """
656 Returns a dictionary of current references. It returns
656 Returns a dictionary of current references. It returns
657 bookmarks, branches, closed_branches, and tags for given repository
657 bookmarks, branches, closed_branches, and tags for given repository
658
658
659 It's possible to specify ret_type to show only `files` or `dirs`.
659 It's possible to specify ret_type to show only `files` or `dirs`.
660
660
661 This command can only be run using an |authtoken| with admin rights,
661 This command can only be run using an |authtoken| with admin rights,
662 or users with at least read rights to |repos|.
662 or users with at least read rights to |repos|.
663
663
664 :param apiuser: This is filled automatically from the |authtoken|.
664 :param apiuser: This is filled automatically from the |authtoken|.
665 :type apiuser: AuthUser
665 :type apiuser: AuthUser
666 :param repoid: The repository name or repository ID.
666 :param repoid: The repository name or repository ID.
667 :type repoid: str or int
667 :type repoid: str or int
668
668
669 Example output:
669 Example output:
670
670
671 .. code-block:: bash
671 .. code-block:: bash
672
672
673 id : <id_given_in_input>
673 id : <id_given_in_input>
674 "result": {
674 "result": {
675 "bookmarks": {
675 "bookmarks": {
676 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
676 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
677 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
677 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
678 },
678 },
679 "branches": {
679 "branches": {
680 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
680 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
681 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
681 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
682 },
682 },
683 "branches_closed": {},
683 "branches_closed": {},
684 "tags": {
684 "tags": {
685 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
685 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
686 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
686 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
687 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
687 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
688 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
688 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
689 }
689 }
690 }
690 }
691 error: null
691 error: null
692 """
692 """
693
693
694 repo = get_repo_or_error(repoid)
694 repo = get_repo_or_error(repoid)
695 if not has_superadmin_permission(apiuser):
695 if not has_superadmin_permission(apiuser):
696 _perms = ('repository.admin', 'repository.write', 'repository.read',)
696 _perms = ('repository.admin', 'repository.write', 'repository.read',)
697 validate_repo_permissions(apiuser, repoid, repo, _perms)
697 validate_repo_permissions(apiuser, repoid, repo, _perms)
698
698
699 try:
699 try:
700 # check if repo is not empty by any chance, skip quicker if it is.
700 # check if repo is not empty by any chance, skip quicker if it is.
701 vcs_instance = repo.scm_instance()
701 vcs_instance = repo.scm_instance()
702 refs = vcs_instance.refs()
702 refs = vcs_instance.refs()
703 return refs
703 return refs
704 except Exception:
704 except Exception:
705 log.exception("Exception occurred while trying to get repo refs")
705 log.exception("Exception occurred while trying to get repo refs")
706 raise JSONRPCError(
706 raise JSONRPCError(
707 'failed to get repo: `%s` references' % repo.repo_name
707 'failed to get repo: `%s` references' % repo.repo_name
708 )
708 )
709
709
710
710
711 @jsonrpc_method()
711 @jsonrpc_method()
712 def create_repo(
712 def create_repo(
713 request, apiuser, repo_name, repo_type,
713 request, apiuser, repo_name, repo_type,
714 owner=Optional(OAttr('apiuser')),
714 owner=Optional(OAttr('apiuser')),
715 description=Optional(''),
715 description=Optional(''),
716 private=Optional(False),
716 private=Optional(False),
717 clone_uri=Optional(None),
717 clone_uri=Optional(None),
718 push_uri=Optional(None),
718 push_uri=Optional(None),
719 landing_rev=Optional(None),
719 landing_rev=Optional(None),
720 enable_statistics=Optional(False),
720 enable_statistics=Optional(False),
721 enable_locking=Optional(False),
721 enable_locking=Optional(False),
722 enable_downloads=Optional(False),
722 enable_downloads=Optional(False),
723 copy_permissions=Optional(False)):
723 copy_permissions=Optional(False)):
724 """
724 """
725 Creates a repository.
725 Creates a repository.
726
726
727 * If the repository name contains "/", repository will be created inside
727 * If the repository name contains "/", repository will be created inside
728 a repository group or nested repository groups
728 a repository group or nested repository groups
729
729
730 For example "foo/bar/repo1" will create |repo| called "repo1" inside
730 For example "foo/bar/repo1" will create |repo| called "repo1" inside
731 group "foo/bar". You have to have permissions to access and write to
731 group "foo/bar". You have to have permissions to access and write to
732 the last repository group ("bar" in this example)
732 the last repository group ("bar" in this example)
733
733
734 This command can only be run using an |authtoken| with at least
734 This command can only be run using an |authtoken| with at least
735 permissions to create repositories, or write permissions to
735 permissions to create repositories, or write permissions to
736 parent repository groups.
736 parent repository groups.
737
737
738 :param apiuser: This is filled automatically from the |authtoken|.
738 :param apiuser: This is filled automatically from the |authtoken|.
739 :type apiuser: AuthUser
739 :type apiuser: AuthUser
740 :param repo_name: Set the repository name.
740 :param repo_name: Set the repository name.
741 :type repo_name: str
741 :type repo_name: str
742 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
742 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
743 :type repo_type: str
743 :type repo_type: str
744 :param owner: user_id or username
744 :param owner: user_id or username
745 :type owner: Optional(str)
745 :type owner: Optional(str)
746 :param description: Set the repository description.
746 :param description: Set the repository description.
747 :type description: Optional(str)
747 :type description: Optional(str)
748 :param private: set repository as private
748 :param private: set repository as private
749 :type private: bool
749 :type private: bool
750 :param clone_uri: set clone_uri
750 :param clone_uri: set clone_uri
751 :type clone_uri: str
751 :type clone_uri: str
752 :param push_uri: set push_uri
752 :param push_uri: set push_uri
753 :type push_uri: str
753 :type push_uri: str
754 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
754 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
755 :type landing_rev: str
755 :type landing_rev: str
756 :param enable_locking:
756 :param enable_locking:
757 :type enable_locking: bool
757 :type enable_locking: bool
758 :param enable_downloads:
758 :param enable_downloads:
759 :type enable_downloads: bool
759 :type enable_downloads: bool
760 :param enable_statistics:
760 :param enable_statistics:
761 :type enable_statistics: bool
761 :type enable_statistics: bool
762 :param copy_permissions: Copy permission from group in which the
762 :param copy_permissions: Copy permission from group in which the
763 repository is being created.
763 repository is being created.
764 :type copy_permissions: bool
764 :type copy_permissions: bool
765
765
766
766
767 Example output:
767 Example output:
768
768
769 .. code-block:: bash
769 .. code-block:: bash
770
770
771 id : <id_given_in_input>
771 id : <id_given_in_input>
772 result: {
772 result: {
773 "msg": "Created new repository `<reponame>`",
773 "msg": "Created new repository `<reponame>`",
774 "success": true,
774 "success": true,
775 "task": "<celery task id or None if done sync>"
775 "task": "<celery task id or None if done sync>"
776 }
776 }
777 error: null
777 error: null
778
778
779
779
780 Example error output:
780 Example error output:
781
781
782 .. code-block:: bash
782 .. code-block:: bash
783
783
784 id : <id_given_in_input>
784 id : <id_given_in_input>
785 result : null
785 result : null
786 error : {
786 error : {
787 'failed to create repository `<repo_name>`'
787 'failed to create repository `<repo_name>`'
788 }
788 }
789
789
790 """
790 """
791
791
792 owner = validate_set_owner_permissions(apiuser, owner)
792 owner = validate_set_owner_permissions(apiuser, owner)
793
793
794 description = Optional.extract(description)
794 description = Optional.extract(description)
795 copy_permissions = Optional.extract(copy_permissions)
795 copy_permissions = Optional.extract(copy_permissions)
796 clone_uri = Optional.extract(clone_uri)
796 clone_uri = Optional.extract(clone_uri)
797 push_uri = Optional.extract(push_uri)
797 push_uri = Optional.extract(push_uri)
798
798
799 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
799 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
800 if isinstance(private, Optional):
800 if isinstance(private, Optional):
801 private = defs.get('repo_private') or Optional.extract(private)
801 private = defs.get('repo_private') or Optional.extract(private)
802 if isinstance(repo_type, Optional):
802 if isinstance(repo_type, Optional):
803 repo_type = defs.get('repo_type')
803 repo_type = defs.get('repo_type')
804 if isinstance(enable_statistics, Optional):
804 if isinstance(enable_statistics, Optional):
805 enable_statistics = defs.get('repo_enable_statistics')
805 enable_statistics = defs.get('repo_enable_statistics')
806 if isinstance(enable_locking, Optional):
806 if isinstance(enable_locking, Optional):
807 enable_locking = defs.get('repo_enable_locking')
807 enable_locking = defs.get('repo_enable_locking')
808 if isinstance(enable_downloads, Optional):
808 if isinstance(enable_downloads, Optional):
809 enable_downloads = defs.get('repo_enable_downloads')
809 enable_downloads = defs.get('repo_enable_downloads')
810
810
811 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
811 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
812 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
812 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
813 ref_choices = list(set(ref_choices + [landing_ref]))
813 ref_choices = list(set(ref_choices + [landing_ref]))
814
814
815 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
815 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
816
816
817 schema = repo_schema.RepoSchema().bind(
817 schema = repo_schema.RepoSchema().bind(
818 repo_type_options=rhodecode.BACKENDS.keys(),
818 repo_type_options=rhodecode.BACKENDS.keys(),
819 repo_ref_options=ref_choices,
819 repo_ref_options=ref_choices,
820 repo_type=repo_type,
820 repo_type=repo_type,
821 # user caller
821 # user caller
822 user=apiuser)
822 user=apiuser)
823
823
824 try:
824 try:
825 schema_data = schema.deserialize(dict(
825 schema_data = schema.deserialize(dict(
826 repo_name=repo_name,
826 repo_name=repo_name,
827 repo_type=repo_type,
827 repo_type=repo_type,
828 repo_owner=owner.username,
828 repo_owner=owner.username,
829 repo_description=description,
829 repo_description=description,
830 repo_landing_commit_ref=landing_commit_ref,
830 repo_landing_commit_ref=landing_commit_ref,
831 repo_clone_uri=clone_uri,
831 repo_clone_uri=clone_uri,
832 repo_push_uri=push_uri,
832 repo_push_uri=push_uri,
833 repo_private=private,
833 repo_private=private,
834 repo_copy_permissions=copy_permissions,
834 repo_copy_permissions=copy_permissions,
835 repo_enable_statistics=enable_statistics,
835 repo_enable_statistics=enable_statistics,
836 repo_enable_downloads=enable_downloads,
836 repo_enable_downloads=enable_downloads,
837 repo_enable_locking=enable_locking))
837 repo_enable_locking=enable_locking))
838 except validation_schema.Invalid as err:
838 except validation_schema.Invalid as err:
839 raise JSONRPCValidationError(colander_exc=err)
839 raise JSONRPCValidationError(colander_exc=err)
840
840
841 try:
841 try:
842 data = {
842 data = {
843 'owner': owner,
843 'owner': owner,
844 'repo_name': schema_data['repo_group']['repo_name_without_group'],
844 'repo_name': schema_data['repo_group']['repo_name_without_group'],
845 'repo_name_full': schema_data['repo_name'],
845 'repo_name_full': schema_data['repo_name'],
846 'repo_group': schema_data['repo_group']['repo_group_id'],
846 'repo_group': schema_data['repo_group']['repo_group_id'],
847 'repo_type': schema_data['repo_type'],
847 'repo_type': schema_data['repo_type'],
848 'repo_description': schema_data['repo_description'],
848 'repo_description': schema_data['repo_description'],
849 'repo_private': schema_data['repo_private'],
849 'repo_private': schema_data['repo_private'],
850 'clone_uri': schema_data['repo_clone_uri'],
850 'clone_uri': schema_data['repo_clone_uri'],
851 'push_uri': schema_data['repo_push_uri'],
851 'push_uri': schema_data['repo_push_uri'],
852 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
852 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
853 'enable_statistics': schema_data['repo_enable_statistics'],
853 'enable_statistics': schema_data['repo_enable_statistics'],
854 'enable_locking': schema_data['repo_enable_locking'],
854 'enable_locking': schema_data['repo_enable_locking'],
855 'enable_downloads': schema_data['repo_enable_downloads'],
855 'enable_downloads': schema_data['repo_enable_downloads'],
856 'repo_copy_permissions': schema_data['repo_copy_permissions'],
856 'repo_copy_permissions': schema_data['repo_copy_permissions'],
857 }
857 }
858
858
859 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
859 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
860 task_id = get_task_id(task)
860 task_id = get_task_id(task)
861 # no commit, it's done in RepoModel, or async via celery
861 # no commit, it's done in RepoModel, or async via celery
862 return {
862 return {
863 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
863 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
864 'success': True, # cannot return the repo data here since fork
864 'success': True, # cannot return the repo data here since fork
865 # can be done async
865 # can be done async
866 'task': task_id
866 'task': task_id
867 }
867 }
868 except Exception:
868 except Exception:
869 log.exception(
869 log.exception(
870 u"Exception while trying to create the repository %s",
870 u"Exception while trying to create the repository %s",
871 schema_data['repo_name'])
871 schema_data['repo_name'])
872 raise JSONRPCError(
872 raise JSONRPCError(
873 'failed to create repository `%s`' % (schema_data['repo_name'],))
873 'failed to create repository `%s`' % (schema_data['repo_name'],))
874
874
875
875
876 @jsonrpc_method()
876 @jsonrpc_method()
877 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
877 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
878 description=Optional('')):
878 description=Optional('')):
879 """
879 """
880 Adds an extra field to a repository.
880 Adds an extra field to a repository.
881
881
882 This command can only be run using an |authtoken| with at least
882 This command can only be run using an |authtoken| with at least
883 write permissions to the |repo|.
883 write permissions to the |repo|.
884
884
885 :param apiuser: This is filled automatically from the |authtoken|.
885 :param apiuser: This is filled automatically from the |authtoken|.
886 :type apiuser: AuthUser
886 :type apiuser: AuthUser
887 :param repoid: Set the repository name or repository id.
887 :param repoid: Set the repository name or repository id.
888 :type repoid: str or int
888 :type repoid: str or int
889 :param key: Create a unique field key for this repository.
889 :param key: Create a unique field key for this repository.
890 :type key: str
890 :type key: str
891 :param label:
891 :param label:
892 :type label: Optional(str)
892 :type label: Optional(str)
893 :param description:
893 :param description:
894 :type description: Optional(str)
894 :type description: Optional(str)
895 """
895 """
896 repo = get_repo_or_error(repoid)
896 repo = get_repo_or_error(repoid)
897 if not has_superadmin_permission(apiuser):
897 if not has_superadmin_permission(apiuser):
898 _perms = ('repository.admin',)
898 _perms = ('repository.admin',)
899 validate_repo_permissions(apiuser, repoid, repo, _perms)
899 validate_repo_permissions(apiuser, repoid, repo, _perms)
900
900
901 label = Optional.extract(label) or key
901 label = Optional.extract(label) or key
902 description = Optional.extract(description)
902 description = Optional.extract(description)
903
903
904 field = RepositoryField.get_by_key_name(key, repo)
904 field = RepositoryField.get_by_key_name(key, repo)
905 if field:
905 if field:
906 raise JSONRPCError('Field with key '
906 raise JSONRPCError('Field with key '
907 '`%s` exists for repo `%s`' % (key, repoid))
907 '`%s` exists for repo `%s`' % (key, repoid))
908
908
909 try:
909 try:
910 RepoModel().add_repo_field(repo, key, field_label=label,
910 RepoModel().add_repo_field(repo, key, field_label=label,
911 field_desc=description)
911 field_desc=description)
912 Session().commit()
912 Session().commit()
913 return {
913 return {
914 'msg': "Added new repository field `%s`" % (key,),
914 'msg': "Added new repository field `%s`" % (key,),
915 'success': True,
915 'success': True,
916 }
916 }
917 except Exception:
917 except Exception:
918 log.exception("Exception occurred while trying to add field to repo")
918 log.exception("Exception occurred while trying to add field to repo")
919 raise JSONRPCError(
919 raise JSONRPCError(
920 'failed to create new field for repository `%s`' % (repoid,))
920 'failed to create new field for repository `%s`' % (repoid,))
921
921
922
922
923 @jsonrpc_method()
923 @jsonrpc_method()
924 def remove_field_from_repo(request, apiuser, repoid, key):
924 def remove_field_from_repo(request, apiuser, repoid, key):
925 """
925 """
926 Removes an extra field from a repository.
926 Removes an extra field from a repository.
927
927
928 This command can only be run using an |authtoken| with at least
928 This command can only be run using an |authtoken| with at least
929 write permissions to the |repo|.
929 write permissions to the |repo|.
930
930
931 :param apiuser: This is filled automatically from the |authtoken|.
931 :param apiuser: This is filled automatically from the |authtoken|.
932 :type apiuser: AuthUser
932 :type apiuser: AuthUser
933 :param repoid: Set the repository name or repository ID.
933 :param repoid: Set the repository name or repository ID.
934 :type repoid: str or int
934 :type repoid: str or int
935 :param key: Set the unique field key for this repository.
935 :param key: Set the unique field key for this repository.
936 :type key: str
936 :type key: str
937 """
937 """
938
938
939 repo = get_repo_or_error(repoid)
939 repo = get_repo_or_error(repoid)
940 if not has_superadmin_permission(apiuser):
940 if not has_superadmin_permission(apiuser):
941 _perms = ('repository.admin',)
941 _perms = ('repository.admin',)
942 validate_repo_permissions(apiuser, repoid, repo, _perms)
942 validate_repo_permissions(apiuser, repoid, repo, _perms)
943
943
944 field = RepositoryField.get_by_key_name(key, repo)
944 field = RepositoryField.get_by_key_name(key, repo)
945 if not field:
945 if not field:
946 raise JSONRPCError('Field with key `%s` does not '
946 raise JSONRPCError('Field with key `%s` does not '
947 'exists for repo `%s`' % (key, repoid))
947 'exists for repo `%s`' % (key, repoid))
948
948
949 try:
949 try:
950 RepoModel().delete_repo_field(repo, field_key=key)
950 RepoModel().delete_repo_field(repo, field_key=key)
951 Session().commit()
951 Session().commit()
952 return {
952 return {
953 'msg': "Deleted repository field `%s`" % (key,),
953 'msg': "Deleted repository field `%s`" % (key,),
954 'success': True,
954 'success': True,
955 }
955 }
956 except Exception:
956 except Exception:
957 log.exception(
957 log.exception(
958 "Exception occurred while trying to delete field from repo")
958 "Exception occurred while trying to delete field from repo")
959 raise JSONRPCError(
959 raise JSONRPCError(
960 'failed to delete field for repository `%s`' % (repoid,))
960 'failed to delete field for repository `%s`' % (repoid,))
961
961
962
962
963 @jsonrpc_method()
963 @jsonrpc_method()
964 def update_repo(
964 def update_repo(
965 request, apiuser, repoid, repo_name=Optional(None),
965 request, apiuser, repoid, repo_name=Optional(None),
966 owner=Optional(OAttr('apiuser')), description=Optional(''),
966 owner=Optional(OAttr('apiuser')), description=Optional(''),
967 private=Optional(False),
967 private=Optional(False),
968 clone_uri=Optional(None), push_uri=Optional(None),
968 clone_uri=Optional(None), push_uri=Optional(None),
969 landing_rev=Optional(None), fork_of=Optional(None),
969 landing_rev=Optional(None), fork_of=Optional(None),
970 enable_statistics=Optional(False),
970 enable_statistics=Optional(False),
971 enable_locking=Optional(False),
971 enable_locking=Optional(False),
972 enable_downloads=Optional(False), fields=Optional('')):
972 enable_downloads=Optional(False), fields=Optional('')):
973 """
973 """
974 Updates a repository with the given information.
974 Updates a repository with the given information.
975
975
976 This command can only be run using an |authtoken| with at least
976 This command can only be run using an |authtoken| with at least
977 admin permissions to the |repo|.
977 admin permissions to the |repo|.
978
978
979 * If the repository name contains "/", repository will be updated
979 * If the repository name contains "/", repository will be updated
980 accordingly with a repository group or nested repository groups
980 accordingly with a repository group or nested repository groups
981
981
982 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
982 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
983 called "repo-test" and place it inside group "foo/bar".
983 called "repo-test" and place it inside group "foo/bar".
984 You have to have permissions to access and write to the last repository
984 You have to have permissions to access and write to the last repository
985 group ("bar" in this example)
985 group ("bar" in this example)
986
986
987 :param apiuser: This is filled automatically from the |authtoken|.
987 :param apiuser: This is filled automatically from the |authtoken|.
988 :type apiuser: AuthUser
988 :type apiuser: AuthUser
989 :param repoid: repository name or repository ID.
989 :param repoid: repository name or repository ID.
990 :type repoid: str or int
990 :type repoid: str or int
991 :param repo_name: Update the |repo| name, including the
991 :param repo_name: Update the |repo| name, including the
992 repository group it's in.
992 repository group it's in.
993 :type repo_name: str
993 :type repo_name: str
994 :param owner: Set the |repo| owner.
994 :param owner: Set the |repo| owner.
995 :type owner: str
995 :type owner: str
996 :param fork_of: Set the |repo| as fork of another |repo|.
996 :param fork_of: Set the |repo| as fork of another |repo|.
997 :type fork_of: str
997 :type fork_of: str
998 :param description: Update the |repo| description.
998 :param description: Update the |repo| description.
999 :type description: str
999 :type description: str
1000 :param private: Set the |repo| as private. (True | False)
1000 :param private: Set the |repo| as private. (True | False)
1001 :type private: bool
1001 :type private: bool
1002 :param clone_uri: Update the |repo| clone URI.
1002 :param clone_uri: Update the |repo| clone URI.
1003 :type clone_uri: str
1003 :type clone_uri: str
1004 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1004 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1005 :type landing_rev: str
1005 :type landing_rev: str
1006 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1006 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1007 :type enable_statistics: bool
1007 :type enable_statistics: bool
1008 :param enable_locking: Enable |repo| locking.
1008 :param enable_locking: Enable |repo| locking.
1009 :type enable_locking: bool
1009 :type enable_locking: bool
1010 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1010 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1011 :type enable_downloads: bool
1011 :type enable_downloads: bool
1012 :param fields: Add extra fields to the |repo|. Use the following
1012 :param fields: Add extra fields to the |repo|. Use the following
1013 example format: ``field_key=field_val,field_key2=fieldval2``.
1013 example format: ``field_key=field_val,field_key2=fieldval2``.
1014 Escape ', ' with \,
1014 Escape ', ' with \,
1015 :type fields: str
1015 :type fields: str
1016 """
1016 """
1017
1017
1018 repo = get_repo_or_error(repoid)
1018 repo = get_repo_or_error(repoid)
1019
1019
1020 include_secrets = False
1020 include_secrets = False
1021 if not has_superadmin_permission(apiuser):
1021 if not has_superadmin_permission(apiuser):
1022 _perms = ('repository.admin',)
1022 _perms = ('repository.admin',)
1023 validate_repo_permissions(apiuser, repoid, repo, _perms)
1023 validate_repo_permissions(apiuser, repoid, repo, _perms)
1024 else:
1024 else:
1025 include_secrets = True
1025 include_secrets = True
1026
1026
1027 updates = dict(
1027 updates = dict(
1028 repo_name=repo_name
1028 repo_name=repo_name
1029 if not isinstance(repo_name, Optional) else repo.repo_name,
1029 if not isinstance(repo_name, Optional) else repo.repo_name,
1030
1030
1031 fork_id=fork_of
1031 fork_id=fork_of
1032 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1032 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1033
1033
1034 user=owner
1034 user=owner
1035 if not isinstance(owner, Optional) else repo.user.username,
1035 if not isinstance(owner, Optional) else repo.user.username,
1036
1036
1037 repo_description=description
1037 repo_description=description
1038 if not isinstance(description, Optional) else repo.description,
1038 if not isinstance(description, Optional) else repo.description,
1039
1039
1040 repo_private=private
1040 repo_private=private
1041 if not isinstance(private, Optional) else repo.private,
1041 if not isinstance(private, Optional) else repo.private,
1042
1042
1043 clone_uri=clone_uri
1043 clone_uri=clone_uri
1044 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1044 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1045
1045
1046 push_uri=push_uri
1046 push_uri=push_uri
1047 if not isinstance(push_uri, Optional) else repo.push_uri,
1047 if not isinstance(push_uri, Optional) else repo.push_uri,
1048
1048
1049 repo_landing_rev=landing_rev
1049 repo_landing_rev=landing_rev
1050 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1050 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1051
1051
1052 repo_enable_statistics=enable_statistics
1052 repo_enable_statistics=enable_statistics
1053 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1053 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1054
1054
1055 repo_enable_locking=enable_locking
1055 repo_enable_locking=enable_locking
1056 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1056 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1057
1057
1058 repo_enable_downloads=enable_downloads
1058 repo_enable_downloads=enable_downloads
1059 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1059 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1060
1060
1061 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1061 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1062 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1062 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1063 request.translate, repo=repo)
1063 request.translate, repo=repo)
1064 ref_choices = list(set(ref_choices + [landing_ref]))
1064 ref_choices = list(set(ref_choices + [landing_ref]))
1065
1065
1066 old_values = repo.get_api_data()
1066 old_values = repo.get_api_data()
1067 repo_type = repo.repo_type
1067 repo_type = repo.repo_type
1068 schema = repo_schema.RepoSchema().bind(
1068 schema = repo_schema.RepoSchema().bind(
1069 repo_type_options=rhodecode.BACKENDS.keys(),
1069 repo_type_options=rhodecode.BACKENDS.keys(),
1070 repo_ref_options=ref_choices,
1070 repo_ref_options=ref_choices,
1071 repo_type=repo_type,
1071 repo_type=repo_type,
1072 # user caller
1072 # user caller
1073 user=apiuser,
1073 user=apiuser,
1074 old_values=old_values)
1074 old_values=old_values)
1075 try:
1075 try:
1076 schema_data = schema.deserialize(dict(
1076 schema_data = schema.deserialize(dict(
1077 # we save old value, users cannot change type
1077 # we save old value, users cannot change type
1078 repo_type=repo_type,
1078 repo_type=repo_type,
1079
1079
1080 repo_name=updates['repo_name'],
1080 repo_name=updates['repo_name'],
1081 repo_owner=updates['user'],
1081 repo_owner=updates['user'],
1082 repo_description=updates['repo_description'],
1082 repo_description=updates['repo_description'],
1083 repo_clone_uri=updates['clone_uri'],
1083 repo_clone_uri=updates['clone_uri'],
1084 repo_push_uri=updates['push_uri'],
1084 repo_push_uri=updates['push_uri'],
1085 repo_fork_of=updates['fork_id'],
1085 repo_fork_of=updates['fork_id'],
1086 repo_private=updates['repo_private'],
1086 repo_private=updates['repo_private'],
1087 repo_landing_commit_ref=updates['repo_landing_rev'],
1087 repo_landing_commit_ref=updates['repo_landing_rev'],
1088 repo_enable_statistics=updates['repo_enable_statistics'],
1088 repo_enable_statistics=updates['repo_enable_statistics'],
1089 repo_enable_downloads=updates['repo_enable_downloads'],
1089 repo_enable_downloads=updates['repo_enable_downloads'],
1090 repo_enable_locking=updates['repo_enable_locking']))
1090 repo_enable_locking=updates['repo_enable_locking']))
1091 except validation_schema.Invalid as err:
1091 except validation_schema.Invalid as err:
1092 raise JSONRPCValidationError(colander_exc=err)
1092 raise JSONRPCValidationError(colander_exc=err)
1093
1093
1094 # save validated data back into the updates dict
1094 # save validated data back into the updates dict
1095 validated_updates = dict(
1095 validated_updates = dict(
1096 repo_name=schema_data['repo_group']['repo_name_without_group'],
1096 repo_name=schema_data['repo_group']['repo_name_without_group'],
1097 repo_group=schema_data['repo_group']['repo_group_id'],
1097 repo_group=schema_data['repo_group']['repo_group_id'],
1098
1098
1099 user=schema_data['repo_owner'],
1099 user=schema_data['repo_owner'],
1100 repo_description=schema_data['repo_description'],
1100 repo_description=schema_data['repo_description'],
1101 repo_private=schema_data['repo_private'],
1101 repo_private=schema_data['repo_private'],
1102 clone_uri=schema_data['repo_clone_uri'],
1102 clone_uri=schema_data['repo_clone_uri'],
1103 push_uri=schema_data['repo_push_uri'],
1103 push_uri=schema_data['repo_push_uri'],
1104 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1104 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1105 repo_enable_statistics=schema_data['repo_enable_statistics'],
1105 repo_enable_statistics=schema_data['repo_enable_statistics'],
1106 repo_enable_locking=schema_data['repo_enable_locking'],
1106 repo_enable_locking=schema_data['repo_enable_locking'],
1107 repo_enable_downloads=schema_data['repo_enable_downloads'],
1107 repo_enable_downloads=schema_data['repo_enable_downloads'],
1108 )
1108 )
1109
1109
1110 if schema_data['repo_fork_of']:
1110 if schema_data['repo_fork_of']:
1111 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1111 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1112 validated_updates['fork_id'] = fork_repo.repo_id
1112 validated_updates['fork_id'] = fork_repo.repo_id
1113
1113
1114 # extra fields
1114 # extra fields
1115 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1115 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1116 if fields:
1116 if fields:
1117 validated_updates.update(fields)
1117 validated_updates.update(fields)
1118
1118
1119 try:
1119 try:
1120 RepoModel().update(repo, **validated_updates)
1120 RepoModel().update(repo, **validated_updates)
1121 audit_logger.store_api(
1121 audit_logger.store_api(
1122 'repo.edit', action_data={'old_data': old_values},
1122 'repo.edit', action_data={'old_data': old_values},
1123 user=apiuser, repo=repo)
1123 user=apiuser, repo=repo)
1124 Session().commit()
1124 Session().commit()
1125 return {
1125 return {
1126 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1126 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1127 'repository': repo.get_api_data(include_secrets=include_secrets)
1127 'repository': repo.get_api_data(include_secrets=include_secrets)
1128 }
1128 }
1129 except Exception:
1129 except Exception:
1130 log.exception(
1130 log.exception(
1131 u"Exception while trying to update the repository %s",
1131 u"Exception while trying to update the repository %s",
1132 repoid)
1132 repoid)
1133 raise JSONRPCError('failed to update repo `%s`' % repoid)
1133 raise JSONRPCError('failed to update repo `%s`' % repoid)
1134
1134
1135
1135
1136 @jsonrpc_method()
1136 @jsonrpc_method()
1137 def fork_repo(request, apiuser, repoid, fork_name,
1137 def fork_repo(request, apiuser, repoid, fork_name,
1138 owner=Optional(OAttr('apiuser')),
1138 owner=Optional(OAttr('apiuser')),
1139 description=Optional(''),
1139 description=Optional(''),
1140 private=Optional(False),
1140 private=Optional(False),
1141 clone_uri=Optional(None),
1141 clone_uri=Optional(None),
1142 landing_rev=Optional(None),
1142 landing_rev=Optional(None),
1143 copy_permissions=Optional(False)):
1143 copy_permissions=Optional(False)):
1144 """
1144 """
1145 Creates a fork of the specified |repo|.
1145 Creates a fork of the specified |repo|.
1146
1146
1147 * If the fork_name contains "/", fork will be created inside
1147 * If the fork_name contains "/", fork will be created inside
1148 a repository group or nested repository groups
1148 a repository group or nested repository groups
1149
1149
1150 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1150 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1151 inside group "foo/bar". You have to have permissions to access and
1151 inside group "foo/bar". You have to have permissions to access and
1152 write to the last repository group ("bar" in this example)
1152 write to the last repository group ("bar" in this example)
1153
1153
1154 This command can only be run using an |authtoken| with minimum
1154 This command can only be run using an |authtoken| with minimum
1155 read permissions of the forked repo, create fork permissions for an user.
1155 read permissions of the forked repo, create fork permissions for an user.
1156
1156
1157 :param apiuser: This is filled automatically from the |authtoken|.
1157 :param apiuser: This is filled automatically from the |authtoken|.
1158 :type apiuser: AuthUser
1158 :type apiuser: AuthUser
1159 :param repoid: Set repository name or repository ID.
1159 :param repoid: Set repository name or repository ID.
1160 :type repoid: str or int
1160 :type repoid: str or int
1161 :param fork_name: Set the fork name, including it's repository group membership.
1161 :param fork_name: Set the fork name, including it's repository group membership.
1162 :type fork_name: str
1162 :type fork_name: str
1163 :param owner: Set the fork owner.
1163 :param owner: Set the fork owner.
1164 :type owner: str
1164 :type owner: str
1165 :param description: Set the fork description.
1165 :param description: Set the fork description.
1166 :type description: str
1166 :type description: str
1167 :param copy_permissions: Copy permissions from parent |repo|. The
1167 :param copy_permissions: Copy permissions from parent |repo|. The
1168 default is False.
1168 default is False.
1169 :type copy_permissions: bool
1169 :type copy_permissions: bool
1170 :param private: Make the fork private. The default is False.
1170 :param private: Make the fork private. The default is False.
1171 :type private: bool
1171 :type private: bool
1172 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1172 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1173
1173
1174 Example output:
1174 Example output:
1175
1175
1176 .. code-block:: bash
1176 .. code-block:: bash
1177
1177
1178 id : <id_for_response>
1178 id : <id_for_response>
1179 api_key : "<api_key>"
1179 api_key : "<api_key>"
1180 args: {
1180 args: {
1181 "repoid" : "<reponame or repo_id>",
1181 "repoid" : "<reponame or repo_id>",
1182 "fork_name": "<forkname>",
1182 "fork_name": "<forkname>",
1183 "owner": "<username or user_id = Optional(=apiuser)>",
1183 "owner": "<username or user_id = Optional(=apiuser)>",
1184 "description": "<description>",
1184 "description": "<description>",
1185 "copy_permissions": "<bool>",
1185 "copy_permissions": "<bool>",
1186 "private": "<bool>",
1186 "private": "<bool>",
1187 "landing_rev": "<landing_rev>"
1187 "landing_rev": "<landing_rev>"
1188 }
1188 }
1189
1189
1190 Example error output:
1190 Example error output:
1191
1191
1192 .. code-block:: bash
1192 .. code-block:: bash
1193
1193
1194 id : <id_given_in_input>
1194 id : <id_given_in_input>
1195 result: {
1195 result: {
1196 "msg": "Created fork of `<reponame>` as `<forkname>`",
1196 "msg": "Created fork of `<reponame>` as `<forkname>`",
1197 "success": true,
1197 "success": true,
1198 "task": "<celery task id or None if done sync>"
1198 "task": "<celery task id or None if done sync>"
1199 }
1199 }
1200 error: null
1200 error: null
1201
1201
1202 """
1202 """
1203
1203
1204 repo = get_repo_or_error(repoid)
1204 repo = get_repo_or_error(repoid)
1205 repo_name = repo.repo_name
1205 repo_name = repo.repo_name
1206
1206
1207 if not has_superadmin_permission(apiuser):
1207 if not has_superadmin_permission(apiuser):
1208 # check if we have at least read permission for
1208 # check if we have at least read permission for
1209 # this repo that we fork !
1209 # this repo that we fork !
1210 _perms = ('repository.admin', 'repository.write', 'repository.read')
1210 _perms = ('repository.admin', 'repository.write', 'repository.read')
1211 validate_repo_permissions(apiuser, repoid, repo, _perms)
1211 validate_repo_permissions(apiuser, repoid, repo, _perms)
1212
1212
1213 # check if the regular user has at least fork permissions as well
1213 # check if the regular user has at least fork permissions as well
1214 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1214 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1215 raise JSONRPCForbidden()
1215 raise JSONRPCForbidden()
1216
1216
1217 # check if user can set owner parameter
1217 # check if user can set owner parameter
1218 owner = validate_set_owner_permissions(apiuser, owner)
1218 owner = validate_set_owner_permissions(apiuser, owner)
1219
1219
1220 description = Optional.extract(description)
1220 description = Optional.extract(description)
1221 copy_permissions = Optional.extract(copy_permissions)
1221 copy_permissions = Optional.extract(copy_permissions)
1222 clone_uri = Optional.extract(clone_uri)
1222 clone_uri = Optional.extract(clone_uri)
1223
1223
1224 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1224 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1225 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1225 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1226 ref_choices = list(set(ref_choices + [landing_ref]))
1226 ref_choices = list(set(ref_choices + [landing_ref]))
1227 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1227 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1228
1228
1229 private = Optional.extract(private)
1229 private = Optional.extract(private)
1230
1230
1231 schema = repo_schema.RepoSchema().bind(
1231 schema = repo_schema.RepoSchema().bind(
1232 repo_type_options=rhodecode.BACKENDS.keys(),
1232 repo_type_options=rhodecode.BACKENDS.keys(),
1233 repo_ref_options=ref_choices,
1233 repo_ref_options=ref_choices,
1234 repo_type=repo.repo_type,
1234 repo_type=repo.repo_type,
1235 # user caller
1235 # user caller
1236 user=apiuser)
1236 user=apiuser)
1237
1237
1238 try:
1238 try:
1239 schema_data = schema.deserialize(dict(
1239 schema_data = schema.deserialize(dict(
1240 repo_name=fork_name,
1240 repo_name=fork_name,
1241 repo_type=repo.repo_type,
1241 repo_type=repo.repo_type,
1242 repo_owner=owner.username,
1242 repo_owner=owner.username,
1243 repo_description=description,
1243 repo_description=description,
1244 repo_landing_commit_ref=landing_commit_ref,
1244 repo_landing_commit_ref=landing_commit_ref,
1245 repo_clone_uri=clone_uri,
1245 repo_clone_uri=clone_uri,
1246 repo_private=private,
1246 repo_private=private,
1247 repo_copy_permissions=copy_permissions))
1247 repo_copy_permissions=copy_permissions))
1248 except validation_schema.Invalid as err:
1248 except validation_schema.Invalid as err:
1249 raise JSONRPCValidationError(colander_exc=err)
1249 raise JSONRPCValidationError(colander_exc=err)
1250
1250
1251 try:
1251 try:
1252 data = {
1252 data = {
1253 'fork_parent_id': repo.repo_id,
1253 'fork_parent_id': repo.repo_id,
1254
1254
1255 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1255 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1256 'repo_name_full': schema_data['repo_name'],
1256 'repo_name_full': schema_data['repo_name'],
1257 'repo_group': schema_data['repo_group']['repo_group_id'],
1257 'repo_group': schema_data['repo_group']['repo_group_id'],
1258 'repo_type': schema_data['repo_type'],
1258 'repo_type': schema_data['repo_type'],
1259 'description': schema_data['repo_description'],
1259 'description': schema_data['repo_description'],
1260 'private': schema_data['repo_private'],
1260 'private': schema_data['repo_private'],
1261 'copy_permissions': schema_data['repo_copy_permissions'],
1261 'copy_permissions': schema_data['repo_copy_permissions'],
1262 'landing_rev': schema_data['repo_landing_commit_ref'],
1262 'landing_rev': schema_data['repo_landing_commit_ref'],
1263 }
1263 }
1264
1264
1265 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1265 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1266 # no commit, it's done in RepoModel, or async via celery
1266 # no commit, it's done in RepoModel, or async via celery
1267 task_id = get_task_id(task)
1267 task_id = get_task_id(task)
1268
1268
1269 return {
1269 return {
1270 'msg': 'Created fork of `%s` as `%s`' % (
1270 'msg': 'Created fork of `%s` as `%s`' % (
1271 repo.repo_name, schema_data['repo_name']),
1271 repo.repo_name, schema_data['repo_name']),
1272 'success': True, # cannot return the repo data here since fork
1272 'success': True, # cannot return the repo data here since fork
1273 # can be done async
1273 # can be done async
1274 'task': task_id
1274 'task': task_id
1275 }
1275 }
1276 except Exception:
1276 except Exception:
1277 log.exception(
1277 log.exception(
1278 u"Exception while trying to create fork %s",
1278 u"Exception while trying to create fork %s",
1279 schema_data['repo_name'])
1279 schema_data['repo_name'])
1280 raise JSONRPCError(
1280 raise JSONRPCError(
1281 'failed to fork repository `%s` as `%s`' % (
1281 'failed to fork repository `%s` as `%s`' % (
1282 repo_name, schema_data['repo_name']))
1282 repo_name, schema_data['repo_name']))
1283
1283
1284
1284
1285 @jsonrpc_method()
1285 @jsonrpc_method()
1286 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1286 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1287 """
1287 """
1288 Deletes a repository.
1288 Deletes a repository.
1289
1289
1290 * When the `forks` parameter is set it's possible to detach or delete
1290 * When the `forks` parameter is set it's possible to detach or delete
1291 forks of deleted repository.
1291 forks of deleted repository.
1292
1292
1293 This command can only be run using an |authtoken| with admin
1293 This command can only be run using an |authtoken| with admin
1294 permissions on the |repo|.
1294 permissions on the |repo|.
1295
1295
1296 :param apiuser: This is filled automatically from the |authtoken|.
1296 :param apiuser: This is filled automatically from the |authtoken|.
1297 :type apiuser: AuthUser
1297 :type apiuser: AuthUser
1298 :param repoid: Set the repository name or repository ID.
1298 :param repoid: Set the repository name or repository ID.
1299 :type repoid: str or int
1299 :type repoid: str or int
1300 :param forks: Set to `detach` or `delete` forks from the |repo|.
1300 :param forks: Set to `detach` or `delete` forks from the |repo|.
1301 :type forks: Optional(str)
1301 :type forks: Optional(str)
1302
1302
1303 Example error output:
1303 Example error output:
1304
1304
1305 .. code-block:: bash
1305 .. code-block:: bash
1306
1306
1307 id : <id_given_in_input>
1307 id : <id_given_in_input>
1308 result: {
1308 result: {
1309 "msg": "Deleted repository `<reponame>`",
1309 "msg": "Deleted repository `<reponame>`",
1310 "success": true
1310 "success": true
1311 }
1311 }
1312 error: null
1312 error: null
1313 """
1313 """
1314
1314
1315 repo = get_repo_or_error(repoid)
1315 repo = get_repo_or_error(repoid)
1316 repo_name = repo.repo_name
1316 repo_name = repo.repo_name
1317 if not has_superadmin_permission(apiuser):
1317 if not has_superadmin_permission(apiuser):
1318 _perms = ('repository.admin',)
1318 _perms = ('repository.admin',)
1319 validate_repo_permissions(apiuser, repoid, repo, _perms)
1319 validate_repo_permissions(apiuser, repoid, repo, _perms)
1320
1320
1321 try:
1321 try:
1322 handle_forks = Optional.extract(forks)
1322 handle_forks = Optional.extract(forks)
1323 _forks_msg = ''
1323 _forks_msg = ''
1324 _forks = [f for f in repo.forks]
1324 _forks = [f for f in repo.forks]
1325 if handle_forks == 'detach':
1325 if handle_forks == 'detach':
1326 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1326 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1327 elif handle_forks == 'delete':
1327 elif handle_forks == 'delete':
1328 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1328 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1329 elif _forks:
1329 elif _forks:
1330 raise JSONRPCError(
1330 raise JSONRPCError(
1331 'Cannot delete `%s` it still contains attached forks' %
1331 'Cannot delete `%s` it still contains attached forks' %
1332 (repo.repo_name,)
1332 (repo.repo_name,)
1333 )
1333 )
1334 old_data = repo.get_api_data()
1334 old_data = repo.get_api_data()
1335 RepoModel().delete(repo, forks=forks)
1335 RepoModel().delete(repo, forks=forks)
1336
1336
1337 repo = audit_logger.RepoWrap(repo_id=None,
1337 repo = audit_logger.RepoWrap(repo_id=None,
1338 repo_name=repo.repo_name)
1338 repo_name=repo.repo_name)
1339
1339
1340 audit_logger.store_api(
1340 audit_logger.store_api(
1341 'repo.delete', action_data={'old_data': old_data},
1341 'repo.delete', action_data={'old_data': old_data},
1342 user=apiuser, repo=repo)
1342 user=apiuser, repo=repo)
1343
1343
1344 ScmModel().mark_for_invalidation(repo_name, delete=True)
1344 ScmModel().mark_for_invalidation(repo_name, delete=True)
1345 Session().commit()
1345 Session().commit()
1346 return {
1346 return {
1347 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1347 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1348 'success': True
1348 'success': True
1349 }
1349 }
1350 except Exception:
1350 except Exception:
1351 log.exception("Exception occurred while trying to delete repo")
1351 log.exception("Exception occurred while trying to delete repo")
1352 raise JSONRPCError(
1352 raise JSONRPCError(
1353 'failed to delete repository `%s`' % (repo_name,)
1353 'failed to delete repository `%s`' % (repo_name,)
1354 )
1354 )
1355
1355
1356
1356
1357 #TODO: marcink, change name ?
1357 #TODO: marcink, change name ?
1358 @jsonrpc_method()
1358 @jsonrpc_method()
1359 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1359 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1360 """
1360 """
1361 Invalidates the cache for the specified repository.
1361 Invalidates the cache for the specified repository.
1362
1362
1363 This command can only be run using an |authtoken| with admin rights to
1363 This command can only be run using an |authtoken| with admin rights to
1364 the specified repository.
1364 the specified repository.
1365
1365
1366 This command takes the following options:
1366 This command takes the following options:
1367
1367
1368 :param apiuser: This is filled automatically from |authtoken|.
1368 :param apiuser: This is filled automatically from |authtoken|.
1369 :type apiuser: AuthUser
1369 :type apiuser: AuthUser
1370 :param repoid: Sets the repository name or repository ID.
1370 :param repoid: Sets the repository name or repository ID.
1371 :type repoid: str or int
1371 :type repoid: str or int
1372 :param delete_keys: This deletes the invalidated keys instead of
1372 :param delete_keys: This deletes the invalidated keys instead of
1373 just flagging them.
1373 just flagging them.
1374 :type delete_keys: Optional(``True`` | ``False``)
1374 :type delete_keys: Optional(``True`` | ``False``)
1375
1375
1376 Example output:
1376 Example output:
1377
1377
1378 .. code-block:: bash
1378 .. code-block:: bash
1379
1379
1380 id : <id_given_in_input>
1380 id : <id_given_in_input>
1381 result : {
1381 result : {
1382 'msg': Cache for repository `<repository name>` was invalidated,
1382 'msg': Cache for repository `<repository name>` was invalidated,
1383 'repository': <repository name>
1383 'repository': <repository name>
1384 }
1384 }
1385 error : null
1385 error : null
1386
1386
1387 Example error output:
1387 Example error output:
1388
1388
1389 .. code-block:: bash
1389 .. code-block:: bash
1390
1390
1391 id : <id_given_in_input>
1391 id : <id_given_in_input>
1392 result : null
1392 result : null
1393 error : {
1393 error : {
1394 'Error occurred during cache invalidation action'
1394 'Error occurred during cache invalidation action'
1395 }
1395 }
1396
1396
1397 """
1397 """
1398
1398
1399 repo = get_repo_or_error(repoid)
1399 repo = get_repo_or_error(repoid)
1400 if not has_superadmin_permission(apiuser):
1400 if not has_superadmin_permission(apiuser):
1401 _perms = ('repository.admin', 'repository.write',)
1401 _perms = ('repository.admin', 'repository.write',)
1402 validate_repo_permissions(apiuser, repoid, repo, _perms)
1402 validate_repo_permissions(apiuser, repoid, repo, _perms)
1403
1403
1404 delete = Optional.extract(delete_keys)
1404 delete = Optional.extract(delete_keys)
1405 try:
1405 try:
1406 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1406 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1407 return {
1407 return {
1408 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1408 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1409 'repository': repo.repo_name
1409 'repository': repo.repo_name
1410 }
1410 }
1411 except Exception:
1411 except Exception:
1412 log.exception(
1412 log.exception(
1413 "Exception occurred while trying to invalidate repo cache")
1413 "Exception occurred while trying to invalidate repo cache")
1414 raise JSONRPCError(
1414 raise JSONRPCError(
1415 'Error occurred during cache invalidation action'
1415 'Error occurred during cache invalidation action'
1416 )
1416 )
1417
1417
1418
1418
1419 #TODO: marcink, change name ?
1419 #TODO: marcink, change name ?
1420 @jsonrpc_method()
1420 @jsonrpc_method()
1421 def lock(request, apiuser, repoid, locked=Optional(None),
1421 def lock(request, apiuser, repoid, locked=Optional(None),
1422 userid=Optional(OAttr('apiuser'))):
1422 userid=Optional(OAttr('apiuser'))):
1423 """
1423 """
1424 Sets the lock state of the specified |repo| by the given user.
1424 Sets the lock state of the specified |repo| by the given user.
1425 From more information, see :ref:`repo-locking`.
1425 From more information, see :ref:`repo-locking`.
1426
1426
1427 * If the ``userid`` option is not set, the repository is locked to the
1427 * If the ``userid`` option is not set, the repository is locked to the
1428 user who called the method.
1428 user who called the method.
1429 * If the ``locked`` parameter is not set, the current lock state of the
1429 * If the ``locked`` parameter is not set, the current lock state of the
1430 repository is displayed.
1430 repository is displayed.
1431
1431
1432 This command can only be run using an |authtoken| with admin rights to
1432 This command can only be run using an |authtoken| with admin rights to
1433 the specified repository.
1433 the specified repository.
1434
1434
1435 This command takes the following options:
1435 This command takes the following options:
1436
1436
1437 :param apiuser: This is filled automatically from the |authtoken|.
1437 :param apiuser: This is filled automatically from the |authtoken|.
1438 :type apiuser: AuthUser
1438 :type apiuser: AuthUser
1439 :param repoid: Sets the repository name or repository ID.
1439 :param repoid: Sets the repository name or repository ID.
1440 :type repoid: str or int
1440 :type repoid: str or int
1441 :param locked: Sets the lock state.
1441 :param locked: Sets the lock state.
1442 :type locked: Optional(``True`` | ``False``)
1442 :type locked: Optional(``True`` | ``False``)
1443 :param userid: Set the repository lock to this user.
1443 :param userid: Set the repository lock to this user.
1444 :type userid: Optional(str or int)
1444 :type userid: Optional(str or int)
1445
1445
1446 Example error output:
1446 Example error output:
1447
1447
1448 .. code-block:: bash
1448 .. code-block:: bash
1449
1449
1450 id : <id_given_in_input>
1450 id : <id_given_in_input>
1451 result : {
1451 result : {
1452 'repo': '<reponame>',
1452 'repo': '<reponame>',
1453 'locked': <bool: lock state>,
1453 'locked': <bool: lock state>,
1454 'locked_since': <int: lock timestamp>,
1454 'locked_since': <int: lock timestamp>,
1455 'locked_by': <username of person who made the lock>,
1455 'locked_by': <username of person who made the lock>,
1456 'lock_reason': <str: reason for locking>,
1456 'lock_reason': <str: reason for locking>,
1457 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1457 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1458 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1458 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1459 or
1459 or
1460 'msg': 'Repo `<repository name>` not locked.'
1460 'msg': 'Repo `<repository name>` not locked.'
1461 or
1461 or
1462 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1462 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1463 }
1463 }
1464 error : null
1464 error : null
1465
1465
1466 Example error output:
1466 Example error output:
1467
1467
1468 .. code-block:: bash
1468 .. code-block:: bash
1469
1469
1470 id : <id_given_in_input>
1470 id : <id_given_in_input>
1471 result : null
1471 result : null
1472 error : {
1472 error : {
1473 'Error occurred locking repository `<reponame>`'
1473 'Error occurred locking repository `<reponame>`'
1474 }
1474 }
1475 """
1475 """
1476
1476
1477 repo = get_repo_or_error(repoid)
1477 repo = get_repo_or_error(repoid)
1478 if not has_superadmin_permission(apiuser):
1478 if not has_superadmin_permission(apiuser):
1479 # check if we have at least write permission for this repo !
1479 # check if we have at least write permission for this repo !
1480 _perms = ('repository.admin', 'repository.write',)
1480 _perms = ('repository.admin', 'repository.write',)
1481 validate_repo_permissions(apiuser, repoid, repo, _perms)
1481 validate_repo_permissions(apiuser, repoid, repo, _perms)
1482
1482
1483 # make sure normal user does not pass someone else userid,
1483 # make sure normal user does not pass someone else userid,
1484 # he is not allowed to do that
1484 # he is not allowed to do that
1485 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1485 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1486 raise JSONRPCError('userid is not the same as your user')
1486 raise JSONRPCError('userid is not the same as your user')
1487
1487
1488 if isinstance(userid, Optional):
1488 if isinstance(userid, Optional):
1489 userid = apiuser.user_id
1489 userid = apiuser.user_id
1490
1490
1491 user = get_user_or_error(userid)
1491 user = get_user_or_error(userid)
1492
1492
1493 if isinstance(locked, Optional):
1493 if isinstance(locked, Optional):
1494 lockobj = repo.locked
1494 lockobj = repo.locked
1495
1495
1496 if lockobj[0] is None:
1496 if lockobj[0] is None:
1497 _d = {
1497 _d = {
1498 'repo': repo.repo_name,
1498 'repo': repo.repo_name,
1499 'locked': False,
1499 'locked': False,
1500 'locked_since': None,
1500 'locked_since': None,
1501 'locked_by': None,
1501 'locked_by': None,
1502 'lock_reason': None,
1502 'lock_reason': None,
1503 'lock_state_changed': False,
1503 'lock_state_changed': False,
1504 'msg': 'Repo `%s` not locked.' % repo.repo_name
1504 'msg': 'Repo `%s` not locked.' % repo.repo_name
1505 }
1505 }
1506 return _d
1506 return _d
1507 else:
1507 else:
1508 _user_id, _time, _reason = lockobj
1508 _user_id, _time, _reason = lockobj
1509 lock_user = get_user_or_error(userid)
1509 lock_user = get_user_or_error(userid)
1510 _d = {
1510 _d = {
1511 'repo': repo.repo_name,
1511 'repo': repo.repo_name,
1512 'locked': True,
1512 'locked': True,
1513 'locked_since': _time,
1513 'locked_since': _time,
1514 'locked_by': lock_user.username,
1514 'locked_by': lock_user.username,
1515 'lock_reason': _reason,
1515 'lock_reason': _reason,
1516 'lock_state_changed': False,
1516 'lock_state_changed': False,
1517 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1517 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1518 % (repo.repo_name, lock_user.username,
1518 % (repo.repo_name, lock_user.username,
1519 json.dumps(time_to_datetime(_time))))
1519 json.dumps(time_to_datetime(_time))))
1520 }
1520 }
1521 return _d
1521 return _d
1522
1522
1523 # force locked state through a flag
1523 # force locked state through a flag
1524 else:
1524 else:
1525 locked = str2bool(locked)
1525 locked = str2bool(locked)
1526 lock_reason = Repository.LOCK_API
1526 lock_reason = Repository.LOCK_API
1527 try:
1527 try:
1528 if locked:
1528 if locked:
1529 lock_time = time.time()
1529 lock_time = time.time()
1530 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1530 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1531 else:
1531 else:
1532 lock_time = None
1532 lock_time = None
1533 Repository.unlock(repo)
1533 Repository.unlock(repo)
1534 _d = {
1534 _d = {
1535 'repo': repo.repo_name,
1535 'repo': repo.repo_name,
1536 'locked': locked,
1536 'locked': locked,
1537 'locked_since': lock_time,
1537 'locked_since': lock_time,
1538 'locked_by': user.username,
1538 'locked_by': user.username,
1539 'lock_reason': lock_reason,
1539 'lock_reason': lock_reason,
1540 'lock_state_changed': True,
1540 'lock_state_changed': True,
1541 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1541 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1542 % (user.username, repo.repo_name, locked))
1542 % (user.username, repo.repo_name, locked))
1543 }
1543 }
1544 return _d
1544 return _d
1545 except Exception:
1545 except Exception:
1546 log.exception(
1546 log.exception(
1547 "Exception occurred while trying to lock repository")
1547 "Exception occurred while trying to lock repository")
1548 raise JSONRPCError(
1548 raise JSONRPCError(
1549 'Error occurred locking repository `%s`' % repo.repo_name
1549 'Error occurred locking repository `%s`' % repo.repo_name
1550 )
1550 )
1551
1551
1552
1552
1553 @jsonrpc_method()
1553 @jsonrpc_method()
1554 def comment_commit(
1554 def comment_commit(
1555 request, apiuser, repoid, commit_id, message, status=Optional(None),
1555 request, apiuser, repoid, commit_id, message, status=Optional(None),
1556 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1556 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1557 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1557 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1558 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1558 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1559 """
1559 """
1560 Set a commit comment, and optionally change the status of the commit.
1560 Set a commit comment, and optionally change the status of the commit.
1561
1561
1562 :param apiuser: This is filled automatically from the |authtoken|.
1562 :param apiuser: This is filled automatically from the |authtoken|.
1563 :type apiuser: AuthUser
1563 :type apiuser: AuthUser
1564 :param repoid: Set the repository name or repository ID.
1564 :param repoid: Set the repository name or repository ID.
1565 :type repoid: str or int
1565 :type repoid: str or int
1566 :param commit_id: Specify the commit_id for which to set a comment.
1566 :param commit_id: Specify the commit_id for which to set a comment.
1567 :type commit_id: str
1567 :type commit_id: str
1568 :param message: The comment text.
1568 :param message: The comment text.
1569 :type message: str
1569 :type message: str
1570 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1570 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1571 'approved', 'rejected', 'under_review'
1571 'approved', 'rejected', 'under_review'
1572 :type status: str
1572 :type status: str
1573 :param comment_type: Comment type, one of: 'note', 'todo'
1573 :param comment_type: Comment type, one of: 'note', 'todo'
1574 :type comment_type: Optional(str), default: 'note'
1574 :type comment_type: Optional(str), default: 'note'
1575 :param resolves_comment_id: id of comment which this one will resolve
1575 :param resolves_comment_id: id of comment which this one will resolve
1576 :type resolves_comment_id: Optional(int)
1576 :type resolves_comment_id: Optional(int)
1577 :param extra_recipients: list of user ids or usernames to add
1577 :param extra_recipients: list of user ids or usernames to add
1578 notifications for this comment. Acts like a CC for notification
1578 notifications for this comment. Acts like a CC for notification
1579 :type extra_recipients: Optional(list)
1579 :type extra_recipients: Optional(list)
1580 :param userid: Set the user name of the comment creator.
1580 :param userid: Set the user name of the comment creator.
1581 :type userid: Optional(str or int)
1581 :type userid: Optional(str or int)
1582 :param send_email: Define if this comment should also send email notification
1582 :param send_email: Define if this comment should also send email notification
1583 :type send_email: Optional(bool)
1583 :type send_email: Optional(bool)
1584
1584
1585 Example error output:
1585 Example error output:
1586
1586
1587 .. code-block:: bash
1587 .. code-block:: bash
1588
1588
1589 {
1589 {
1590 "id" : <id_given_in_input>,
1590 "id" : <id_given_in_input>,
1591 "result" : {
1591 "result" : {
1592 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1592 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1593 "status_change": null or <status>,
1593 "status_change": null or <status>,
1594 "success": true
1594 "success": true
1595 },
1595 },
1596 "error" : null
1596 "error" : null
1597 }
1597 }
1598
1598
1599 """
1599 """
1600 _ = request.translate
1600 _ = request.translate
1601
1601
1602 repo = get_repo_or_error(repoid)
1602 repo = get_repo_or_error(repoid)
1603 if not has_superadmin_permission(apiuser):
1603 if not has_superadmin_permission(apiuser):
1604 _perms = ('repository.read', 'repository.write', 'repository.admin')
1604 _perms = ('repository.read', 'repository.write', 'repository.admin')
1605 validate_repo_permissions(apiuser, repoid, repo, _perms)
1605 validate_repo_permissions(apiuser, repoid, repo, _perms)
1606 db_repo_name = repo.repo_name
1606 db_repo_name = repo.repo_name
1607
1607
1608 try:
1608 try:
1609 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1609 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1610 commit_id = commit.raw_id
1610 commit_id = commit.raw_id
1611 except Exception as e:
1611 except Exception as e:
1612 log.exception('Failed to fetch commit')
1612 log.exception('Failed to fetch commit')
1613 raise JSONRPCError(safe_str(e))
1613 raise JSONRPCError(safe_str(e))
1614
1614
1615 if isinstance(userid, Optional):
1615 if isinstance(userid, Optional):
1616 userid = apiuser.user_id
1616 userid = apiuser.user_id
1617
1617
1618 user = get_user_or_error(userid)
1618 user = get_user_or_error(userid)
1619 status = Optional.extract(status)
1619 status = Optional.extract(status)
1620 comment_type = Optional.extract(comment_type)
1620 comment_type = Optional.extract(comment_type)
1621 resolves_comment_id = Optional.extract(resolves_comment_id)
1621 resolves_comment_id = Optional.extract(resolves_comment_id)
1622 extra_recipients = Optional.extract(extra_recipients)
1622 extra_recipients = Optional.extract(extra_recipients)
1623 send_email = Optional.extract(send_email, binary=True)
1623 send_email = Optional.extract(send_email, binary=True)
1624
1624
1625 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1625 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1626 if status and status not in allowed_statuses:
1626 if status and status not in allowed_statuses:
1627 raise JSONRPCError('Bad status, must be on '
1627 raise JSONRPCError('Bad status, must be on '
1628 'of %s got %s' % (allowed_statuses, status,))
1628 'of %s got %s' % (allowed_statuses, status,))
1629
1629
1630 if resolves_comment_id:
1630 if resolves_comment_id:
1631 comment = ChangesetComment.get(resolves_comment_id)
1631 comment = ChangesetComment.get(resolves_comment_id)
1632 if not comment:
1632 if not comment:
1633 raise JSONRPCError(
1633 raise JSONRPCError(
1634 'Invalid resolves_comment_id `%s` for this commit.'
1634 'Invalid resolves_comment_id `%s` for this commit.'
1635 % resolves_comment_id)
1635 % resolves_comment_id)
1636 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1636 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1637 raise JSONRPCError(
1637 raise JSONRPCError(
1638 'Comment `%s` is wrong type for setting status to resolved.'
1638 'Comment `%s` is wrong type for setting status to resolved.'
1639 % resolves_comment_id)
1639 % resolves_comment_id)
1640
1640
1641 try:
1641 try:
1642 rc_config = SettingsModel().get_all_settings()
1642 rc_config = SettingsModel().get_all_settings()
1643 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1643 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1644 status_change_label = ChangesetStatus.get_status_lbl(status)
1644 status_change_label = ChangesetStatus.get_status_lbl(status)
1645 comment = CommentsModel().create(
1645 comment = CommentsModel().create(
1646 message, repo, user, commit_id=commit_id,
1646 message, repo, user, commit_id=commit_id,
1647 status_change=status_change_label,
1647 status_change=status_change_label,
1648 status_change_type=status,
1648 status_change_type=status,
1649 renderer=renderer,
1649 renderer=renderer,
1650 comment_type=comment_type,
1650 comment_type=comment_type,
1651 resolves_comment_id=resolves_comment_id,
1651 resolves_comment_id=resolves_comment_id,
1652 auth_user=apiuser,
1652 auth_user=apiuser,
1653 extra_recipients=extra_recipients,
1653 extra_recipients=extra_recipients,
1654 send_email=send_email
1654 send_email=send_email
1655 )
1655 )
1656 is_inline = bool(comment.f_path and comment.line_no)
1656 is_inline = comment.is_inline
1657
1657
1658 if status:
1658 if status:
1659 # also do a status change
1659 # also do a status change
1660 try:
1660 try:
1661 ChangesetStatusModel().set_status(
1661 ChangesetStatusModel().set_status(
1662 repo, status, user, comment, revision=commit_id,
1662 repo, status, user, comment, revision=commit_id,
1663 dont_allow_on_closed_pull_request=True
1663 dont_allow_on_closed_pull_request=True
1664 )
1664 )
1665 except StatusChangeOnClosedPullRequestError:
1665 except StatusChangeOnClosedPullRequestError:
1666 log.exception(
1666 log.exception(
1667 "Exception occurred while trying to change repo commit status")
1667 "Exception occurred while trying to change repo commit status")
1668 msg = ('Changing status on a commit associated with '
1668 msg = ('Changing status on a commit associated with '
1669 'a closed pull request is not allowed')
1669 'a closed pull request is not allowed')
1670 raise JSONRPCError(msg)
1670 raise JSONRPCError(msg)
1671
1671
1672 CommentsModel().trigger_commit_comment_hook(
1672 CommentsModel().trigger_commit_comment_hook(
1673 repo, apiuser, 'create',
1673 repo, apiuser, 'create',
1674 data={'comment': comment, 'commit': commit})
1674 data={'comment': comment, 'commit': commit})
1675
1675
1676 Session().commit()
1676 Session().commit()
1677
1677
1678 comment_broadcast_channel = channelstream.comment_channel(
1678 comment_broadcast_channel = channelstream.comment_channel(
1679 db_repo_name, commit_obj=commit)
1679 db_repo_name, commit_obj=commit)
1680
1680
1681 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1681 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1682 comment_type = 'inline' if is_inline else 'general'
1682 comment_type = 'inline' if is_inline else 'general'
1683 channelstream.comment_channelstream_push(
1683 channelstream.comment_channelstream_push(
1684 request, comment_broadcast_channel, apiuser,
1684 request, comment_broadcast_channel, apiuser,
1685 _('posted a new {} comment').format(comment_type),
1685 _('posted a new {} comment').format(comment_type),
1686 comment_data=comment_data)
1686 comment_data=comment_data)
1687
1687
1688 return {
1688 return {
1689 'msg': (
1689 'msg': (
1690 'Commented on commit `%s` for repository `%s`' % (
1690 'Commented on commit `%s` for repository `%s`' % (
1691 comment.revision, repo.repo_name)),
1691 comment.revision, repo.repo_name)),
1692 'status_change': status,
1692 'status_change': status,
1693 'success': True,
1693 'success': True,
1694 }
1694 }
1695 except JSONRPCError:
1695 except JSONRPCError:
1696 # catch any inside errors, and re-raise them to prevent from
1696 # catch any inside errors, and re-raise them to prevent from
1697 # below global catch to silence them
1697 # below global catch to silence them
1698 raise
1698 raise
1699 except Exception:
1699 except Exception:
1700 log.exception("Exception occurred while trying to comment on commit")
1700 log.exception("Exception occurred while trying to comment on commit")
1701 raise JSONRPCError(
1701 raise JSONRPCError(
1702 'failed to set comment on repository `%s`' % (repo.repo_name,)
1702 'failed to set comment on repository `%s`' % (repo.repo_name,)
1703 )
1703 )
1704
1704
1705
1705
1706 @jsonrpc_method()
1706 @jsonrpc_method()
1707 def get_repo_comments(request, apiuser, repoid,
1707 def get_repo_comments(request, apiuser, repoid,
1708 commit_id=Optional(None), comment_type=Optional(None),
1708 commit_id=Optional(None), comment_type=Optional(None),
1709 userid=Optional(None)):
1709 userid=Optional(None)):
1710 """
1710 """
1711 Get all comments for a repository
1711 Get all comments for a repository
1712
1712
1713 :param apiuser: This is filled automatically from the |authtoken|.
1713 :param apiuser: This is filled automatically from the |authtoken|.
1714 :type apiuser: AuthUser
1714 :type apiuser: AuthUser
1715 :param repoid: Set the repository name or repository ID.
1715 :param repoid: Set the repository name or repository ID.
1716 :type repoid: str or int
1716 :type repoid: str or int
1717 :param commit_id: Optionally filter the comments by the commit_id
1717 :param commit_id: Optionally filter the comments by the commit_id
1718 :type commit_id: Optional(str), default: None
1718 :type commit_id: Optional(str), default: None
1719 :param comment_type: Optionally filter the comments by the comment_type
1719 :param comment_type: Optionally filter the comments by the comment_type
1720 one of: 'note', 'todo'
1720 one of: 'note', 'todo'
1721 :type comment_type: Optional(str), default: None
1721 :type comment_type: Optional(str), default: None
1722 :param userid: Optionally filter the comments by the author of comment
1722 :param userid: Optionally filter the comments by the author of comment
1723 :type userid: Optional(str or int), Default: None
1723 :type userid: Optional(str or int), Default: None
1724
1724
1725 Example error output:
1725 Example error output:
1726
1726
1727 .. code-block:: bash
1727 .. code-block:: bash
1728
1728
1729 {
1729 {
1730 "id" : <id_given_in_input>,
1730 "id" : <id_given_in_input>,
1731 "result" : [
1731 "result" : [
1732 {
1732 {
1733 "comment_author": <USER_DETAILS>,
1733 "comment_author": <USER_DETAILS>,
1734 "comment_created_on": "2017-02-01T14:38:16.309",
1734 "comment_created_on": "2017-02-01T14:38:16.309",
1735 "comment_f_path": "file.txt",
1735 "comment_f_path": "file.txt",
1736 "comment_id": 282,
1736 "comment_id": 282,
1737 "comment_lineno": "n1",
1737 "comment_lineno": "n1",
1738 "comment_resolved_by": null,
1738 "comment_resolved_by": null,
1739 "comment_status": [],
1739 "comment_status": [],
1740 "comment_text": "This file needs a header",
1740 "comment_text": "This file needs a header",
1741 "comment_type": "todo",
1741 "comment_type": "todo",
1742 "comment_last_version: 0
1742 "comment_last_version: 0
1743 }
1743 }
1744 ],
1744 ],
1745 "error" : null
1745 "error" : null
1746 }
1746 }
1747
1747
1748 """
1748 """
1749 repo = get_repo_or_error(repoid)
1749 repo = get_repo_or_error(repoid)
1750 if not has_superadmin_permission(apiuser):
1750 if not has_superadmin_permission(apiuser):
1751 _perms = ('repository.read', 'repository.write', 'repository.admin')
1751 _perms = ('repository.read', 'repository.write', 'repository.admin')
1752 validate_repo_permissions(apiuser, repoid, repo, _perms)
1752 validate_repo_permissions(apiuser, repoid, repo, _perms)
1753
1753
1754 commit_id = Optional.extract(commit_id)
1754 commit_id = Optional.extract(commit_id)
1755
1755
1756 userid = Optional.extract(userid)
1756 userid = Optional.extract(userid)
1757 if userid:
1757 if userid:
1758 user = get_user_or_error(userid)
1758 user = get_user_or_error(userid)
1759 else:
1759 else:
1760 user = None
1760 user = None
1761
1761
1762 comment_type = Optional.extract(comment_type)
1762 comment_type = Optional.extract(comment_type)
1763 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1763 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1764 raise JSONRPCError(
1764 raise JSONRPCError(
1765 'comment_type must be one of `{}` got {}'.format(
1765 'comment_type must be one of `{}` got {}'.format(
1766 ChangesetComment.COMMENT_TYPES, comment_type)
1766 ChangesetComment.COMMENT_TYPES, comment_type)
1767 )
1767 )
1768
1768
1769 comments = CommentsModel().get_repository_comments(
1769 comments = CommentsModel().get_repository_comments(
1770 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1770 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1771 return comments
1771 return comments
1772
1772
1773
1773
1774 @jsonrpc_method()
1774 @jsonrpc_method()
1775 def get_comment(request, apiuser, comment_id):
1775 def get_comment(request, apiuser, comment_id):
1776 """
1776 """
1777 Get single comment from repository or pull_request
1777 Get single comment from repository or pull_request
1778
1778
1779 :param apiuser: This is filled automatically from the |authtoken|.
1779 :param apiuser: This is filled automatically from the |authtoken|.
1780 :type apiuser: AuthUser
1780 :type apiuser: AuthUser
1781 :param comment_id: comment id found in the URL of comment
1781 :param comment_id: comment id found in the URL of comment
1782 :type comment_id: str or int
1782 :type comment_id: str or int
1783
1783
1784 Example error output:
1784 Example error output:
1785
1785
1786 .. code-block:: bash
1786 .. code-block:: bash
1787
1787
1788 {
1788 {
1789 "id" : <id_given_in_input>,
1789 "id" : <id_given_in_input>,
1790 "result" : {
1790 "result" : {
1791 "comment_author": <USER_DETAILS>,
1791 "comment_author": <USER_DETAILS>,
1792 "comment_created_on": "2017-02-01T14:38:16.309",
1792 "comment_created_on": "2017-02-01T14:38:16.309",
1793 "comment_f_path": "file.txt",
1793 "comment_f_path": "file.txt",
1794 "comment_id": 282,
1794 "comment_id": 282,
1795 "comment_lineno": "n1",
1795 "comment_lineno": "n1",
1796 "comment_resolved_by": null,
1796 "comment_resolved_by": null,
1797 "comment_status": [],
1797 "comment_status": [],
1798 "comment_text": "This file needs a header",
1798 "comment_text": "This file needs a header",
1799 "comment_type": "todo",
1799 "comment_type": "todo",
1800 "comment_last_version: 0
1800 "comment_last_version: 0
1801 },
1801 },
1802 "error" : null
1802 "error" : null
1803 }
1803 }
1804
1804
1805 """
1805 """
1806
1806
1807 comment = ChangesetComment.get(comment_id)
1807 comment = ChangesetComment.get(comment_id)
1808 if not comment:
1808 if not comment:
1809 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1809 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1810
1810
1811 perms = ('repository.read', 'repository.write', 'repository.admin')
1811 perms = ('repository.read', 'repository.write', 'repository.admin')
1812 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1812 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1813 (user=apiuser, repo_name=comment.repo.repo_name)
1813 (user=apiuser, repo_name=comment.repo.repo_name)
1814
1814
1815 if not has_comment_perm:
1815 if not has_comment_perm:
1816 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1816 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1817
1817
1818 return comment
1818 return comment
1819
1819
1820
1820
1821 @jsonrpc_method()
1821 @jsonrpc_method()
1822 def edit_comment(request, apiuser, message, comment_id, version,
1822 def edit_comment(request, apiuser, message, comment_id, version,
1823 userid=Optional(OAttr('apiuser'))):
1823 userid=Optional(OAttr('apiuser'))):
1824 """
1824 """
1825 Edit comment on the pull request or commit,
1825 Edit comment on the pull request or commit,
1826 specified by the `comment_id` and version. Initially version should be 0
1826 specified by the `comment_id` and version. Initially version should be 0
1827
1827
1828 :param apiuser: This is filled automatically from the |authtoken|.
1828 :param apiuser: This is filled automatically from the |authtoken|.
1829 :type apiuser: AuthUser
1829 :type apiuser: AuthUser
1830 :param comment_id: Specify the comment_id for editing
1830 :param comment_id: Specify the comment_id for editing
1831 :type comment_id: int
1831 :type comment_id: int
1832 :param version: version of the comment that will be created, starts from 0
1832 :param version: version of the comment that will be created, starts from 0
1833 :type version: int
1833 :type version: int
1834 :param message: The text content of the comment.
1834 :param message: The text content of the comment.
1835 :type message: str
1835 :type message: str
1836 :param userid: Comment on the pull request as this user
1836 :param userid: Comment on the pull request as this user
1837 :type userid: Optional(str or int)
1837 :type userid: Optional(str or int)
1838
1838
1839 Example output:
1839 Example output:
1840
1840
1841 .. code-block:: bash
1841 .. code-block:: bash
1842
1842
1843 id : <id_given_in_input>
1843 id : <id_given_in_input>
1844 result : {
1844 result : {
1845 "comment": "<comment data>",
1845 "comment": "<comment data>",
1846 "version": "<Integer>",
1846 "version": "<Integer>",
1847 },
1847 },
1848 error : null
1848 error : null
1849 """
1849 """
1850
1850
1851 auth_user = apiuser
1851 auth_user = apiuser
1852 comment = ChangesetComment.get(comment_id)
1852 comment = ChangesetComment.get(comment_id)
1853 if not comment:
1853 if not comment:
1854 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1854 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1855
1855
1856 is_super_admin = has_superadmin_permission(apiuser)
1856 is_super_admin = has_superadmin_permission(apiuser)
1857 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1857 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1858 (user=apiuser, repo_name=comment.repo.repo_name)
1858 (user=apiuser, repo_name=comment.repo.repo_name)
1859
1859
1860 if not isinstance(userid, Optional):
1860 if not isinstance(userid, Optional):
1861 if is_super_admin or is_repo_admin:
1861 if is_super_admin or is_repo_admin:
1862 apiuser = get_user_or_error(userid)
1862 apiuser = get_user_or_error(userid)
1863 auth_user = apiuser.AuthUser()
1863 auth_user = apiuser.AuthUser()
1864 else:
1864 else:
1865 raise JSONRPCError('userid is not the same as your user')
1865 raise JSONRPCError('userid is not the same as your user')
1866
1866
1867 comment_author = comment.author.user_id == auth_user.user_id
1867 comment_author = comment.author.user_id == auth_user.user_id
1868 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1868 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1869 raise JSONRPCError("you don't have access to edit this comment")
1869 raise JSONRPCError("you don't have access to edit this comment")
1870
1870
1871 try:
1871 try:
1872 comment_history = CommentsModel().edit(
1872 comment_history = CommentsModel().edit(
1873 comment_id=comment_id,
1873 comment_id=comment_id,
1874 text=message,
1874 text=message,
1875 auth_user=auth_user,
1875 auth_user=auth_user,
1876 version=version,
1876 version=version,
1877 )
1877 )
1878 Session().commit()
1878 Session().commit()
1879 except CommentVersionMismatch:
1879 except CommentVersionMismatch:
1880 raise JSONRPCError(
1880 raise JSONRPCError(
1881 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1881 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1882 )
1882 )
1883 if not comment_history and not message:
1883 if not comment_history and not message:
1884 raise JSONRPCError(
1884 raise JSONRPCError(
1885 "comment ({}) can't be changed with empty string".format(comment_id)
1885 "comment ({}) can't be changed with empty string".format(comment_id)
1886 )
1886 )
1887
1887
1888 if comment.pull_request:
1888 if comment.pull_request:
1889 pull_request = comment.pull_request
1889 pull_request = comment.pull_request
1890 PullRequestModel().trigger_pull_request_hook(
1890 PullRequestModel().trigger_pull_request_hook(
1891 pull_request, apiuser, 'comment_edit',
1891 pull_request, apiuser, 'comment_edit',
1892 data={'comment': comment})
1892 data={'comment': comment})
1893 else:
1893 else:
1894 db_repo = comment.repo
1894 db_repo = comment.repo
1895 commit_id = comment.revision
1895 commit_id = comment.revision
1896 commit = db_repo.get_commit(commit_id)
1896 commit = db_repo.get_commit(commit_id)
1897 CommentsModel().trigger_commit_comment_hook(
1897 CommentsModel().trigger_commit_comment_hook(
1898 db_repo, apiuser, 'edit',
1898 db_repo, apiuser, 'edit',
1899 data={'comment': comment, 'commit': commit})
1899 data={'comment': comment, 'commit': commit})
1900
1900
1901 data = {
1901 data = {
1902 'comment': comment,
1902 'comment': comment,
1903 'version': comment_history.version if comment_history else None,
1903 'version': comment_history.version if comment_history else None,
1904 }
1904 }
1905 return data
1905 return data
1906
1906
1907
1907
1908 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1908 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1909 # @jsonrpc_method()
1909 # @jsonrpc_method()
1910 # def delete_comment(request, apiuser, comment_id):
1910 # def delete_comment(request, apiuser, comment_id):
1911 # auth_user = apiuser
1911 # auth_user = apiuser
1912 #
1912 #
1913 # comment = ChangesetComment.get(comment_id)
1913 # comment = ChangesetComment.get(comment_id)
1914 # if not comment:
1914 # if not comment:
1915 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1915 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1916 #
1916 #
1917 # is_super_admin = has_superadmin_permission(apiuser)
1917 # is_super_admin = has_superadmin_permission(apiuser)
1918 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1918 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1919 # (user=apiuser, repo_name=comment.repo.repo_name)
1919 # (user=apiuser, repo_name=comment.repo.repo_name)
1920 #
1920 #
1921 # comment_author = comment.author.user_id == auth_user.user_id
1921 # comment_author = comment.author.user_id == auth_user.user_id
1922 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1922 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1923 # raise JSONRPCError("you don't have access to edit this comment")
1923 # raise JSONRPCError("you don't have access to edit this comment")
1924
1924
1925 @jsonrpc_method()
1925 @jsonrpc_method()
1926 def grant_user_permission(request, apiuser, repoid, userid, perm):
1926 def grant_user_permission(request, apiuser, repoid, userid, perm):
1927 """
1927 """
1928 Grant permissions for the specified user on the given repository,
1928 Grant permissions for the specified user on the given repository,
1929 or update existing permissions if found.
1929 or update existing permissions if found.
1930
1930
1931 This command can only be run using an |authtoken| with admin
1931 This command can only be run using an |authtoken| with admin
1932 permissions on the |repo|.
1932 permissions on the |repo|.
1933
1933
1934 :param apiuser: This is filled automatically from the |authtoken|.
1934 :param apiuser: This is filled automatically from the |authtoken|.
1935 :type apiuser: AuthUser
1935 :type apiuser: AuthUser
1936 :param repoid: Set the repository name or repository ID.
1936 :param repoid: Set the repository name or repository ID.
1937 :type repoid: str or int
1937 :type repoid: str or int
1938 :param userid: Set the user name.
1938 :param userid: Set the user name.
1939 :type userid: str
1939 :type userid: str
1940 :param perm: Set the user permissions, using the following format
1940 :param perm: Set the user permissions, using the following format
1941 ``(repository.(none|read|write|admin))``
1941 ``(repository.(none|read|write|admin))``
1942 :type perm: str
1942 :type perm: str
1943
1943
1944 Example output:
1944 Example output:
1945
1945
1946 .. code-block:: bash
1946 .. code-block:: bash
1947
1947
1948 id : <id_given_in_input>
1948 id : <id_given_in_input>
1949 result: {
1949 result: {
1950 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1950 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1951 "success": true
1951 "success": true
1952 }
1952 }
1953 error: null
1953 error: null
1954 """
1954 """
1955
1955
1956 repo = get_repo_or_error(repoid)
1956 repo = get_repo_or_error(repoid)
1957 user = get_user_or_error(userid)
1957 user = get_user_or_error(userid)
1958 perm = get_perm_or_error(perm)
1958 perm = get_perm_or_error(perm)
1959 if not has_superadmin_permission(apiuser):
1959 if not has_superadmin_permission(apiuser):
1960 _perms = ('repository.admin',)
1960 _perms = ('repository.admin',)
1961 validate_repo_permissions(apiuser, repoid, repo, _perms)
1961 validate_repo_permissions(apiuser, repoid, repo, _perms)
1962
1962
1963 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1963 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1964 try:
1964 try:
1965 changes = RepoModel().update_permissions(
1965 changes = RepoModel().update_permissions(
1966 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1966 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1967
1967
1968 action_data = {
1968 action_data = {
1969 'added': changes['added'],
1969 'added': changes['added'],
1970 'updated': changes['updated'],
1970 'updated': changes['updated'],
1971 'deleted': changes['deleted'],
1971 'deleted': changes['deleted'],
1972 }
1972 }
1973 audit_logger.store_api(
1973 audit_logger.store_api(
1974 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1974 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1975 Session().commit()
1975 Session().commit()
1976 PermissionModel().flush_user_permission_caches(changes)
1976 PermissionModel().flush_user_permission_caches(changes)
1977
1977
1978 return {
1978 return {
1979 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1979 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1980 perm.permission_name, user.username, repo.repo_name
1980 perm.permission_name, user.username, repo.repo_name
1981 ),
1981 ),
1982 'success': True
1982 'success': True
1983 }
1983 }
1984 except Exception:
1984 except Exception:
1985 log.exception("Exception occurred while trying edit permissions for repo")
1985 log.exception("Exception occurred while trying edit permissions for repo")
1986 raise JSONRPCError(
1986 raise JSONRPCError(
1987 'failed to edit permission for user: `%s` in repo: `%s`' % (
1987 'failed to edit permission for user: `%s` in repo: `%s`' % (
1988 userid, repoid
1988 userid, repoid
1989 )
1989 )
1990 )
1990 )
1991
1991
1992
1992
1993 @jsonrpc_method()
1993 @jsonrpc_method()
1994 def revoke_user_permission(request, apiuser, repoid, userid):
1994 def revoke_user_permission(request, apiuser, repoid, userid):
1995 """
1995 """
1996 Revoke permission for a user on the specified repository.
1996 Revoke permission for a user on the specified repository.
1997
1997
1998 This command can only be run using an |authtoken| with admin
1998 This command can only be run using an |authtoken| with admin
1999 permissions on the |repo|.
1999 permissions on the |repo|.
2000
2000
2001 :param apiuser: This is filled automatically from the |authtoken|.
2001 :param apiuser: This is filled automatically from the |authtoken|.
2002 :type apiuser: AuthUser
2002 :type apiuser: AuthUser
2003 :param repoid: Set the repository name or repository ID.
2003 :param repoid: Set the repository name or repository ID.
2004 :type repoid: str or int
2004 :type repoid: str or int
2005 :param userid: Set the user name of revoked user.
2005 :param userid: Set the user name of revoked user.
2006 :type userid: str or int
2006 :type userid: str or int
2007
2007
2008 Example error output:
2008 Example error output:
2009
2009
2010 .. code-block:: bash
2010 .. code-block:: bash
2011
2011
2012 id : <id_given_in_input>
2012 id : <id_given_in_input>
2013 result: {
2013 result: {
2014 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
2014 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
2015 "success": true
2015 "success": true
2016 }
2016 }
2017 error: null
2017 error: null
2018 """
2018 """
2019
2019
2020 repo = get_repo_or_error(repoid)
2020 repo = get_repo_or_error(repoid)
2021 user = get_user_or_error(userid)
2021 user = get_user_or_error(userid)
2022 if not has_superadmin_permission(apiuser):
2022 if not has_superadmin_permission(apiuser):
2023 _perms = ('repository.admin',)
2023 _perms = ('repository.admin',)
2024 validate_repo_permissions(apiuser, repoid, repo, _perms)
2024 validate_repo_permissions(apiuser, repoid, repo, _perms)
2025
2025
2026 perm_deletions = [[user.user_id, None, "user"]]
2026 perm_deletions = [[user.user_id, None, "user"]]
2027 try:
2027 try:
2028 changes = RepoModel().update_permissions(
2028 changes = RepoModel().update_permissions(
2029 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2029 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2030
2030
2031 action_data = {
2031 action_data = {
2032 'added': changes['added'],
2032 'added': changes['added'],
2033 'updated': changes['updated'],
2033 'updated': changes['updated'],
2034 'deleted': changes['deleted'],
2034 'deleted': changes['deleted'],
2035 }
2035 }
2036 audit_logger.store_api(
2036 audit_logger.store_api(
2037 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2037 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2038 Session().commit()
2038 Session().commit()
2039 PermissionModel().flush_user_permission_caches(changes)
2039 PermissionModel().flush_user_permission_caches(changes)
2040
2040
2041 return {
2041 return {
2042 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
2042 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
2043 user.username, repo.repo_name
2043 user.username, repo.repo_name
2044 ),
2044 ),
2045 'success': True
2045 'success': True
2046 }
2046 }
2047 except Exception:
2047 except Exception:
2048 log.exception("Exception occurred while trying revoke permissions to repo")
2048 log.exception("Exception occurred while trying revoke permissions to repo")
2049 raise JSONRPCError(
2049 raise JSONRPCError(
2050 'failed to edit permission for user: `%s` in repo: `%s`' % (
2050 'failed to edit permission for user: `%s` in repo: `%s`' % (
2051 userid, repoid
2051 userid, repoid
2052 )
2052 )
2053 )
2053 )
2054
2054
2055
2055
2056 @jsonrpc_method()
2056 @jsonrpc_method()
2057 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2057 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2058 """
2058 """
2059 Grant permission for a user group on the specified repository,
2059 Grant permission for a user group on the specified repository,
2060 or update existing permissions.
2060 or update existing permissions.
2061
2061
2062 This command can only be run using an |authtoken| with admin
2062 This command can only be run using an |authtoken| with admin
2063 permissions on the |repo|.
2063 permissions on the |repo|.
2064
2064
2065 :param apiuser: This is filled automatically from the |authtoken|.
2065 :param apiuser: This is filled automatically from the |authtoken|.
2066 :type apiuser: AuthUser
2066 :type apiuser: AuthUser
2067 :param repoid: Set the repository name or repository ID.
2067 :param repoid: Set the repository name or repository ID.
2068 :type repoid: str or int
2068 :type repoid: str or int
2069 :param usergroupid: Specify the ID of the user group.
2069 :param usergroupid: Specify the ID of the user group.
2070 :type usergroupid: str or int
2070 :type usergroupid: str or int
2071 :param perm: Set the user group permissions using the following
2071 :param perm: Set the user group permissions using the following
2072 format: (repository.(none|read|write|admin))
2072 format: (repository.(none|read|write|admin))
2073 :type perm: str
2073 :type perm: str
2074
2074
2075 Example output:
2075 Example output:
2076
2076
2077 .. code-block:: bash
2077 .. code-block:: bash
2078
2078
2079 id : <id_given_in_input>
2079 id : <id_given_in_input>
2080 result : {
2080 result : {
2081 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2081 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2082 "success": true
2082 "success": true
2083
2083
2084 }
2084 }
2085 error : null
2085 error : null
2086
2086
2087 Example error output:
2087 Example error output:
2088
2088
2089 .. code-block:: bash
2089 .. code-block:: bash
2090
2090
2091 id : <id_given_in_input>
2091 id : <id_given_in_input>
2092 result : null
2092 result : null
2093 error : {
2093 error : {
2094 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2094 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2095 }
2095 }
2096
2096
2097 """
2097 """
2098
2098
2099 repo = get_repo_or_error(repoid)
2099 repo = get_repo_or_error(repoid)
2100 perm = get_perm_or_error(perm)
2100 perm = get_perm_or_error(perm)
2101 if not has_superadmin_permission(apiuser):
2101 if not has_superadmin_permission(apiuser):
2102 _perms = ('repository.admin',)
2102 _perms = ('repository.admin',)
2103 validate_repo_permissions(apiuser, repoid, repo, _perms)
2103 validate_repo_permissions(apiuser, repoid, repo, _perms)
2104
2104
2105 user_group = get_user_group_or_error(usergroupid)
2105 user_group = get_user_group_or_error(usergroupid)
2106 if not has_superadmin_permission(apiuser):
2106 if not has_superadmin_permission(apiuser):
2107 # check if we have at least read permission for this user group !
2107 # check if we have at least read permission for this user group !
2108 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2108 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2109 if not HasUserGroupPermissionAnyApi(*_perms)(
2109 if not HasUserGroupPermissionAnyApi(*_perms)(
2110 user=apiuser, user_group_name=user_group.users_group_name):
2110 user=apiuser, user_group_name=user_group.users_group_name):
2111 raise JSONRPCError(
2111 raise JSONRPCError(
2112 'user group `%s` does not exist' % (usergroupid,))
2112 'user group `%s` does not exist' % (usergroupid,))
2113
2113
2114 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2114 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2115 try:
2115 try:
2116 changes = RepoModel().update_permissions(
2116 changes = RepoModel().update_permissions(
2117 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2117 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2118 action_data = {
2118 action_data = {
2119 'added': changes['added'],
2119 'added': changes['added'],
2120 'updated': changes['updated'],
2120 'updated': changes['updated'],
2121 'deleted': changes['deleted'],
2121 'deleted': changes['deleted'],
2122 }
2122 }
2123 audit_logger.store_api(
2123 audit_logger.store_api(
2124 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2124 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2125 Session().commit()
2125 Session().commit()
2126 PermissionModel().flush_user_permission_caches(changes)
2126 PermissionModel().flush_user_permission_caches(changes)
2127
2127
2128 return {
2128 return {
2129 'msg': 'Granted perm: `%s` for user group: `%s` in '
2129 'msg': 'Granted perm: `%s` for user group: `%s` in '
2130 'repo: `%s`' % (
2130 'repo: `%s`' % (
2131 perm.permission_name, user_group.users_group_name,
2131 perm.permission_name, user_group.users_group_name,
2132 repo.repo_name
2132 repo.repo_name
2133 ),
2133 ),
2134 'success': True
2134 'success': True
2135 }
2135 }
2136 except Exception:
2136 except Exception:
2137 log.exception(
2137 log.exception(
2138 "Exception occurred while trying change permission on repo")
2138 "Exception occurred while trying change permission on repo")
2139 raise JSONRPCError(
2139 raise JSONRPCError(
2140 'failed to edit permission for user group: `%s` in '
2140 'failed to edit permission for user group: `%s` in '
2141 'repo: `%s`' % (
2141 'repo: `%s`' % (
2142 usergroupid, repo.repo_name
2142 usergroupid, repo.repo_name
2143 )
2143 )
2144 )
2144 )
2145
2145
2146
2146
2147 @jsonrpc_method()
2147 @jsonrpc_method()
2148 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2148 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2149 """
2149 """
2150 Revoke the permissions of a user group on a given repository.
2150 Revoke the permissions of a user group on a given repository.
2151
2151
2152 This command can only be run using an |authtoken| with admin
2152 This command can only be run using an |authtoken| with admin
2153 permissions on the |repo|.
2153 permissions on the |repo|.
2154
2154
2155 :param apiuser: This is filled automatically from the |authtoken|.
2155 :param apiuser: This is filled automatically from the |authtoken|.
2156 :type apiuser: AuthUser
2156 :type apiuser: AuthUser
2157 :param repoid: Set the repository name or repository ID.
2157 :param repoid: Set the repository name or repository ID.
2158 :type repoid: str or int
2158 :type repoid: str or int
2159 :param usergroupid: Specify the user group ID.
2159 :param usergroupid: Specify the user group ID.
2160 :type usergroupid: str or int
2160 :type usergroupid: str or int
2161
2161
2162 Example output:
2162 Example output:
2163
2163
2164 .. code-block:: bash
2164 .. code-block:: bash
2165
2165
2166 id : <id_given_in_input>
2166 id : <id_given_in_input>
2167 result: {
2167 result: {
2168 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2168 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2169 "success": true
2169 "success": true
2170 }
2170 }
2171 error: null
2171 error: null
2172 """
2172 """
2173
2173
2174 repo = get_repo_or_error(repoid)
2174 repo = get_repo_or_error(repoid)
2175 if not has_superadmin_permission(apiuser):
2175 if not has_superadmin_permission(apiuser):
2176 _perms = ('repository.admin',)
2176 _perms = ('repository.admin',)
2177 validate_repo_permissions(apiuser, repoid, repo, _perms)
2177 validate_repo_permissions(apiuser, repoid, repo, _perms)
2178
2178
2179 user_group = get_user_group_or_error(usergroupid)
2179 user_group = get_user_group_or_error(usergroupid)
2180 if not has_superadmin_permission(apiuser):
2180 if not has_superadmin_permission(apiuser):
2181 # check if we have at least read permission for this user group !
2181 # check if we have at least read permission for this user group !
2182 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2182 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2183 if not HasUserGroupPermissionAnyApi(*_perms)(
2183 if not HasUserGroupPermissionAnyApi(*_perms)(
2184 user=apiuser, user_group_name=user_group.users_group_name):
2184 user=apiuser, user_group_name=user_group.users_group_name):
2185 raise JSONRPCError(
2185 raise JSONRPCError(
2186 'user group `%s` does not exist' % (usergroupid,))
2186 'user group `%s` does not exist' % (usergroupid,))
2187
2187
2188 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2188 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2189 try:
2189 try:
2190 changes = RepoModel().update_permissions(
2190 changes = RepoModel().update_permissions(
2191 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2191 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2192 action_data = {
2192 action_data = {
2193 'added': changes['added'],
2193 'added': changes['added'],
2194 'updated': changes['updated'],
2194 'updated': changes['updated'],
2195 'deleted': changes['deleted'],
2195 'deleted': changes['deleted'],
2196 }
2196 }
2197 audit_logger.store_api(
2197 audit_logger.store_api(
2198 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2198 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2199 Session().commit()
2199 Session().commit()
2200 PermissionModel().flush_user_permission_caches(changes)
2200 PermissionModel().flush_user_permission_caches(changes)
2201
2201
2202 return {
2202 return {
2203 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2203 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2204 user_group.users_group_name, repo.repo_name
2204 user_group.users_group_name, repo.repo_name
2205 ),
2205 ),
2206 'success': True
2206 'success': True
2207 }
2207 }
2208 except Exception:
2208 except Exception:
2209 log.exception("Exception occurred while trying revoke "
2209 log.exception("Exception occurred while trying revoke "
2210 "user group permission on repo")
2210 "user group permission on repo")
2211 raise JSONRPCError(
2211 raise JSONRPCError(
2212 'failed to edit permission for user group: `%s` in '
2212 'failed to edit permission for user group: `%s` in '
2213 'repo: `%s`' % (
2213 'repo: `%s`' % (
2214 user_group.users_group_name, repo.repo_name
2214 user_group.users_group_name, repo.repo_name
2215 )
2215 )
2216 )
2216 )
2217
2217
2218
2218
2219 @jsonrpc_method()
2219 @jsonrpc_method()
2220 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2220 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2221 """
2221 """
2222 Triggers a pull on the given repository from a remote location. You
2222 Triggers a pull on the given repository from a remote location. You
2223 can use this to keep remote repositories up-to-date.
2223 can use this to keep remote repositories up-to-date.
2224
2224
2225 This command can only be run using an |authtoken| with admin
2225 This command can only be run using an |authtoken| with admin
2226 rights to the specified repository. For more information,
2226 rights to the specified repository. For more information,
2227 see :ref:`config-token-ref`.
2227 see :ref:`config-token-ref`.
2228
2228
2229 This command takes the following options:
2229 This command takes the following options:
2230
2230
2231 :param apiuser: This is filled automatically from the |authtoken|.
2231 :param apiuser: This is filled automatically from the |authtoken|.
2232 :type apiuser: AuthUser
2232 :type apiuser: AuthUser
2233 :param repoid: The repository name or repository ID.
2233 :param repoid: The repository name or repository ID.
2234 :type repoid: str or int
2234 :type repoid: str or int
2235 :param remote_uri: Optional remote URI to pass in for pull
2235 :param remote_uri: Optional remote URI to pass in for pull
2236 :type remote_uri: str
2236 :type remote_uri: str
2237
2237
2238 Example output:
2238 Example output:
2239
2239
2240 .. code-block:: bash
2240 .. code-block:: bash
2241
2241
2242 id : <id_given_in_input>
2242 id : <id_given_in_input>
2243 result : {
2243 result : {
2244 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2244 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2245 "repository": "<repository name>"
2245 "repository": "<repository name>"
2246 }
2246 }
2247 error : null
2247 error : null
2248
2248
2249 Example error output:
2249 Example error output:
2250
2250
2251 .. code-block:: bash
2251 .. code-block:: bash
2252
2252
2253 id : <id_given_in_input>
2253 id : <id_given_in_input>
2254 result : null
2254 result : null
2255 error : {
2255 error : {
2256 "Unable to push changes from `<remote_url>`"
2256 "Unable to push changes from `<remote_url>`"
2257 }
2257 }
2258
2258
2259 """
2259 """
2260
2260
2261 repo = get_repo_or_error(repoid)
2261 repo = get_repo_or_error(repoid)
2262 remote_uri = Optional.extract(remote_uri)
2262 remote_uri = Optional.extract(remote_uri)
2263 remote_uri_display = remote_uri or repo.clone_uri_hidden
2263 remote_uri_display = remote_uri or repo.clone_uri_hidden
2264 if not has_superadmin_permission(apiuser):
2264 if not has_superadmin_permission(apiuser):
2265 _perms = ('repository.admin',)
2265 _perms = ('repository.admin',)
2266 validate_repo_permissions(apiuser, repoid, repo, _perms)
2266 validate_repo_permissions(apiuser, repoid, repo, _perms)
2267
2267
2268 try:
2268 try:
2269 ScmModel().pull_changes(
2269 ScmModel().pull_changes(
2270 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2270 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2271 return {
2271 return {
2272 'msg': 'Pulled from url `%s` on repo `%s`' % (
2272 'msg': 'Pulled from url `%s` on repo `%s`' % (
2273 remote_uri_display, repo.repo_name),
2273 remote_uri_display, repo.repo_name),
2274 'repository': repo.repo_name
2274 'repository': repo.repo_name
2275 }
2275 }
2276 except Exception:
2276 except Exception:
2277 log.exception("Exception occurred while trying to "
2277 log.exception("Exception occurred while trying to "
2278 "pull changes from remote location")
2278 "pull changes from remote location")
2279 raise JSONRPCError(
2279 raise JSONRPCError(
2280 'Unable to pull changes from `%s`' % remote_uri_display
2280 'Unable to pull changes from `%s`' % remote_uri_display
2281 )
2281 )
2282
2282
2283
2283
2284 @jsonrpc_method()
2284 @jsonrpc_method()
2285 def strip(request, apiuser, repoid, revision, branch):
2285 def strip(request, apiuser, repoid, revision, branch):
2286 """
2286 """
2287 Strips the given revision from the specified repository.
2287 Strips the given revision from the specified repository.
2288
2288
2289 * This will remove the revision and all of its decendants.
2289 * This will remove the revision and all of its decendants.
2290
2290
2291 This command can only be run using an |authtoken| with admin rights to
2291 This command can only be run using an |authtoken| with admin rights to
2292 the specified repository.
2292 the specified repository.
2293
2293
2294 This command takes the following options:
2294 This command takes the following options:
2295
2295
2296 :param apiuser: This is filled automatically from the |authtoken|.
2296 :param apiuser: This is filled automatically from the |authtoken|.
2297 :type apiuser: AuthUser
2297 :type apiuser: AuthUser
2298 :param repoid: The repository name or repository ID.
2298 :param repoid: The repository name or repository ID.
2299 :type repoid: str or int
2299 :type repoid: str or int
2300 :param revision: The revision you wish to strip.
2300 :param revision: The revision you wish to strip.
2301 :type revision: str
2301 :type revision: str
2302 :param branch: The branch from which to strip the revision.
2302 :param branch: The branch from which to strip the revision.
2303 :type branch: str
2303 :type branch: str
2304
2304
2305 Example output:
2305 Example output:
2306
2306
2307 .. code-block:: bash
2307 .. code-block:: bash
2308
2308
2309 id : <id_given_in_input>
2309 id : <id_given_in_input>
2310 result : {
2310 result : {
2311 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2311 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2312 "repository": "<repository name>"
2312 "repository": "<repository name>"
2313 }
2313 }
2314 error : null
2314 error : null
2315
2315
2316 Example error output:
2316 Example error output:
2317
2317
2318 .. code-block:: bash
2318 .. code-block:: bash
2319
2319
2320 id : <id_given_in_input>
2320 id : <id_given_in_input>
2321 result : null
2321 result : null
2322 error : {
2322 error : {
2323 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2323 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2324 }
2324 }
2325
2325
2326 """
2326 """
2327
2327
2328 repo = get_repo_or_error(repoid)
2328 repo = get_repo_or_error(repoid)
2329 if not has_superadmin_permission(apiuser):
2329 if not has_superadmin_permission(apiuser):
2330 _perms = ('repository.admin',)
2330 _perms = ('repository.admin',)
2331 validate_repo_permissions(apiuser, repoid, repo, _perms)
2331 validate_repo_permissions(apiuser, repoid, repo, _perms)
2332
2332
2333 try:
2333 try:
2334 ScmModel().strip(repo, revision, branch)
2334 ScmModel().strip(repo, revision, branch)
2335 audit_logger.store_api(
2335 audit_logger.store_api(
2336 'repo.commit.strip', action_data={'commit_id': revision},
2336 'repo.commit.strip', action_data={'commit_id': revision},
2337 repo=repo,
2337 repo=repo,
2338 user=apiuser, commit=True)
2338 user=apiuser, commit=True)
2339
2339
2340 return {
2340 return {
2341 'msg': 'Stripped commit %s from repo `%s`' % (
2341 'msg': 'Stripped commit %s from repo `%s`' % (
2342 revision, repo.repo_name),
2342 revision, repo.repo_name),
2343 'repository': repo.repo_name
2343 'repository': repo.repo_name
2344 }
2344 }
2345 except Exception:
2345 except Exception:
2346 log.exception("Exception while trying to strip")
2346 log.exception("Exception while trying to strip")
2347 raise JSONRPCError(
2347 raise JSONRPCError(
2348 'Unable to strip commit %s from repo `%s`' % (
2348 'Unable to strip commit %s from repo `%s`' % (
2349 revision, repo.repo_name)
2349 revision, repo.repo_name)
2350 )
2350 )
2351
2351
2352
2352
2353 @jsonrpc_method()
2353 @jsonrpc_method()
2354 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2354 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2355 """
2355 """
2356 Returns all settings for a repository. If key is given it only returns the
2356 Returns all settings for a repository. If key is given it only returns the
2357 setting identified by the key or null.
2357 setting identified by the key or null.
2358
2358
2359 :param apiuser: This is filled automatically from the |authtoken|.
2359 :param apiuser: This is filled automatically from the |authtoken|.
2360 :type apiuser: AuthUser
2360 :type apiuser: AuthUser
2361 :param repoid: The repository name or repository id.
2361 :param repoid: The repository name or repository id.
2362 :type repoid: str or int
2362 :type repoid: str or int
2363 :param key: Key of the setting to return.
2363 :param key: Key of the setting to return.
2364 :type: key: Optional(str)
2364 :type: key: Optional(str)
2365
2365
2366 Example output:
2366 Example output:
2367
2367
2368 .. code-block:: bash
2368 .. code-block:: bash
2369
2369
2370 {
2370 {
2371 "error": null,
2371 "error": null,
2372 "id": 237,
2372 "id": 237,
2373 "result": {
2373 "result": {
2374 "extensions_largefiles": true,
2374 "extensions_largefiles": true,
2375 "extensions_evolve": true,
2375 "extensions_evolve": true,
2376 "hooks_changegroup_push_logger": true,
2376 "hooks_changegroup_push_logger": true,
2377 "hooks_changegroup_repo_size": false,
2377 "hooks_changegroup_repo_size": false,
2378 "hooks_outgoing_pull_logger": true,
2378 "hooks_outgoing_pull_logger": true,
2379 "phases_publish": "True",
2379 "phases_publish": "True",
2380 "rhodecode_hg_use_rebase_for_merging": true,
2380 "rhodecode_hg_use_rebase_for_merging": true,
2381 "rhodecode_pr_merge_enabled": true,
2381 "rhodecode_pr_merge_enabled": true,
2382 "rhodecode_use_outdated_comments": true
2382 "rhodecode_use_outdated_comments": true
2383 }
2383 }
2384 }
2384 }
2385 """
2385 """
2386
2386
2387 # Restrict access to this api method to super-admins, and repo admins only.
2387 # Restrict access to this api method to super-admins, and repo admins only.
2388 repo = get_repo_or_error(repoid)
2388 repo = get_repo_or_error(repoid)
2389 if not has_superadmin_permission(apiuser):
2389 if not has_superadmin_permission(apiuser):
2390 _perms = ('repository.admin',)
2390 _perms = ('repository.admin',)
2391 validate_repo_permissions(apiuser, repoid, repo, _perms)
2391 validate_repo_permissions(apiuser, repoid, repo, _perms)
2392
2392
2393 try:
2393 try:
2394 settings_model = VcsSettingsModel(repo=repo)
2394 settings_model = VcsSettingsModel(repo=repo)
2395 settings = settings_model.get_global_settings()
2395 settings = settings_model.get_global_settings()
2396 settings.update(settings_model.get_repo_settings())
2396 settings.update(settings_model.get_repo_settings())
2397
2397
2398 # If only a single setting is requested fetch it from all settings.
2398 # If only a single setting is requested fetch it from all settings.
2399 key = Optional.extract(key)
2399 key = Optional.extract(key)
2400 if key is not None:
2400 if key is not None:
2401 settings = settings.get(key, None)
2401 settings = settings.get(key, None)
2402 except Exception:
2402 except Exception:
2403 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2403 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2404 log.exception(msg)
2404 log.exception(msg)
2405 raise JSONRPCError(msg)
2405 raise JSONRPCError(msg)
2406
2406
2407 return settings
2407 return settings
2408
2408
2409
2409
2410 @jsonrpc_method()
2410 @jsonrpc_method()
2411 def set_repo_settings(request, apiuser, repoid, settings):
2411 def set_repo_settings(request, apiuser, repoid, settings):
2412 """
2412 """
2413 Update repository settings. Returns true on success.
2413 Update repository settings. Returns true on success.
2414
2414
2415 :param apiuser: This is filled automatically from the |authtoken|.
2415 :param apiuser: This is filled automatically from the |authtoken|.
2416 :type apiuser: AuthUser
2416 :type apiuser: AuthUser
2417 :param repoid: The repository name or repository id.
2417 :param repoid: The repository name or repository id.
2418 :type repoid: str or int
2418 :type repoid: str or int
2419 :param settings: The new settings for the repository.
2419 :param settings: The new settings for the repository.
2420 :type: settings: dict
2420 :type: settings: dict
2421
2421
2422 Example output:
2422 Example output:
2423
2423
2424 .. code-block:: bash
2424 .. code-block:: bash
2425
2425
2426 {
2426 {
2427 "error": null,
2427 "error": null,
2428 "id": 237,
2428 "id": 237,
2429 "result": true
2429 "result": true
2430 }
2430 }
2431 """
2431 """
2432 # Restrict access to this api method to super-admins, and repo admins only.
2432 # Restrict access to this api method to super-admins, and repo admins only.
2433 repo = get_repo_or_error(repoid)
2433 repo = get_repo_or_error(repoid)
2434 if not has_superadmin_permission(apiuser):
2434 if not has_superadmin_permission(apiuser):
2435 _perms = ('repository.admin',)
2435 _perms = ('repository.admin',)
2436 validate_repo_permissions(apiuser, repoid, repo, _perms)
2436 validate_repo_permissions(apiuser, repoid, repo, _perms)
2437
2437
2438 if type(settings) is not dict:
2438 if type(settings) is not dict:
2439 raise JSONRPCError('Settings have to be a JSON Object.')
2439 raise JSONRPCError('Settings have to be a JSON Object.')
2440
2440
2441 try:
2441 try:
2442 settings_model = VcsSettingsModel(repo=repoid)
2442 settings_model = VcsSettingsModel(repo=repoid)
2443
2443
2444 # Merge global, repo and incoming settings.
2444 # Merge global, repo and incoming settings.
2445 new_settings = settings_model.get_global_settings()
2445 new_settings = settings_model.get_global_settings()
2446 new_settings.update(settings_model.get_repo_settings())
2446 new_settings.update(settings_model.get_repo_settings())
2447 new_settings.update(settings)
2447 new_settings.update(settings)
2448
2448
2449 # Update the settings.
2449 # Update the settings.
2450 inherit_global_settings = new_settings.get(
2450 inherit_global_settings = new_settings.get(
2451 'inherit_global_settings', False)
2451 'inherit_global_settings', False)
2452 settings_model.create_or_update_repo_settings(
2452 settings_model.create_or_update_repo_settings(
2453 new_settings, inherit_global_settings=inherit_global_settings)
2453 new_settings, inherit_global_settings=inherit_global_settings)
2454 Session().commit()
2454 Session().commit()
2455 except Exception:
2455 except Exception:
2456 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2456 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2457 log.exception(msg)
2457 log.exception(msg)
2458 raise JSONRPCError(msg)
2458 raise JSONRPCError(msg)
2459
2459
2460 # Indicate success.
2460 # Indicate success.
2461 return True
2461 return True
2462
2462
2463
2463
2464 @jsonrpc_method()
2464 @jsonrpc_method()
2465 def maintenance(request, apiuser, repoid):
2465 def maintenance(request, apiuser, repoid):
2466 """
2466 """
2467 Triggers a maintenance on the given repository.
2467 Triggers a maintenance on the given repository.
2468
2468
2469 This command can only be run using an |authtoken| with admin
2469 This command can only be run using an |authtoken| with admin
2470 rights to the specified repository. For more information,
2470 rights to the specified repository. For more information,
2471 see :ref:`config-token-ref`.
2471 see :ref:`config-token-ref`.
2472
2472
2473 This command takes the following options:
2473 This command takes the following options:
2474
2474
2475 :param apiuser: This is filled automatically from the |authtoken|.
2475 :param apiuser: This is filled automatically from the |authtoken|.
2476 :type apiuser: AuthUser
2476 :type apiuser: AuthUser
2477 :param repoid: The repository name or repository ID.
2477 :param repoid: The repository name or repository ID.
2478 :type repoid: str or int
2478 :type repoid: str or int
2479
2479
2480 Example output:
2480 Example output:
2481
2481
2482 .. code-block:: bash
2482 .. code-block:: bash
2483
2483
2484 id : <id_given_in_input>
2484 id : <id_given_in_input>
2485 result : {
2485 result : {
2486 "msg": "executed maintenance command",
2486 "msg": "executed maintenance command",
2487 "executed_actions": [
2487 "executed_actions": [
2488 <action_message>, <action_message2>...
2488 <action_message>, <action_message2>...
2489 ],
2489 ],
2490 "repository": "<repository name>"
2490 "repository": "<repository name>"
2491 }
2491 }
2492 error : null
2492 error : null
2493
2493
2494 Example error output:
2494 Example error output:
2495
2495
2496 .. code-block:: bash
2496 .. code-block:: bash
2497
2497
2498 id : <id_given_in_input>
2498 id : <id_given_in_input>
2499 result : null
2499 result : null
2500 error : {
2500 error : {
2501 "Unable to execute maintenance on `<reponame>`"
2501 "Unable to execute maintenance on `<reponame>`"
2502 }
2502 }
2503
2503
2504 """
2504 """
2505
2505
2506 repo = get_repo_or_error(repoid)
2506 repo = get_repo_or_error(repoid)
2507 if not has_superadmin_permission(apiuser):
2507 if not has_superadmin_permission(apiuser):
2508 _perms = ('repository.admin',)
2508 _perms = ('repository.admin',)
2509 validate_repo_permissions(apiuser, repoid, repo, _perms)
2509 validate_repo_permissions(apiuser, repoid, repo, _perms)
2510
2510
2511 try:
2511 try:
2512 maintenance = repo_maintenance.RepoMaintenance()
2512 maintenance = repo_maintenance.RepoMaintenance()
2513 executed_actions = maintenance.execute(repo)
2513 executed_actions = maintenance.execute(repo)
2514
2514
2515 return {
2515 return {
2516 'msg': 'executed maintenance command',
2516 'msg': 'executed maintenance command',
2517 'executed_actions': executed_actions,
2517 'executed_actions': executed_actions,
2518 'repository': repo.repo_name
2518 'repository': repo.repo_name
2519 }
2519 }
2520 except Exception:
2520 except Exception:
2521 log.exception("Exception occurred while trying to run maintenance")
2521 log.exception("Exception occurred while trying to run maintenance")
2522 raise JSONRPCError(
2522 raise JSONRPCError(
2523 'Unable to execute maintenance on `%s`' % repo.repo_name)
2523 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,1658 +1,1661 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'repo_commits': '/{repo_name}/commits',
44 'repo_commits': '/{repo_name}/commits',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 }[name].format(**kwargs)
59 }[name].format(**kwargs)
60
60
61 if params:
61 if params:
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 return base_url
63 return base_url
64
64
65
65
66 @pytest.mark.usefixtures('app', 'autologin_user')
66 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.backends("git", "hg")
67 @pytest.mark.backends("git", "hg")
68 class TestPullrequestsView(object):
68 class TestPullrequestsView(object):
69
69
70 def test_index(self, backend):
70 def test_index(self, backend):
71 self.app.get(route_path(
71 self.app.get(route_path(
72 'pullrequest_new',
72 'pullrequest_new',
73 repo_name=backend.repo_name))
73 repo_name=backend.repo_name))
74
74
75 def test_option_menu_create_pull_request_exists(self, backend):
75 def test_option_menu_create_pull_request_exists(self, backend):
76 repo_name = backend.repo_name
76 repo_name = backend.repo_name
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78
78
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 'pullrequest_new', repo_name=repo_name)
80 'pullrequest_new', repo_name=repo_name)
81 response.mustcontain(create_pr_link)
81 response.mustcontain(create_pr_link)
82
82
83 def test_create_pr_form_with_raw_commit_id(self, backend):
83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 repo = backend.repo
84 repo = backend.repo
85
85
86 self.app.get(
86 self.app.get(
87 route_path('pullrequest_new', repo_name=repo.repo_name,
87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 commit=repo.get_commit().raw_id),
88 commit=repo.get_commit().raw_id),
89 status=200)
89 status=200)
90
90
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 pull_request = pr_util.create_pull_request(
94 pull_request = pr_util.create_pull_request(
95 mergeable=pr_merge_enabled, enable_notifications=False)
95 mergeable=pr_merge_enabled, enable_notifications=False)
96
96
97 response = self.app.get(route_path(
97 response = self.app.get(route_path(
98 'pullrequest_show',
98 'pullrequest_show',
99 repo_name=pull_request.target_repo.scm_instance().name,
99 repo_name=pull_request.target_repo.scm_instance().name,
100 pull_request_id=pull_request.pull_request_id,
100 pull_request_id=pull_request.pull_request_id,
101 params={'range-diff': range_diff}))
101 params={'range-diff': range_diff}))
102
102
103 for commit_id in pull_request.revisions:
103 for commit_id in pull_request.revisions:
104 response.mustcontain(commit_id)
104 response.mustcontain(commit_id)
105
105
106 response.mustcontain(pull_request.target_ref_parts.type)
106 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.name)
107 response.mustcontain(pull_request.target_ref_parts.name)
108
108
109 response.mustcontain('class="pull-request-merge"')
109 response.mustcontain('class="pull-request-merge"')
110
110
111 if pr_merge_enabled:
111 if pr_merge_enabled:
112 response.mustcontain('Pull request reviewer approval is pending')
112 response.mustcontain('Pull request reviewer approval is pending')
113 else:
113 else:
114 response.mustcontain('Server-side pull request merging is disabled.')
114 response.mustcontain('Server-side pull request merging is disabled.')
115
115
116 if range_diff == "1":
116 if range_diff == "1":
117 response.mustcontain('Turn off: Show the diff as commit range')
117 response.mustcontain('Turn off: Show the diff as commit range')
118
118
119 def test_show_versions_of_pr(self, backend, csrf_token):
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
120 commits = [
121 {'message': 'initial-commit',
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
123
124 {'message': 'commit-1',
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
126 # Above is the initial version of PR that changes a single line
127
127
128 # from now on we'll add 3x commit adding a nother line on each step
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
131
132 {'message': 'commit-3',
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
134
135 {'message': 'commit-4',
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
137 ]
138
138
139 commit_ids = backend.create_master_repo(commits)
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
143 target_repo_name = target.repo_name
144
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
149
150 response = self.app.post(
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
152 [
153 ('source_repo', source_repo_name),
153 ('source_repo', source_repo_name),
154 ('source_ref', source_ref),
154 ('source_ref', source_ref),
155 ('target_repo', target_repo_name),
155 ('target_repo', target_repo_name),
156 ('target_ref', target_ref),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
175 ('user', ''),
176 ('csrf_token', csrf_token),
176 ('csrf_token', csrf_token),
177 ],
177 ],
178 status=302)
178 status=302)
179
179
180 location = response.headers['Location']
180 location = response.headers['Location']
181
181
182 pull_request_id = location.rsplit('/', 1)[1]
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
184 pull_request = PullRequest.get(int(pull_request_id))
185
185
186 pull_request_id = pull_request.pull_request_id
186 pull_request_id = pull_request.pull_request_id
187
187
188 # Show initial version of PR
188 # Show initial version of PR
189 response = self.app.get(
189 response = self.app.get(
190 route_path('pullrequest_show',
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
192 pull_request_id=pull_request_id))
193
193
194 response.mustcontain('commit-1')
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
197 response.mustcontain(no=['commit-4'])
198
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
202 response.mustcontain(no=['LINE5'])
203
203
204 # update PR #1
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
207 response = self.app.post(
208 route_path('pullrequest_update',
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
211
212 # update PR #2
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
215 response = self.app.post(
216 route_path('pullrequest_update',
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
219
220 # update PR #3
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
223 response = self.app.post(
224 route_path('pullrequest_update',
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
227
228 # Show final version !
228 # Show final version !
229 response = self.app.get(
229 response = self.app.get(
230 route_path('pullrequest_show',
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
232 pull_request_id=pull_request_id))
233
233
234 # 3 updates, and the latest == 4
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
237
238 # initial show must have 3 commits, and 3 adds
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
242 response.mustcontain('commit-4')
243
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
248
249 # fetch versions
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
252 assert len(versions) == 3
253
253
254 # show v1,v2,v3,v4
254 # show v1,v2,v3,v4
255 def cb_line(text):
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
257
258 def cb_context(text):
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
260 '</span><span>{}</span></span>'.format(text)
261
261
262 commit_tests = {
262 commit_tests = {
263 # in response, not in response
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
268 }
269 diff_tests = {
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
274 }
275 for idx, ver in enumerate(versions, 1):
275 for idx, ver in enumerate(versions, 1):
276
276
277 response = self.app.get(
277 response = self.app.get(
278 route_path('pullrequest_show',
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
281 params={'version': ver}))
282
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
285
286 yes, no = commit_tests[idx]
286 yes, no = commit_tests[idx]
287 for y in yes:
287 for y in yes:
288 response.mustcontain(y)
288 response.mustcontain(y)
289 for n in no:
289 for n in no:
290 response.mustcontain(no=n)
290 response.mustcontain(no=n)
291
291
292 yes, no = diff_tests[idx]
292 yes, no = diff_tests[idx]
293 for y in yes:
293 for y in yes:
294 response.mustcontain(cb_line(y))
294 response.mustcontain(cb_line(y))
295 for n in no:
295 for n in no:
296 response.mustcontain(no=n)
296 response.mustcontain(no=n)
297
297
298 # show diff between versions
298 # show diff between versions
299 diff_compare_tests = {
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
303 }
304 for idx, ver in enumerate(versions, 1):
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
305 adds, context = diff_compare_tests[idx]
306
306
307 to_ver = ver+1
307 to_ver = ver+1
308 if idx == 3:
308 if idx == 3:
309 to_ver = 'latest'
309 to_ver = 'latest'
310
310
311 response = self.app.get(
311 response = self.app.get(
312 route_path('pullrequest_show',
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
315 params={'from_version': versions[0], 'version': to_ver}))
316
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
318
319 for a in adds:
319 for a in adds:
320 response.mustcontain(cb_line(a))
320 response.mustcontain(cb_line(a))
321 for c in context:
321 for c in context:
322 response.mustcontain(cb_context(c))
322 response.mustcontain(cb_context(c))
323
323
324 # test version v2 -> v3
324 # test version v2 -> v3
325 response = self.app.get(
325 response = self.app.get(
326 route_path('pullrequest_show',
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
329 params={'from_version': versions[1], 'version': versions[2]}))
330
330
331 response.mustcontain(cb_context('LINE1'))
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
334 response.mustcontain(cb_line('LINE4'))
335
335
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 # Logout
337 # Logout
338 response = self.app.post(
338 response = self.app.post(
339 h.route_path('logout'),
339 h.route_path('logout'),
340 params={'csrf_token': csrf_token})
340 params={'csrf_token': csrf_token})
341 # Login as regular user
341 # Login as regular user
342 response = self.app.post(h.route_path('login'),
342 response = self.app.post(h.route_path('login'),
343 {'username': TEST_USER_REGULAR_LOGIN,
343 {'username': TEST_USER_REGULAR_LOGIN,
344 'password': 'test12'})
344 'password': 'test12'})
345
345
346 pull_request = pr_util.create_pull_request(
346 pull_request = pr_util.create_pull_request(
347 author=TEST_USER_REGULAR_LOGIN)
347 author=TEST_USER_REGULAR_LOGIN)
348
348
349 response = self.app.get(route_path(
349 response = self.app.get(route_path(
350 'pullrequest_show',
350 'pullrequest_show',
351 repo_name=pull_request.target_repo.scm_instance().name,
351 repo_name=pull_request.target_repo.scm_instance().name,
352 pull_request_id=pull_request.pull_request_id))
352 pull_request_id=pull_request.pull_request_id))
353
353
354 response.mustcontain('Server-side pull request merging is disabled.')
354 response.mustcontain('Server-side pull request merging is disabled.')
355
355
356 assert_response = response.assert_response()
356 assert_response = response.assert_response()
357 # for regular user without a merge permissions, we don't see it
357 # for regular user without a merge permissions, we don't see it
358 assert_response.no_element_exists('#close-pull-request-action')
358 assert_response.no_element_exists('#close-pull-request-action')
359
359
360 user_util.grant_user_permission_to_repo(
360 user_util.grant_user_permission_to_repo(
361 pull_request.target_repo,
361 pull_request.target_repo,
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 'repository.write')
363 'repository.write')
364 response = self.app.get(route_path(
364 response = self.app.get(route_path(
365 'pullrequest_show',
365 'pullrequest_show',
366 repo_name=pull_request.target_repo.scm_instance().name,
366 repo_name=pull_request.target_repo.scm_instance().name,
367 pull_request_id=pull_request.pull_request_id))
367 pull_request_id=pull_request.pull_request_id))
368
368
369 response.mustcontain('Server-side pull request merging is disabled.')
369 response.mustcontain('Server-side pull request merging is disabled.')
370
370
371 assert_response = response.assert_response()
371 assert_response = response.assert_response()
372 # now regular user has a merge permissions, we have CLOSE button
372 # now regular user has a merge permissions, we have CLOSE button
373 assert_response.one_element_exists('#close-pull-request-action')
373 assert_response.one_element_exists('#close-pull-request-action')
374
374
375 def test_show_invalid_commit_id(self, pr_util):
375 def test_show_invalid_commit_id(self, pr_util):
376 # Simulating invalid revisions which will cause a lookup error
376 # Simulating invalid revisions which will cause a lookup error
377 pull_request = pr_util.create_pull_request()
377 pull_request = pr_util.create_pull_request()
378 pull_request.revisions = ['invalid']
378 pull_request.revisions = ['invalid']
379 Session().add(pull_request)
379 Session().add(pull_request)
380 Session().commit()
380 Session().commit()
381
381
382 response = self.app.get(route_path(
382 response = self.app.get(route_path(
383 'pullrequest_show',
383 'pullrequest_show',
384 repo_name=pull_request.target_repo.scm_instance().name,
384 repo_name=pull_request.target_repo.scm_instance().name,
385 pull_request_id=pull_request.pull_request_id))
385 pull_request_id=pull_request.pull_request_id))
386
386
387 for commit_id in pull_request.revisions:
387 for commit_id in pull_request.revisions:
388 response.mustcontain(commit_id)
388 response.mustcontain(commit_id)
389
389
390 def test_show_invalid_source_reference(self, pr_util):
390 def test_show_invalid_source_reference(self, pr_util):
391 pull_request = pr_util.create_pull_request()
391 pull_request = pr_util.create_pull_request()
392 pull_request.source_ref = 'branch:b:invalid'
392 pull_request.source_ref = 'branch:b:invalid'
393 Session().add(pull_request)
393 Session().add(pull_request)
394 Session().commit()
394 Session().commit()
395
395
396 self.app.get(route_path(
396 self.app.get(route_path(
397 'pullrequest_show',
397 'pullrequest_show',
398 repo_name=pull_request.target_repo.scm_instance().name,
398 repo_name=pull_request.target_repo.scm_instance().name,
399 pull_request_id=pull_request.pull_request_id))
399 pull_request_id=pull_request.pull_request_id))
400
400
401 def test_edit_title_description(self, pr_util, csrf_token):
401 def test_edit_title_description(self, pr_util, csrf_token):
402 pull_request = pr_util.create_pull_request()
402 pull_request = pr_util.create_pull_request()
403 pull_request_id = pull_request.pull_request_id
403 pull_request_id = pull_request.pull_request_id
404
404
405 response = self.app.post(
405 response = self.app.post(
406 route_path('pullrequest_update',
406 route_path('pullrequest_update',
407 repo_name=pull_request.target_repo.repo_name,
407 repo_name=pull_request.target_repo.repo_name,
408 pull_request_id=pull_request_id),
408 pull_request_id=pull_request_id),
409 params={
409 params={
410 'edit_pull_request': 'true',
410 'edit_pull_request': 'true',
411 'title': 'New title',
411 'title': 'New title',
412 'description': 'New description',
412 'description': 'New description',
413 'csrf_token': csrf_token})
413 'csrf_token': csrf_token})
414
414
415 assert_session_flash(
415 assert_session_flash(
416 response, u'Pull request title & description updated.',
416 response, u'Pull request title & description updated.',
417 category='success')
417 category='success')
418
418
419 pull_request = PullRequest.get(pull_request_id)
419 pull_request = PullRequest.get(pull_request_id)
420 assert pull_request.title == 'New title'
420 assert pull_request.title == 'New title'
421 assert pull_request.description == 'New description'
421 assert pull_request.description == 'New description'
422
422
423 def test_edit_title_description_closed(self, pr_util, csrf_token):
423 def test_edit_title_description_closed(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
424 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
425 pull_request_id = pull_request.pull_request_id
426 repo_name = pull_request.target_repo.repo_name
426 repo_name = pull_request.target_repo.repo_name
427 pr_util.close()
427 pr_util.close()
428
428
429 response = self.app.post(
429 response = self.app.post(
430 route_path('pullrequest_update',
430 route_path('pullrequest_update',
431 repo_name=repo_name, pull_request_id=pull_request_id),
431 repo_name=repo_name, pull_request_id=pull_request_id),
432 params={
432 params={
433 'edit_pull_request': 'true',
433 'edit_pull_request': 'true',
434 'title': 'New title',
434 'title': 'New title',
435 'description': 'New description',
435 'description': 'New description',
436 'csrf_token': csrf_token}, status=200)
436 'csrf_token': csrf_token}, status=200)
437 assert_session_flash(
437 assert_session_flash(
438 response, u'Cannot update closed pull requests.',
438 response, u'Cannot update closed pull requests.',
439 category='error')
439 category='error')
440
440
441 def test_update_invalid_source_reference(self, pr_util, csrf_token):
441 def test_update_invalid_source_reference(self, pr_util, csrf_token):
442 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
442 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
443
443
444 pull_request = pr_util.create_pull_request()
444 pull_request = pr_util.create_pull_request()
445 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
445 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
446 Session().add(pull_request)
446 Session().add(pull_request)
447 Session().commit()
447 Session().commit()
448
448
449 pull_request_id = pull_request.pull_request_id
449 pull_request_id = pull_request.pull_request_id
450
450
451 response = self.app.post(
451 response = self.app.post(
452 route_path('pullrequest_update',
452 route_path('pullrequest_update',
453 repo_name=pull_request.target_repo.repo_name,
453 repo_name=pull_request.target_repo.repo_name,
454 pull_request_id=pull_request_id),
454 pull_request_id=pull_request_id),
455 params={'update_commits': 'true', 'csrf_token': csrf_token})
455 params={'update_commits': 'true', 'csrf_token': csrf_token})
456
456
457 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
457 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
458 UpdateFailureReason.MISSING_SOURCE_REF])
458 UpdateFailureReason.MISSING_SOURCE_REF])
459 assert_session_flash(response, expected_msg, category='error')
459 assert_session_flash(response, expected_msg, category='error')
460
460
461 def test_missing_target_reference(self, pr_util, csrf_token):
461 def test_missing_target_reference(self, pr_util, csrf_token):
462 from rhodecode.lib.vcs.backends.base import MergeFailureReason
462 from rhodecode.lib.vcs.backends.base import MergeFailureReason
463 pull_request = pr_util.create_pull_request(
463 pull_request = pr_util.create_pull_request(
464 approved=True, mergeable=True)
464 approved=True, mergeable=True)
465 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
465 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
466 pull_request.target_ref = unicode_reference
466 pull_request.target_ref = unicode_reference
467 Session().add(pull_request)
467 Session().add(pull_request)
468 Session().commit()
468 Session().commit()
469
469
470 pull_request_id = pull_request.pull_request_id
470 pull_request_id = pull_request.pull_request_id
471 pull_request_url = route_path(
471 pull_request_url = route_path(
472 'pullrequest_show',
472 'pullrequest_show',
473 repo_name=pull_request.target_repo.repo_name,
473 repo_name=pull_request.target_repo.repo_name,
474 pull_request_id=pull_request_id)
474 pull_request_id=pull_request_id)
475
475
476 response = self.app.get(pull_request_url)
476 response = self.app.get(pull_request_url)
477 target_ref_id = 'invalid-branch'
477 target_ref_id = 'invalid-branch'
478 merge_resp = MergeResponse(
478 merge_resp = MergeResponse(
479 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
479 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
480 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
480 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
481 response.assert_response().element_contains(
481 response.assert_response().element_contains(
482 'div[data-role="merge-message"]', merge_resp.merge_status_message)
482 'div[data-role="merge-message"]', merge_resp.merge_status_message)
483
483
484 def test_comment_and_close_pull_request_custom_message_approved(
484 def test_comment_and_close_pull_request_custom_message_approved(
485 self, pr_util, csrf_token, xhr_header):
485 self, pr_util, csrf_token, xhr_header):
486
486
487 pull_request = pr_util.create_pull_request(approved=True)
487 pull_request = pr_util.create_pull_request(approved=True)
488 pull_request_id = pull_request.pull_request_id
488 pull_request_id = pull_request.pull_request_id
489 author = pull_request.user_id
489 author = pull_request.user_id
490 repo = pull_request.target_repo.repo_id
490 repo = pull_request.target_repo.repo_id
491
491
492 self.app.post(
492 self.app.post(
493 route_path('pullrequest_comment_create',
493 route_path('pullrequest_comment_create',
494 repo_name=pull_request.target_repo.scm_instance().name,
494 repo_name=pull_request.target_repo.scm_instance().name,
495 pull_request_id=pull_request_id),
495 pull_request_id=pull_request_id),
496 params={
496 params={
497 'close_pull_request': '1',
497 'close_pull_request': '1',
498 'text': 'Closing a PR',
498 'text': 'Closing a PR',
499 'csrf_token': csrf_token},
499 'csrf_token': csrf_token},
500 extra_environ=xhr_header,)
500 extra_environ=xhr_header,)
501
501
502 journal = UserLog.query()\
502 journal = UserLog.query()\
503 .filter(UserLog.user_id == author)\
503 .filter(UserLog.user_id == author)\
504 .filter(UserLog.repository_id == repo) \
504 .filter(UserLog.repository_id == repo) \
505 .order_by(UserLog.user_log_id.asc()) \
505 .order_by(UserLog.user_log_id.asc()) \
506 .all()
506 .all()
507 assert journal[-1].action == 'repo.pull_request.close'
507 assert journal[-1].action == 'repo.pull_request.close'
508
508
509 pull_request = PullRequest.get(pull_request_id)
509 pull_request = PullRequest.get(pull_request_id)
510 assert pull_request.is_closed()
510 assert pull_request.is_closed()
511
511
512 status = ChangesetStatusModel().get_status(
512 status = ChangesetStatusModel().get_status(
513 pull_request.source_repo, pull_request=pull_request)
513 pull_request.source_repo, pull_request=pull_request)
514 assert status == ChangesetStatus.STATUS_APPROVED
514 assert status == ChangesetStatus.STATUS_APPROVED
515 comments = ChangesetComment().query() \
515 comments = ChangesetComment().query() \
516 .filter(ChangesetComment.pull_request == pull_request) \
516 .filter(ChangesetComment.pull_request == pull_request) \
517 .order_by(ChangesetComment.comment_id.asc())\
517 .order_by(ChangesetComment.comment_id.asc())\
518 .all()
518 .all()
519 assert comments[-1].text == 'Closing a PR'
519 assert comments[-1].text == 'Closing a PR'
520
520
521 def test_comment_force_close_pull_request_rejected(
521 def test_comment_force_close_pull_request_rejected(
522 self, pr_util, csrf_token, xhr_header):
522 self, pr_util, csrf_token, xhr_header):
523 pull_request = pr_util.create_pull_request()
523 pull_request = pr_util.create_pull_request()
524 pull_request_id = pull_request.pull_request_id
524 pull_request_id = pull_request.pull_request_id
525 PullRequestModel().update_reviewers(
525 PullRequestModel().update_reviewers(
526 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
526 pull_request_id, [
527 (1, ['reason'], False, 'reviewer', []),
528 (2, ['reason2'], False, 'reviewer', [])],
527 pull_request.author)
529 pull_request.author)
528 author = pull_request.user_id
530 author = pull_request.user_id
529 repo = pull_request.target_repo.repo_id
531 repo = pull_request.target_repo.repo_id
530
532
531 self.app.post(
533 self.app.post(
532 route_path('pullrequest_comment_create',
534 route_path('pullrequest_comment_create',
533 repo_name=pull_request.target_repo.scm_instance().name,
535 repo_name=pull_request.target_repo.scm_instance().name,
534 pull_request_id=pull_request_id),
536 pull_request_id=pull_request_id),
535 params={
537 params={
536 'close_pull_request': '1',
538 'close_pull_request': '1',
537 'csrf_token': csrf_token},
539 'csrf_token': csrf_token},
538 extra_environ=xhr_header)
540 extra_environ=xhr_header)
539
541
540 pull_request = PullRequest.get(pull_request_id)
542 pull_request = PullRequest.get(pull_request_id)
541
543
542 journal = UserLog.query()\
544 journal = UserLog.query()\
543 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
545 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
544 .order_by(UserLog.user_log_id.asc()) \
546 .order_by(UserLog.user_log_id.asc()) \
545 .all()
547 .all()
546 assert journal[-1].action == 'repo.pull_request.close'
548 assert journal[-1].action == 'repo.pull_request.close'
547
549
548 # check only the latest status, not the review status
550 # check only the latest status, not the review status
549 status = ChangesetStatusModel().get_status(
551 status = ChangesetStatusModel().get_status(
550 pull_request.source_repo, pull_request=pull_request)
552 pull_request.source_repo, pull_request=pull_request)
551 assert status == ChangesetStatus.STATUS_REJECTED
553 assert status == ChangesetStatus.STATUS_REJECTED
552
554
553 def test_comment_and_close_pull_request(
555 def test_comment_and_close_pull_request(
554 self, pr_util, csrf_token, xhr_header):
556 self, pr_util, csrf_token, xhr_header):
555 pull_request = pr_util.create_pull_request()
557 pull_request = pr_util.create_pull_request()
556 pull_request_id = pull_request.pull_request_id
558 pull_request_id = pull_request.pull_request_id
557
559
558 response = self.app.post(
560 response = self.app.post(
559 route_path('pullrequest_comment_create',
561 route_path('pullrequest_comment_create',
560 repo_name=pull_request.target_repo.scm_instance().name,
562 repo_name=pull_request.target_repo.scm_instance().name,
561 pull_request_id=pull_request.pull_request_id),
563 pull_request_id=pull_request.pull_request_id),
562 params={
564 params={
563 'close_pull_request': 'true',
565 'close_pull_request': 'true',
564 'csrf_token': csrf_token},
566 'csrf_token': csrf_token},
565 extra_environ=xhr_header)
567 extra_environ=xhr_header)
566
568
567 assert response.json
569 assert response.json
568
570
569 pull_request = PullRequest.get(pull_request_id)
571 pull_request = PullRequest.get(pull_request_id)
570 assert pull_request.is_closed()
572 assert pull_request.is_closed()
571
573
572 # check only the latest status, not the review status
574 # check only the latest status, not the review status
573 status = ChangesetStatusModel().get_status(
575 status = ChangesetStatusModel().get_status(
574 pull_request.source_repo, pull_request=pull_request)
576 pull_request.source_repo, pull_request=pull_request)
575 assert status == ChangesetStatus.STATUS_REJECTED
577 assert status == ChangesetStatus.STATUS_REJECTED
576
578
577 def test_comment_and_close_pull_request_try_edit_comment(
579 def test_comment_and_close_pull_request_try_edit_comment(
578 self, pr_util, csrf_token, xhr_header
580 self, pr_util, csrf_token, xhr_header
579 ):
581 ):
580 pull_request = pr_util.create_pull_request()
582 pull_request = pr_util.create_pull_request()
581 pull_request_id = pull_request.pull_request_id
583 pull_request_id = pull_request.pull_request_id
582 target_scm = pull_request.target_repo.scm_instance()
584 target_scm = pull_request.target_repo.scm_instance()
583 target_scm_name = target_scm.name
585 target_scm_name = target_scm.name
584
586
585 response = self.app.post(
587 response = self.app.post(
586 route_path(
588 route_path(
587 'pullrequest_comment_create',
589 'pullrequest_comment_create',
588 repo_name=target_scm_name,
590 repo_name=target_scm_name,
589 pull_request_id=pull_request_id,
591 pull_request_id=pull_request_id,
590 ),
592 ),
591 params={
593 params={
592 'close_pull_request': 'true',
594 'close_pull_request': 'true',
593 'csrf_token': csrf_token,
595 'csrf_token': csrf_token,
594 },
596 },
595 extra_environ=xhr_header)
597 extra_environ=xhr_header)
596
598
597 assert response.json
599 assert response.json
598
600
599 pull_request = PullRequest.get(pull_request_id)
601 pull_request = PullRequest.get(pull_request_id)
600 target_scm = pull_request.target_repo.scm_instance()
602 target_scm = pull_request.target_repo.scm_instance()
601 target_scm_name = target_scm.name
603 target_scm_name = target_scm.name
602 assert pull_request.is_closed()
604 assert pull_request.is_closed()
603
605
604 # check only the latest status, not the review status
606 # check only the latest status, not the review status
605 status = ChangesetStatusModel().get_status(
607 status = ChangesetStatusModel().get_status(
606 pull_request.source_repo, pull_request=pull_request)
608 pull_request.source_repo, pull_request=pull_request)
607 assert status == ChangesetStatus.STATUS_REJECTED
609 assert status == ChangesetStatus.STATUS_REJECTED
608
610
609 comment_id = response.json.get('comment_id', None)
611 comment_id = response.json.get('comment_id', None)
610 test_text = 'test'
612 test_text = 'test'
611 response = self.app.post(
613 response = self.app.post(
612 route_path(
614 route_path(
613 'pullrequest_comment_edit',
615 'pullrequest_comment_edit',
614 repo_name=target_scm_name,
616 repo_name=target_scm_name,
615 pull_request_id=pull_request_id,
617 pull_request_id=pull_request_id,
616 comment_id=comment_id,
618 comment_id=comment_id,
617 ),
619 ),
618 extra_environ=xhr_header,
620 extra_environ=xhr_header,
619 params={
621 params={
620 'csrf_token': csrf_token,
622 'csrf_token': csrf_token,
621 'text': test_text,
623 'text': test_text,
622 },
624 },
623 status=403,
625 status=403,
624 )
626 )
625 assert response.status_int == 403
627 assert response.status_int == 403
626
628
627 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
629 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
628 pull_request = pr_util.create_pull_request()
630 pull_request = pr_util.create_pull_request()
629 target_scm = pull_request.target_repo.scm_instance()
631 target_scm = pull_request.target_repo.scm_instance()
630 target_scm_name = target_scm.name
632 target_scm_name = target_scm.name
631
633
632 response = self.app.post(
634 response = self.app.post(
633 route_path(
635 route_path(
634 'pullrequest_comment_create',
636 'pullrequest_comment_create',
635 repo_name=target_scm_name,
637 repo_name=target_scm_name,
636 pull_request_id=pull_request.pull_request_id),
638 pull_request_id=pull_request.pull_request_id),
637 params={
639 params={
638 'csrf_token': csrf_token,
640 'csrf_token': csrf_token,
639 'text': 'init',
641 'text': 'init',
640 },
642 },
641 extra_environ=xhr_header,
643 extra_environ=xhr_header,
642 )
644 )
643 assert response.json
645 assert response.json
644
646
645 comment_id = response.json.get('comment_id', None)
647 comment_id = response.json.get('comment_id', None)
646 assert comment_id
648 assert comment_id
647 test_text = 'test'
649 test_text = 'test'
648 self.app.post(
650 self.app.post(
649 route_path(
651 route_path(
650 'pullrequest_comment_edit',
652 'pullrequest_comment_edit',
651 repo_name=target_scm_name,
653 repo_name=target_scm_name,
652 pull_request_id=pull_request.pull_request_id,
654 pull_request_id=pull_request.pull_request_id,
653 comment_id=comment_id,
655 comment_id=comment_id,
654 ),
656 ),
655 extra_environ=xhr_header,
657 extra_environ=xhr_header,
656 params={
658 params={
657 'csrf_token': csrf_token,
659 'csrf_token': csrf_token,
658 'text': test_text,
660 'text': test_text,
659 'version': '0',
661 'version': '0',
660 },
662 },
661
663
662 )
664 )
663 text_form_db = ChangesetComment.query().filter(
665 text_form_db = ChangesetComment.query().filter(
664 ChangesetComment.comment_id == comment_id).first().text
666 ChangesetComment.comment_id == comment_id).first().text
665 assert test_text == text_form_db
667 assert test_text == text_form_db
666
668
667 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
669 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
668 pull_request = pr_util.create_pull_request()
670 pull_request = pr_util.create_pull_request()
669 target_scm = pull_request.target_repo.scm_instance()
671 target_scm = pull_request.target_repo.scm_instance()
670 target_scm_name = target_scm.name
672 target_scm_name = target_scm.name
671
673
672 response = self.app.post(
674 response = self.app.post(
673 route_path(
675 route_path(
674 'pullrequest_comment_create',
676 'pullrequest_comment_create',
675 repo_name=target_scm_name,
677 repo_name=target_scm_name,
676 pull_request_id=pull_request.pull_request_id),
678 pull_request_id=pull_request.pull_request_id),
677 params={
679 params={
678 'csrf_token': csrf_token,
680 'csrf_token': csrf_token,
679 'text': 'init',
681 'text': 'init',
680 },
682 },
681 extra_environ=xhr_header,
683 extra_environ=xhr_header,
682 )
684 )
683 assert response.json
685 assert response.json
684
686
685 comment_id = response.json.get('comment_id', None)
687 comment_id = response.json.get('comment_id', None)
686 assert comment_id
688 assert comment_id
687 test_text = 'init'
689 test_text = 'init'
688 response = self.app.post(
690 response = self.app.post(
689 route_path(
691 route_path(
690 'pullrequest_comment_edit',
692 'pullrequest_comment_edit',
691 repo_name=target_scm_name,
693 repo_name=target_scm_name,
692 pull_request_id=pull_request.pull_request_id,
694 pull_request_id=pull_request.pull_request_id,
693 comment_id=comment_id,
695 comment_id=comment_id,
694 ),
696 ),
695 extra_environ=xhr_header,
697 extra_environ=xhr_header,
696 params={
698 params={
697 'csrf_token': csrf_token,
699 'csrf_token': csrf_token,
698 'text': test_text,
700 'text': test_text,
699 'version': '0',
701 'version': '0',
700 },
702 },
701 status=404,
703 status=404,
702
704
703 )
705 )
704 assert response.status_int == 404
706 assert response.status_int == 404
705
707
706 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
708 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
707 pull_request = pr_util.create_pull_request()
709 pull_request = pr_util.create_pull_request()
708 target_scm = pull_request.target_repo.scm_instance()
710 target_scm = pull_request.target_repo.scm_instance()
709 target_scm_name = target_scm.name
711 target_scm_name = target_scm.name
710
712
711 response = self.app.post(
713 response = self.app.post(
712 route_path(
714 route_path(
713 'pullrequest_comment_create',
715 'pullrequest_comment_create',
714 repo_name=target_scm_name,
716 repo_name=target_scm_name,
715 pull_request_id=pull_request.pull_request_id),
717 pull_request_id=pull_request.pull_request_id),
716 params={
718 params={
717 'csrf_token': csrf_token,
719 'csrf_token': csrf_token,
718 'text': 'init',
720 'text': 'init',
719 },
721 },
720 extra_environ=xhr_header,
722 extra_environ=xhr_header,
721 )
723 )
722 assert response.json
724 assert response.json
723 comment_id = response.json.get('comment_id', None)
725 comment_id = response.json.get('comment_id', None)
724 assert comment_id
726 assert comment_id
725
727
726 test_text = 'test'
728 test_text = 'test'
727 self.app.post(
729 self.app.post(
728 route_path(
730 route_path(
729 'pullrequest_comment_edit',
731 'pullrequest_comment_edit',
730 repo_name=target_scm_name,
732 repo_name=target_scm_name,
731 pull_request_id=pull_request.pull_request_id,
733 pull_request_id=pull_request.pull_request_id,
732 comment_id=comment_id,
734 comment_id=comment_id,
733 ),
735 ),
734 extra_environ=xhr_header,
736 extra_environ=xhr_header,
735 params={
737 params={
736 'csrf_token': csrf_token,
738 'csrf_token': csrf_token,
737 'text': test_text,
739 'text': test_text,
738 'version': '0',
740 'version': '0',
739 },
741 },
740
742
741 )
743 )
742 test_text_v2 = 'test_v2'
744 test_text_v2 = 'test_v2'
743 response = self.app.post(
745 response = self.app.post(
744 route_path(
746 route_path(
745 'pullrequest_comment_edit',
747 'pullrequest_comment_edit',
746 repo_name=target_scm_name,
748 repo_name=target_scm_name,
747 pull_request_id=pull_request.pull_request_id,
749 pull_request_id=pull_request.pull_request_id,
748 comment_id=comment_id,
750 comment_id=comment_id,
749 ),
751 ),
750 extra_environ=xhr_header,
752 extra_environ=xhr_header,
751 params={
753 params={
752 'csrf_token': csrf_token,
754 'csrf_token': csrf_token,
753 'text': test_text_v2,
755 'text': test_text_v2,
754 'version': '0',
756 'version': '0',
755 },
757 },
756 status=409,
758 status=409,
757 )
759 )
758 assert response.status_int == 409
760 assert response.status_int == 409
759
761
760 text_form_db = ChangesetComment.query().filter(
762 text_form_db = ChangesetComment.query().filter(
761 ChangesetComment.comment_id == comment_id).first().text
763 ChangesetComment.comment_id == comment_id).first().text
762
764
763 assert test_text == text_form_db
765 assert test_text == text_form_db
764 assert test_text_v2 != text_form_db
766 assert test_text_v2 != text_form_db
765
767
766 def test_comment_and_comment_edit_permissions_forbidden(
768 def test_comment_and_comment_edit_permissions_forbidden(
767 self, autologin_regular_user, user_regular, user_admin, pr_util,
769 self, autologin_regular_user, user_regular, user_admin, pr_util,
768 csrf_token, xhr_header):
770 csrf_token, xhr_header):
769 pull_request = pr_util.create_pull_request(
771 pull_request = pr_util.create_pull_request(
770 author=user_admin.username, enable_notifications=False)
772 author=user_admin.username, enable_notifications=False)
771 comment = CommentsModel().create(
773 comment = CommentsModel().create(
772 text='test',
774 text='test',
773 repo=pull_request.target_repo.scm_instance().name,
775 repo=pull_request.target_repo.scm_instance().name,
774 user=user_admin,
776 user=user_admin,
775 pull_request=pull_request,
777 pull_request=pull_request,
776 )
778 )
777 response = self.app.post(
779 response = self.app.post(
778 route_path(
780 route_path(
779 'pullrequest_comment_edit',
781 'pullrequest_comment_edit',
780 repo_name=pull_request.target_repo.scm_instance().name,
782 repo_name=pull_request.target_repo.scm_instance().name,
781 pull_request_id=pull_request.pull_request_id,
783 pull_request_id=pull_request.pull_request_id,
782 comment_id=comment.comment_id,
784 comment_id=comment.comment_id,
783 ),
785 ),
784 extra_environ=xhr_header,
786 extra_environ=xhr_header,
785 params={
787 params={
786 'csrf_token': csrf_token,
788 'csrf_token': csrf_token,
787 'text': 'test_text',
789 'text': 'test_text',
788 },
790 },
789 status=403,
791 status=403,
790 )
792 )
791 assert response.status_int == 403
793 assert response.status_int == 403
792
794
793 def test_create_pull_request(self, backend, csrf_token):
795 def test_create_pull_request(self, backend, csrf_token):
794 commits = [
796 commits = [
795 {'message': 'ancestor'},
797 {'message': 'ancestor'},
796 {'message': 'change'},
798 {'message': 'change'},
797 {'message': 'change2'},
799 {'message': 'change2'},
798 ]
800 ]
799 commit_ids = backend.create_master_repo(commits)
801 commit_ids = backend.create_master_repo(commits)
800 target = backend.create_repo(heads=['ancestor'])
802 target = backend.create_repo(heads=['ancestor'])
801 source = backend.create_repo(heads=['change2'])
803 source = backend.create_repo(heads=['change2'])
802
804
803 response = self.app.post(
805 response = self.app.post(
804 route_path('pullrequest_create', repo_name=source.repo_name),
806 route_path('pullrequest_create', repo_name=source.repo_name),
805 [
807 [
806 ('source_repo', source.repo_name),
808 ('source_repo', source.repo_name),
807 ('source_ref', 'branch:default:' + commit_ids['change2']),
809 ('source_ref', 'branch:default:' + commit_ids['change2']),
808 ('target_repo', target.repo_name),
810 ('target_repo', target.repo_name),
809 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
811 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
810 ('common_ancestor', commit_ids['ancestor']),
812 ('common_ancestor', commit_ids['ancestor']),
811 ('pullrequest_title', 'Title'),
813 ('pullrequest_title', 'Title'),
812 ('pullrequest_desc', 'Description'),
814 ('pullrequest_desc', 'Description'),
813 ('description_renderer', 'markdown'),
815 ('description_renderer', 'markdown'),
814 ('__start__', 'review_members:sequence'),
816 ('__start__', 'review_members:sequence'),
815 ('__start__', 'reviewer:mapping'),
817 ('__start__', 'reviewer:mapping'),
816 ('user_id', '1'),
818 ('user_id', '1'),
817 ('__start__', 'reasons:sequence'),
819 ('__start__', 'reasons:sequence'),
818 ('reason', 'Some reason'),
820 ('reason', 'Some reason'),
819 ('__end__', 'reasons:sequence'),
821 ('__end__', 'reasons:sequence'),
820 ('__start__', 'rules:sequence'),
822 ('__start__', 'rules:sequence'),
821 ('__end__', 'rules:sequence'),
823 ('__end__', 'rules:sequence'),
822 ('mandatory', 'False'),
824 ('mandatory', 'False'),
823 ('__end__', 'reviewer:mapping'),
825 ('__end__', 'reviewer:mapping'),
824 ('__end__', 'review_members:sequence'),
826 ('__end__', 'review_members:sequence'),
825 ('__start__', 'revisions:sequence'),
827 ('__start__', 'revisions:sequence'),
826 ('revisions', commit_ids['change']),
828 ('revisions', commit_ids['change']),
827 ('revisions', commit_ids['change2']),
829 ('revisions', commit_ids['change2']),
828 ('__end__', 'revisions:sequence'),
830 ('__end__', 'revisions:sequence'),
829 ('user', ''),
831 ('user', ''),
830 ('csrf_token', csrf_token),
832 ('csrf_token', csrf_token),
831 ],
833 ],
832 status=302)
834 status=302)
833
835
834 location = response.headers['Location']
836 location = response.headers['Location']
835 pull_request_id = location.rsplit('/', 1)[1]
837 pull_request_id = location.rsplit('/', 1)[1]
836 assert pull_request_id != 'new'
838 assert pull_request_id != 'new'
837 pull_request = PullRequest.get(int(pull_request_id))
839 pull_request = PullRequest.get(int(pull_request_id))
838
840
839 # check that we have now both revisions
841 # check that we have now both revisions
840 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
842 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
841 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
843 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
842 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
844 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
843 assert pull_request.target_ref == expected_target_ref
845 assert pull_request.target_ref == expected_target_ref
844
846
845 def test_reviewer_notifications(self, backend, csrf_token):
847 def test_reviewer_notifications(self, backend, csrf_token):
846 # We have to use the app.post for this test so it will create the
848 # We have to use the app.post for this test so it will create the
847 # notifications properly with the new PR
849 # notifications properly with the new PR
848 commits = [
850 commits = [
849 {'message': 'ancestor',
851 {'message': 'ancestor',
850 'added': [FileNode('file_A', content='content_of_ancestor')]},
852 'added': [FileNode('file_A', content='content_of_ancestor')]},
851 {'message': 'change',
853 {'message': 'change',
852 'added': [FileNode('file_a', content='content_of_change')]},
854 'added': [FileNode('file_a', content='content_of_change')]},
853 {'message': 'change-child'},
855 {'message': 'change-child'},
854 {'message': 'ancestor-child', 'parents': ['ancestor'],
856 {'message': 'ancestor-child', 'parents': ['ancestor'],
855 'added': [
857 'added': [
856 FileNode('file_B', content='content_of_ancestor_child')]},
858 FileNode('file_B', content='content_of_ancestor_child')]},
857 {'message': 'ancestor-child-2'},
859 {'message': 'ancestor-child-2'},
858 ]
860 ]
859 commit_ids = backend.create_master_repo(commits)
861 commit_ids = backend.create_master_repo(commits)
860 target = backend.create_repo(heads=['ancestor-child'])
862 target = backend.create_repo(heads=['ancestor-child'])
861 source = backend.create_repo(heads=['change'])
863 source = backend.create_repo(heads=['change'])
862
864
863 response = self.app.post(
865 response = self.app.post(
864 route_path('pullrequest_create', repo_name=source.repo_name),
866 route_path('pullrequest_create', repo_name=source.repo_name),
865 [
867 [
866 ('source_repo', source.repo_name),
868 ('source_repo', source.repo_name),
867 ('source_ref', 'branch:default:' + commit_ids['change']),
869 ('source_ref', 'branch:default:' + commit_ids['change']),
868 ('target_repo', target.repo_name),
870 ('target_repo', target.repo_name),
869 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
871 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
870 ('common_ancestor', commit_ids['ancestor']),
872 ('common_ancestor', commit_ids['ancestor']),
871 ('pullrequest_title', 'Title'),
873 ('pullrequest_title', 'Title'),
872 ('pullrequest_desc', 'Description'),
874 ('pullrequest_desc', 'Description'),
873 ('description_renderer', 'markdown'),
875 ('description_renderer', 'markdown'),
874 ('__start__', 'review_members:sequence'),
876 ('__start__', 'review_members:sequence'),
875 ('__start__', 'reviewer:mapping'),
877 ('__start__', 'reviewer:mapping'),
876 ('user_id', '2'),
878 ('user_id', '2'),
877 ('__start__', 'reasons:sequence'),
879 ('__start__', 'reasons:sequence'),
878 ('reason', 'Some reason'),
880 ('reason', 'Some reason'),
879 ('__end__', 'reasons:sequence'),
881 ('__end__', 'reasons:sequence'),
880 ('__start__', 'rules:sequence'),
882 ('__start__', 'rules:sequence'),
881 ('__end__', 'rules:sequence'),
883 ('__end__', 'rules:sequence'),
882 ('mandatory', 'False'),
884 ('mandatory', 'False'),
883 ('__end__', 'reviewer:mapping'),
885 ('__end__', 'reviewer:mapping'),
884 ('__end__', 'review_members:sequence'),
886 ('__end__', 'review_members:sequence'),
885 ('__start__', 'revisions:sequence'),
887 ('__start__', 'revisions:sequence'),
886 ('revisions', commit_ids['change']),
888 ('revisions', commit_ids['change']),
887 ('__end__', 'revisions:sequence'),
889 ('__end__', 'revisions:sequence'),
888 ('user', ''),
890 ('user', ''),
889 ('csrf_token', csrf_token),
891 ('csrf_token', csrf_token),
890 ],
892 ],
891 status=302)
893 status=302)
892
894
893 location = response.headers['Location']
895 location = response.headers['Location']
894
896
895 pull_request_id = location.rsplit('/', 1)[1]
897 pull_request_id = location.rsplit('/', 1)[1]
896 assert pull_request_id != 'new'
898 assert pull_request_id != 'new'
897 pull_request = PullRequest.get(int(pull_request_id))
899 pull_request = PullRequest.get(int(pull_request_id))
898
900
899 # Check that a notification was made
901 # Check that a notification was made
900 notifications = Notification.query()\
902 notifications = Notification.query()\
901 .filter(Notification.created_by == pull_request.author.user_id,
903 .filter(Notification.created_by == pull_request.author.user_id,
902 Notification.type_ == Notification.TYPE_PULL_REQUEST,
904 Notification.type_ == Notification.TYPE_PULL_REQUEST,
903 Notification.subject.contains(
905 Notification.subject.contains(
904 "requested a pull request review. !%s" % pull_request_id))
906 "requested a pull request review. !%s" % pull_request_id))
905 assert len(notifications.all()) == 1
907 assert len(notifications.all()) == 1
906
908
907 # Change reviewers and check that a notification was made
909 # Change reviewers and check that a notification was made
908 PullRequestModel().update_reviewers(
910 PullRequestModel().update_reviewers(
909 pull_request.pull_request_id, [(1, [], False, [])],
911 pull_request.pull_request_id, [
912 (1, [], False, 'reviewer', [])
913 ],
910 pull_request.author)
914 pull_request.author)
911 assert len(notifications.all()) == 2
915 assert len(notifications.all()) == 2
912
916
913 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
917 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
914 csrf_token):
915 commits = [
918 commits = [
916 {'message': 'ancestor',
919 {'message': 'ancestor',
917 'added': [FileNode('file_A', content='content_of_ancestor')]},
920 'added': [FileNode('file_A', content='content_of_ancestor')]},
918 {'message': 'change',
921 {'message': 'change',
919 'added': [FileNode('file_a', content='content_of_change')]},
922 'added': [FileNode('file_a', content='content_of_change')]},
920 {'message': 'change-child'},
923 {'message': 'change-child'},
921 {'message': 'ancestor-child', 'parents': ['ancestor'],
924 {'message': 'ancestor-child', 'parents': ['ancestor'],
922 'added': [
925 'added': [
923 FileNode('file_B', content='content_of_ancestor_child')]},
926 FileNode('file_B', content='content_of_ancestor_child')]},
924 {'message': 'ancestor-child-2'},
927 {'message': 'ancestor-child-2'},
925 ]
928 ]
926 commit_ids = backend.create_master_repo(commits)
929 commit_ids = backend.create_master_repo(commits)
927 target = backend.create_repo(heads=['ancestor-child'])
930 target = backend.create_repo(heads=['ancestor-child'])
928 source = backend.create_repo(heads=['change'])
931 source = backend.create_repo(heads=['change'])
929
932
930 response = self.app.post(
933 response = self.app.post(
931 route_path('pullrequest_create', repo_name=source.repo_name),
934 route_path('pullrequest_create', repo_name=source.repo_name),
932 [
935 [
933 ('source_repo', source.repo_name),
936 ('source_repo', source.repo_name),
934 ('source_ref', 'branch:default:' + commit_ids['change']),
937 ('source_ref', 'branch:default:' + commit_ids['change']),
935 ('target_repo', target.repo_name),
938 ('target_repo', target.repo_name),
936 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
939 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
937 ('common_ancestor', commit_ids['ancestor']),
940 ('common_ancestor', commit_ids['ancestor']),
938 ('pullrequest_title', 'Title'),
941 ('pullrequest_title', 'Title'),
939 ('pullrequest_desc', 'Description'),
942 ('pullrequest_desc', 'Description'),
940 ('description_renderer', 'markdown'),
943 ('description_renderer', 'markdown'),
941 ('__start__', 'review_members:sequence'),
944 ('__start__', 'review_members:sequence'),
942 ('__start__', 'reviewer:mapping'),
945 ('__start__', 'reviewer:mapping'),
943 ('user_id', '1'),
946 ('user_id', '1'),
944 ('__start__', 'reasons:sequence'),
947 ('__start__', 'reasons:sequence'),
945 ('reason', 'Some reason'),
948 ('reason', 'Some reason'),
946 ('__end__', 'reasons:sequence'),
949 ('__end__', 'reasons:sequence'),
947 ('__start__', 'rules:sequence'),
950 ('__start__', 'rules:sequence'),
948 ('__end__', 'rules:sequence'),
951 ('__end__', 'rules:sequence'),
949 ('mandatory', 'False'),
952 ('mandatory', 'False'),
950 ('__end__', 'reviewer:mapping'),
953 ('__end__', 'reviewer:mapping'),
951 ('__end__', 'review_members:sequence'),
954 ('__end__', 'review_members:sequence'),
952 ('__start__', 'revisions:sequence'),
955 ('__start__', 'revisions:sequence'),
953 ('revisions', commit_ids['change']),
956 ('revisions', commit_ids['change']),
954 ('__end__', 'revisions:sequence'),
957 ('__end__', 'revisions:sequence'),
955 ('user', ''),
958 ('user', ''),
956 ('csrf_token', csrf_token),
959 ('csrf_token', csrf_token),
957 ],
960 ],
958 status=302)
961 status=302)
959
962
960 location = response.headers['Location']
963 location = response.headers['Location']
961
964
962 pull_request_id = location.rsplit('/', 1)[1]
965 pull_request_id = location.rsplit('/', 1)[1]
963 assert pull_request_id != 'new'
966 assert pull_request_id != 'new'
964 pull_request = PullRequest.get(int(pull_request_id))
967 pull_request = PullRequest.get(int(pull_request_id))
965
968
966 # target_ref has to point to the ancestor's commit_id in order to
969 # target_ref has to point to the ancestor's commit_id in order to
967 # show the correct diff
970 # show the correct diff
968 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
971 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
969 assert pull_request.target_ref == expected_target_ref
972 assert pull_request.target_ref == expected_target_ref
970
973
971 # Check generated diff contents
974 # Check generated diff contents
972 response = response.follow()
975 response = response.follow()
973 response.mustcontain(no=['content_of_ancestor'])
976 response.mustcontain(no=['content_of_ancestor'])
974 response.mustcontain(no=['content_of_ancestor-child'])
977 response.mustcontain(no=['content_of_ancestor-child'])
975 response.mustcontain('content_of_change')
978 response.mustcontain('content_of_change')
976
979
977 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
980 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
978 # Clear any previous calls to rcextensions
981 # Clear any previous calls to rcextensions
979 rhodecode.EXTENSIONS.calls.clear()
982 rhodecode.EXTENSIONS.calls.clear()
980
983
981 pull_request = pr_util.create_pull_request(
984 pull_request = pr_util.create_pull_request(
982 approved=True, mergeable=True)
985 approved=True, mergeable=True)
983 pull_request_id = pull_request.pull_request_id
986 pull_request_id = pull_request.pull_request_id
984 repo_name = pull_request.target_repo.scm_instance().name,
987 repo_name = pull_request.target_repo.scm_instance().name,
985
988
986 url = route_path('pullrequest_merge',
989 url = route_path('pullrequest_merge',
987 repo_name=str(repo_name[0]),
990 repo_name=str(repo_name[0]),
988 pull_request_id=pull_request_id)
991 pull_request_id=pull_request_id)
989 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
992 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
990
993
991 pull_request = PullRequest.get(pull_request_id)
994 pull_request = PullRequest.get(pull_request_id)
992
995
993 assert response.status_int == 200
996 assert response.status_int == 200
994 assert pull_request.is_closed()
997 assert pull_request.is_closed()
995 assert_pull_request_status(
998 assert_pull_request_status(
996 pull_request, ChangesetStatus.STATUS_APPROVED)
999 pull_request, ChangesetStatus.STATUS_APPROVED)
997
1000
998 # Check the relevant log entries were added
1001 # Check the relevant log entries were added
999 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1002 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1000 actions = [log.action for log in user_logs]
1003 actions = [log.action for log in user_logs]
1001 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1004 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1002 expected_actions = [
1005 expected_actions = [
1003 u'repo.pull_request.close',
1006 u'repo.pull_request.close',
1004 u'repo.pull_request.merge',
1007 u'repo.pull_request.merge',
1005 u'repo.pull_request.comment.create'
1008 u'repo.pull_request.comment.create'
1006 ]
1009 ]
1007 assert actions == expected_actions
1010 assert actions == expected_actions
1008
1011
1009 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1012 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1010 actions = [log for log in user_logs]
1013 actions = [log for log in user_logs]
1011 assert actions[-1].action == 'user.push'
1014 assert actions[-1].action == 'user.push'
1012 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1015 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1013
1016
1014 # Check post_push rcextension was really executed
1017 # Check post_push rcextension was really executed
1015 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1018 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1016 assert len(push_calls) == 1
1019 assert len(push_calls) == 1
1017 unused_last_call_args, last_call_kwargs = push_calls[0]
1020 unused_last_call_args, last_call_kwargs = push_calls[0]
1018 assert last_call_kwargs['action'] == 'push'
1021 assert last_call_kwargs['action'] == 'push'
1019 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1022 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1020
1023
1021 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1024 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1022 pull_request = pr_util.create_pull_request(mergeable=False)
1025 pull_request = pr_util.create_pull_request(mergeable=False)
1023 pull_request_id = pull_request.pull_request_id
1026 pull_request_id = pull_request.pull_request_id
1024 pull_request = PullRequest.get(pull_request_id)
1027 pull_request = PullRequest.get(pull_request_id)
1025
1028
1026 response = self.app.post(
1029 response = self.app.post(
1027 route_path('pullrequest_merge',
1030 route_path('pullrequest_merge',
1028 repo_name=pull_request.target_repo.scm_instance().name,
1031 repo_name=pull_request.target_repo.scm_instance().name,
1029 pull_request_id=pull_request.pull_request_id),
1032 pull_request_id=pull_request.pull_request_id),
1030 params={'csrf_token': csrf_token}).follow()
1033 params={'csrf_token': csrf_token}).follow()
1031
1034
1032 assert response.status_int == 200
1035 assert response.status_int == 200
1033 response.mustcontain(
1036 response.mustcontain(
1034 'Merge is not currently possible because of below failed checks.')
1037 'Merge is not currently possible because of below failed checks.')
1035 response.mustcontain('Server-side pull request merging is disabled.')
1038 response.mustcontain('Server-side pull request merging is disabled.')
1036
1039
1037 @pytest.mark.skip_backends('svn')
1040 @pytest.mark.skip_backends('svn')
1038 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1041 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1039 pull_request = pr_util.create_pull_request(mergeable=True)
1042 pull_request = pr_util.create_pull_request(mergeable=True)
1040 pull_request_id = pull_request.pull_request_id
1043 pull_request_id = pull_request.pull_request_id
1041 repo_name = pull_request.target_repo.scm_instance().name
1044 repo_name = pull_request.target_repo.scm_instance().name
1042
1045
1043 response = self.app.post(
1046 response = self.app.post(
1044 route_path('pullrequest_merge',
1047 route_path('pullrequest_merge',
1045 repo_name=repo_name, pull_request_id=pull_request_id),
1048 repo_name=repo_name, pull_request_id=pull_request_id),
1046 params={'csrf_token': csrf_token}).follow()
1049 params={'csrf_token': csrf_token}).follow()
1047
1050
1048 assert response.status_int == 200
1051 assert response.status_int == 200
1049
1052
1050 response.mustcontain(
1053 response.mustcontain(
1051 'Merge is not currently possible because of below failed checks.')
1054 'Merge is not currently possible because of below failed checks.')
1052 response.mustcontain('Pull request reviewer approval is pending.')
1055 response.mustcontain('Pull request reviewer approval is pending.')
1053
1056
1054 def test_merge_pull_request_renders_failure_reason(
1057 def test_merge_pull_request_renders_failure_reason(
1055 self, user_regular, csrf_token, pr_util):
1058 self, user_regular, csrf_token, pr_util):
1056 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1059 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1057 pull_request_id = pull_request.pull_request_id
1060 pull_request_id = pull_request.pull_request_id
1058 repo_name = pull_request.target_repo.scm_instance().name
1061 repo_name = pull_request.target_repo.scm_instance().name
1059
1062
1060 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1063 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1061 MergeFailureReason.PUSH_FAILED,
1064 MergeFailureReason.PUSH_FAILED,
1062 metadata={'target': 'shadow repo',
1065 metadata={'target': 'shadow repo',
1063 'merge_commit': 'xxx'})
1066 'merge_commit': 'xxx'})
1064 model_patcher = mock.patch.multiple(
1067 model_patcher = mock.patch.multiple(
1065 PullRequestModel,
1068 PullRequestModel,
1066 merge_repo=mock.Mock(return_value=merge_resp),
1069 merge_repo=mock.Mock(return_value=merge_resp),
1067 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1070 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1068
1071
1069 with model_patcher:
1072 with model_patcher:
1070 response = self.app.post(
1073 response = self.app.post(
1071 route_path('pullrequest_merge',
1074 route_path('pullrequest_merge',
1072 repo_name=repo_name,
1075 repo_name=repo_name,
1073 pull_request_id=pull_request_id),
1076 pull_request_id=pull_request_id),
1074 params={'csrf_token': csrf_token}, status=302)
1077 params={'csrf_token': csrf_token}, status=302)
1075
1078
1076 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1079 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1077 metadata={'target': 'shadow repo',
1080 metadata={'target': 'shadow repo',
1078 'merge_commit': 'xxx'})
1081 'merge_commit': 'xxx'})
1079 assert_session_flash(response, merge_resp.merge_status_message)
1082 assert_session_flash(response, merge_resp.merge_status_message)
1080
1083
1081 def test_update_source_revision(self, backend, csrf_token):
1084 def test_update_source_revision(self, backend, csrf_token):
1082 commits = [
1085 commits = [
1083 {'message': 'ancestor'},
1086 {'message': 'ancestor'},
1084 {'message': 'change'},
1087 {'message': 'change'},
1085 {'message': 'change-2'},
1088 {'message': 'change-2'},
1086 ]
1089 ]
1087 commit_ids = backend.create_master_repo(commits)
1090 commit_ids = backend.create_master_repo(commits)
1088 target = backend.create_repo(heads=['ancestor'])
1091 target = backend.create_repo(heads=['ancestor'])
1089 source = backend.create_repo(heads=['change'])
1092 source = backend.create_repo(heads=['change'])
1090
1093
1091 # create pr from a in source to A in target
1094 # create pr from a in source to A in target
1092 pull_request = PullRequest()
1095 pull_request = PullRequest()
1093
1096
1094 pull_request.source_repo = source
1097 pull_request.source_repo = source
1095 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1098 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1096 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1099 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1097
1100
1098 pull_request.target_repo = target
1101 pull_request.target_repo = target
1099 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1102 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1100 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1103 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1101
1104
1102 pull_request.revisions = [commit_ids['change']]
1105 pull_request.revisions = [commit_ids['change']]
1103 pull_request.title = u"Test"
1106 pull_request.title = u"Test"
1104 pull_request.description = u"Description"
1107 pull_request.description = u"Description"
1105 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1108 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1106 pull_request.pull_request_state = PullRequest.STATE_CREATED
1109 pull_request.pull_request_state = PullRequest.STATE_CREATED
1107 Session().add(pull_request)
1110 Session().add(pull_request)
1108 Session().commit()
1111 Session().commit()
1109 pull_request_id = pull_request.pull_request_id
1112 pull_request_id = pull_request.pull_request_id
1110
1113
1111 # source has ancestor - change - change-2
1114 # source has ancestor - change - change-2
1112 backend.pull_heads(source, heads=['change-2'])
1115 backend.pull_heads(source, heads=['change-2'])
1113 target_repo_name = target.repo_name
1116 target_repo_name = target.repo_name
1114
1117
1115 # update PR
1118 # update PR
1116 self.app.post(
1119 self.app.post(
1117 route_path('pullrequest_update',
1120 route_path('pullrequest_update',
1118 repo_name=target_repo_name, pull_request_id=pull_request_id),
1121 repo_name=target_repo_name, pull_request_id=pull_request_id),
1119 params={'update_commits': 'true', 'csrf_token': csrf_token})
1122 params={'update_commits': 'true', 'csrf_token': csrf_token})
1120
1123
1121 response = self.app.get(
1124 response = self.app.get(
1122 route_path('pullrequest_show',
1125 route_path('pullrequest_show',
1123 repo_name=target_repo_name,
1126 repo_name=target_repo_name,
1124 pull_request_id=pull_request.pull_request_id))
1127 pull_request_id=pull_request.pull_request_id))
1125
1128
1126 assert response.status_int == 200
1129 assert response.status_int == 200
1127 response.mustcontain('Pull request updated to')
1130 response.mustcontain('Pull request updated to')
1128 response.mustcontain('with 1 added, 0 removed commits.')
1131 response.mustcontain('with 1 added, 0 removed commits.')
1129
1132
1130 # check that we have now both revisions
1133 # check that we have now both revisions
1131 pull_request = PullRequest.get(pull_request_id)
1134 pull_request = PullRequest.get(pull_request_id)
1132 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1135 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1133
1136
1134 def test_update_target_revision(self, backend, csrf_token):
1137 def test_update_target_revision(self, backend, csrf_token):
1135 commits = [
1138 commits = [
1136 {'message': 'ancestor'},
1139 {'message': 'ancestor'},
1137 {'message': 'change'},
1140 {'message': 'change'},
1138 {'message': 'ancestor-new', 'parents': ['ancestor']},
1141 {'message': 'ancestor-new', 'parents': ['ancestor']},
1139 {'message': 'change-rebased'},
1142 {'message': 'change-rebased'},
1140 ]
1143 ]
1141 commit_ids = backend.create_master_repo(commits)
1144 commit_ids = backend.create_master_repo(commits)
1142 target = backend.create_repo(heads=['ancestor'])
1145 target = backend.create_repo(heads=['ancestor'])
1143 source = backend.create_repo(heads=['change'])
1146 source = backend.create_repo(heads=['change'])
1144
1147
1145 # create pr from a in source to A in target
1148 # create pr from a in source to A in target
1146 pull_request = PullRequest()
1149 pull_request = PullRequest()
1147
1150
1148 pull_request.source_repo = source
1151 pull_request.source_repo = source
1149 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1152 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1150 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1153 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1151
1154
1152 pull_request.target_repo = target
1155 pull_request.target_repo = target
1153 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1156 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1154 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1157 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1155
1158
1156 pull_request.revisions = [commit_ids['change']]
1159 pull_request.revisions = [commit_ids['change']]
1157 pull_request.title = u"Test"
1160 pull_request.title = u"Test"
1158 pull_request.description = u"Description"
1161 pull_request.description = u"Description"
1159 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1162 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1160 pull_request.pull_request_state = PullRequest.STATE_CREATED
1163 pull_request.pull_request_state = PullRequest.STATE_CREATED
1161
1164
1162 Session().add(pull_request)
1165 Session().add(pull_request)
1163 Session().commit()
1166 Session().commit()
1164 pull_request_id = pull_request.pull_request_id
1167 pull_request_id = pull_request.pull_request_id
1165
1168
1166 # target has ancestor - ancestor-new
1169 # target has ancestor - ancestor-new
1167 # source has ancestor - ancestor-new - change-rebased
1170 # source has ancestor - ancestor-new - change-rebased
1168 backend.pull_heads(target, heads=['ancestor-new'])
1171 backend.pull_heads(target, heads=['ancestor-new'])
1169 backend.pull_heads(source, heads=['change-rebased'])
1172 backend.pull_heads(source, heads=['change-rebased'])
1170 target_repo_name = target.repo_name
1173 target_repo_name = target.repo_name
1171
1174
1172 # update PR
1175 # update PR
1173 url = route_path('pullrequest_update',
1176 url = route_path('pullrequest_update',
1174 repo_name=target_repo_name,
1177 repo_name=target_repo_name,
1175 pull_request_id=pull_request_id)
1178 pull_request_id=pull_request_id)
1176 self.app.post(url,
1179 self.app.post(url,
1177 params={'update_commits': 'true', 'csrf_token': csrf_token},
1180 params={'update_commits': 'true', 'csrf_token': csrf_token},
1178 status=200)
1181 status=200)
1179
1182
1180 # check that we have now both revisions
1183 # check that we have now both revisions
1181 pull_request = PullRequest.get(pull_request_id)
1184 pull_request = PullRequest.get(pull_request_id)
1182 assert pull_request.revisions == [commit_ids['change-rebased']]
1185 assert pull_request.revisions == [commit_ids['change-rebased']]
1183 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1186 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1184 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1187 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1185
1188
1186 response = self.app.get(
1189 response = self.app.get(
1187 route_path('pullrequest_show',
1190 route_path('pullrequest_show',
1188 repo_name=target_repo_name,
1191 repo_name=target_repo_name,
1189 pull_request_id=pull_request.pull_request_id))
1192 pull_request_id=pull_request.pull_request_id))
1190 assert response.status_int == 200
1193 assert response.status_int == 200
1191 response.mustcontain('Pull request updated to')
1194 response.mustcontain('Pull request updated to')
1192 response.mustcontain('with 1 added, 1 removed commits.')
1195 response.mustcontain('with 1 added, 1 removed commits.')
1193
1196
1194 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1197 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1195 backend = backend_git
1198 backend = backend_git
1196 commits = [
1199 commits = [
1197 {'message': 'master-commit-1'},
1200 {'message': 'master-commit-1'},
1198 {'message': 'master-commit-2-change-1'},
1201 {'message': 'master-commit-2-change-1'},
1199 {'message': 'master-commit-3-change-2'},
1202 {'message': 'master-commit-3-change-2'},
1200
1203
1201 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1204 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1202 {'message': 'feat-commit-2'},
1205 {'message': 'feat-commit-2'},
1203 ]
1206 ]
1204 commit_ids = backend.create_master_repo(commits)
1207 commit_ids = backend.create_master_repo(commits)
1205 target = backend.create_repo(heads=['master-commit-3-change-2'])
1208 target = backend.create_repo(heads=['master-commit-3-change-2'])
1206 source = backend.create_repo(heads=['feat-commit-2'])
1209 source = backend.create_repo(heads=['feat-commit-2'])
1207
1210
1208 # create pr from a in source to A in target
1211 # create pr from a in source to A in target
1209 pull_request = PullRequest()
1212 pull_request = PullRequest()
1210 pull_request.source_repo = source
1213 pull_request.source_repo = source
1211
1214
1212 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1215 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1213 branch=backend.default_branch_name,
1216 branch=backend.default_branch_name,
1214 commit_id=commit_ids['master-commit-3-change-2'])
1217 commit_id=commit_ids['master-commit-3-change-2'])
1215
1218
1216 pull_request.target_repo = target
1219 pull_request.target_repo = target
1217 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1220 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1218 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1221 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1219
1222
1220 pull_request.revisions = [
1223 pull_request.revisions = [
1221 commit_ids['feat-commit-1'],
1224 commit_ids['feat-commit-1'],
1222 commit_ids['feat-commit-2']
1225 commit_ids['feat-commit-2']
1223 ]
1226 ]
1224 pull_request.title = u"Test"
1227 pull_request.title = u"Test"
1225 pull_request.description = u"Description"
1228 pull_request.description = u"Description"
1226 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1229 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1227 pull_request.pull_request_state = PullRequest.STATE_CREATED
1230 pull_request.pull_request_state = PullRequest.STATE_CREATED
1228 Session().add(pull_request)
1231 Session().add(pull_request)
1229 Session().commit()
1232 Session().commit()
1230 pull_request_id = pull_request.pull_request_id
1233 pull_request_id = pull_request.pull_request_id
1231
1234
1232 # PR is created, now we simulate a force-push into target,
1235 # PR is created, now we simulate a force-push into target,
1233 # that drops a 2 last commits
1236 # that drops a 2 last commits
1234 vcsrepo = target.scm_instance()
1237 vcsrepo = target.scm_instance()
1235 vcsrepo.config.clear_section('hooks')
1238 vcsrepo.config.clear_section('hooks')
1236 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1239 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 target_repo_name = target.repo_name
1240 target_repo_name = target.repo_name
1238
1241
1239 # update PR
1242 # update PR
1240 url = route_path('pullrequest_update',
1243 url = route_path('pullrequest_update',
1241 repo_name=target_repo_name,
1244 repo_name=target_repo_name,
1242 pull_request_id=pull_request_id)
1245 pull_request_id=pull_request_id)
1243 self.app.post(url,
1246 self.app.post(url,
1244 params={'update_commits': 'true', 'csrf_token': csrf_token},
1247 params={'update_commits': 'true', 'csrf_token': csrf_token},
1245 status=200)
1248 status=200)
1246
1249
1247 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1250 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1248 assert response.status_int == 200
1251 assert response.status_int == 200
1249 response.mustcontain('Pull request updated to')
1252 response.mustcontain('Pull request updated to')
1250 response.mustcontain('with 0 added, 0 removed commits.')
1253 response.mustcontain('with 0 added, 0 removed commits.')
1251
1254
1252 def test_update_of_ancestor_reference(self, backend, csrf_token):
1255 def test_update_of_ancestor_reference(self, backend, csrf_token):
1253 commits = [
1256 commits = [
1254 {'message': 'ancestor'},
1257 {'message': 'ancestor'},
1255 {'message': 'change'},
1258 {'message': 'change'},
1256 {'message': 'change-2'},
1259 {'message': 'change-2'},
1257 {'message': 'ancestor-new', 'parents': ['ancestor']},
1260 {'message': 'ancestor-new', 'parents': ['ancestor']},
1258 {'message': 'change-rebased'},
1261 {'message': 'change-rebased'},
1259 ]
1262 ]
1260 commit_ids = backend.create_master_repo(commits)
1263 commit_ids = backend.create_master_repo(commits)
1261 target = backend.create_repo(heads=['ancestor'])
1264 target = backend.create_repo(heads=['ancestor'])
1262 source = backend.create_repo(heads=['change'])
1265 source = backend.create_repo(heads=['change'])
1263
1266
1264 # create pr from a in source to A in target
1267 # create pr from a in source to A in target
1265 pull_request = PullRequest()
1268 pull_request = PullRequest()
1266 pull_request.source_repo = source
1269 pull_request.source_repo = source
1267
1270
1268 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1271 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1269 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1272 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1270 pull_request.target_repo = target
1273 pull_request.target_repo = target
1271 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1274 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1272 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1275 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1273 pull_request.revisions = [commit_ids['change']]
1276 pull_request.revisions = [commit_ids['change']]
1274 pull_request.title = u"Test"
1277 pull_request.title = u"Test"
1275 pull_request.description = u"Description"
1278 pull_request.description = u"Description"
1276 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1279 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1277 pull_request.pull_request_state = PullRequest.STATE_CREATED
1280 pull_request.pull_request_state = PullRequest.STATE_CREATED
1278 Session().add(pull_request)
1281 Session().add(pull_request)
1279 Session().commit()
1282 Session().commit()
1280 pull_request_id = pull_request.pull_request_id
1283 pull_request_id = pull_request.pull_request_id
1281
1284
1282 # target has ancestor - ancestor-new
1285 # target has ancestor - ancestor-new
1283 # source has ancestor - ancestor-new - change-rebased
1286 # source has ancestor - ancestor-new - change-rebased
1284 backend.pull_heads(target, heads=['ancestor-new'])
1287 backend.pull_heads(target, heads=['ancestor-new'])
1285 backend.pull_heads(source, heads=['change-rebased'])
1288 backend.pull_heads(source, heads=['change-rebased'])
1286 target_repo_name = target.repo_name
1289 target_repo_name = target.repo_name
1287
1290
1288 # update PR
1291 # update PR
1289 self.app.post(
1292 self.app.post(
1290 route_path('pullrequest_update',
1293 route_path('pullrequest_update',
1291 repo_name=target_repo_name, pull_request_id=pull_request_id),
1294 repo_name=target_repo_name, pull_request_id=pull_request_id),
1292 params={'update_commits': 'true', 'csrf_token': csrf_token},
1295 params={'update_commits': 'true', 'csrf_token': csrf_token},
1293 status=200)
1296 status=200)
1294
1297
1295 # Expect the target reference to be updated correctly
1298 # Expect the target reference to be updated correctly
1296 pull_request = PullRequest.get(pull_request_id)
1299 pull_request = PullRequest.get(pull_request_id)
1297 assert pull_request.revisions == [commit_ids['change-rebased']]
1300 assert pull_request.revisions == [commit_ids['change-rebased']]
1298 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1301 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1299 branch=backend.default_branch_name,
1302 branch=backend.default_branch_name,
1300 commit_id=commit_ids['ancestor-new'])
1303 commit_id=commit_ids['ancestor-new'])
1301 assert pull_request.target_ref == expected_target_ref
1304 assert pull_request.target_ref == expected_target_ref
1302
1305
1303 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1306 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1304 branch_name = 'development'
1307 branch_name = 'development'
1305 commits = [
1308 commits = [
1306 {'message': 'initial-commit'},
1309 {'message': 'initial-commit'},
1307 {'message': 'old-feature'},
1310 {'message': 'old-feature'},
1308 {'message': 'new-feature', 'branch': branch_name},
1311 {'message': 'new-feature', 'branch': branch_name},
1309 ]
1312 ]
1310 repo = backend_git.create_repo(commits)
1313 repo = backend_git.create_repo(commits)
1311 repo_name = repo.repo_name
1314 repo_name = repo.repo_name
1312 commit_ids = backend_git.commit_ids
1315 commit_ids = backend_git.commit_ids
1313
1316
1314 pull_request = PullRequest()
1317 pull_request = PullRequest()
1315 pull_request.source_repo = repo
1318 pull_request.source_repo = repo
1316 pull_request.target_repo = repo
1319 pull_request.target_repo = repo
1317 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1320 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1318 branch=branch_name, commit_id=commit_ids['new-feature'])
1321 branch=branch_name, commit_id=commit_ids['new-feature'])
1319 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1322 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1320 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1323 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1321 pull_request.revisions = [commit_ids['new-feature']]
1324 pull_request.revisions = [commit_ids['new-feature']]
1322 pull_request.title = u"Test"
1325 pull_request.title = u"Test"
1323 pull_request.description = u"Description"
1326 pull_request.description = u"Description"
1324 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1327 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1325 pull_request.pull_request_state = PullRequest.STATE_CREATED
1328 pull_request.pull_request_state = PullRequest.STATE_CREATED
1326 Session().add(pull_request)
1329 Session().add(pull_request)
1327 Session().commit()
1330 Session().commit()
1328
1331
1329 pull_request_id = pull_request.pull_request_id
1332 pull_request_id = pull_request.pull_request_id
1330
1333
1331 vcs = repo.scm_instance()
1334 vcs = repo.scm_instance()
1332 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1335 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1333 # NOTE(marcink): run GC to ensure the commits are gone
1336 # NOTE(marcink): run GC to ensure the commits are gone
1334 vcs.run_gc()
1337 vcs.run_gc()
1335
1338
1336 response = self.app.get(route_path(
1339 response = self.app.get(route_path(
1337 'pullrequest_show',
1340 'pullrequest_show',
1338 repo_name=repo_name,
1341 repo_name=repo_name,
1339 pull_request_id=pull_request_id))
1342 pull_request_id=pull_request_id))
1340
1343
1341 assert response.status_int == 200
1344 assert response.status_int == 200
1342
1345
1343 response.assert_response().element_contains(
1346 response.assert_response().element_contains(
1344 '#changeset_compare_view_content .alert strong',
1347 '#changeset_compare_view_content .alert strong',
1345 'Missing commits')
1348 'Missing commits')
1346 response.assert_response().element_contains(
1349 response.assert_response().element_contains(
1347 '#changeset_compare_view_content .alert',
1350 '#changeset_compare_view_content .alert',
1348 'This pull request cannot be displayed, because one or more'
1351 'This pull request cannot be displayed, because one or more'
1349 ' commits no longer exist in the source repository.')
1352 ' commits no longer exist in the source repository.')
1350
1353
1351 def test_strip_commits_from_pull_request(
1354 def test_strip_commits_from_pull_request(
1352 self, backend, pr_util, csrf_token):
1355 self, backend, pr_util, csrf_token):
1353 commits = [
1356 commits = [
1354 {'message': 'initial-commit'},
1357 {'message': 'initial-commit'},
1355 {'message': 'old-feature'},
1358 {'message': 'old-feature'},
1356 {'message': 'new-feature', 'parents': ['initial-commit']},
1359 {'message': 'new-feature', 'parents': ['initial-commit']},
1357 ]
1360 ]
1358 pull_request = pr_util.create_pull_request(
1361 pull_request = pr_util.create_pull_request(
1359 commits, target_head='initial-commit', source_head='new-feature',
1362 commits, target_head='initial-commit', source_head='new-feature',
1360 revisions=['new-feature'])
1363 revisions=['new-feature'])
1361
1364
1362 vcs = pr_util.source_repository.scm_instance()
1365 vcs = pr_util.source_repository.scm_instance()
1363 if backend.alias == 'git':
1366 if backend.alias == 'git':
1364 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1367 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1365 else:
1368 else:
1366 vcs.strip(pr_util.commit_ids['new-feature'])
1369 vcs.strip(pr_util.commit_ids['new-feature'])
1367
1370
1368 response = self.app.get(route_path(
1371 response = self.app.get(route_path(
1369 'pullrequest_show',
1372 'pullrequest_show',
1370 repo_name=pr_util.target_repository.repo_name,
1373 repo_name=pr_util.target_repository.repo_name,
1371 pull_request_id=pull_request.pull_request_id))
1374 pull_request_id=pull_request.pull_request_id))
1372
1375
1373 assert response.status_int == 200
1376 assert response.status_int == 200
1374
1377
1375 response.assert_response().element_contains(
1378 response.assert_response().element_contains(
1376 '#changeset_compare_view_content .alert strong',
1379 '#changeset_compare_view_content .alert strong',
1377 'Missing commits')
1380 'Missing commits')
1378 response.assert_response().element_contains(
1381 response.assert_response().element_contains(
1379 '#changeset_compare_view_content .alert',
1382 '#changeset_compare_view_content .alert',
1380 'This pull request cannot be displayed, because one or more'
1383 'This pull request cannot be displayed, because one or more'
1381 ' commits no longer exist in the source repository.')
1384 ' commits no longer exist in the source repository.')
1382 response.assert_response().element_contains(
1385 response.assert_response().element_contains(
1383 '#update_commits',
1386 '#update_commits',
1384 'Update commits')
1387 'Update commits')
1385
1388
1386 def test_strip_commits_and_update(
1389 def test_strip_commits_and_update(
1387 self, backend, pr_util, csrf_token):
1390 self, backend, pr_util, csrf_token):
1388 commits = [
1391 commits = [
1389 {'message': 'initial-commit'},
1392 {'message': 'initial-commit'},
1390 {'message': 'old-feature'},
1393 {'message': 'old-feature'},
1391 {'message': 'new-feature', 'parents': ['old-feature']},
1394 {'message': 'new-feature', 'parents': ['old-feature']},
1392 ]
1395 ]
1393 pull_request = pr_util.create_pull_request(
1396 pull_request = pr_util.create_pull_request(
1394 commits, target_head='old-feature', source_head='new-feature',
1397 commits, target_head='old-feature', source_head='new-feature',
1395 revisions=['new-feature'], mergeable=True)
1398 revisions=['new-feature'], mergeable=True)
1396 pr_id = pull_request.pull_request_id
1399 pr_id = pull_request.pull_request_id
1397 target_repo_name = pull_request.target_repo.repo_name
1400 target_repo_name = pull_request.target_repo.repo_name
1398
1401
1399 vcs = pr_util.source_repository.scm_instance()
1402 vcs = pr_util.source_repository.scm_instance()
1400 if backend.alias == 'git':
1403 if backend.alias == 'git':
1401 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1404 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1402 else:
1405 else:
1403 vcs.strip(pr_util.commit_ids['new-feature'])
1406 vcs.strip(pr_util.commit_ids['new-feature'])
1404
1407
1405 url = route_path('pullrequest_update',
1408 url = route_path('pullrequest_update',
1406 repo_name=target_repo_name,
1409 repo_name=target_repo_name,
1407 pull_request_id=pr_id)
1410 pull_request_id=pr_id)
1408 response = self.app.post(url,
1411 response = self.app.post(url,
1409 params={'update_commits': 'true',
1412 params={'update_commits': 'true',
1410 'csrf_token': csrf_token})
1413 'csrf_token': csrf_token})
1411
1414
1412 assert response.status_int == 200
1415 assert response.status_int == 200
1413 assert response.body == '{"response": true, "redirect_url": null}'
1416 assert response.body == '{"response": true, "redirect_url": null}'
1414
1417
1415 # Make sure that after update, it won't raise 500 errors
1418 # Make sure that after update, it won't raise 500 errors
1416 response = self.app.get(route_path(
1419 response = self.app.get(route_path(
1417 'pullrequest_show',
1420 'pullrequest_show',
1418 repo_name=target_repo_name,
1421 repo_name=target_repo_name,
1419 pull_request_id=pr_id))
1422 pull_request_id=pr_id))
1420
1423
1421 assert response.status_int == 200
1424 assert response.status_int == 200
1422 response.assert_response().element_contains(
1425 response.assert_response().element_contains(
1423 '#changeset_compare_view_content .alert strong',
1426 '#changeset_compare_view_content .alert strong',
1424 'Missing commits')
1427 'Missing commits')
1425
1428
1426 def test_branch_is_a_link(self, pr_util):
1429 def test_branch_is_a_link(self, pr_util):
1427 pull_request = pr_util.create_pull_request()
1430 pull_request = pr_util.create_pull_request()
1428 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1431 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1429 pull_request.target_ref = 'branch:target:abcdef1234567890'
1432 pull_request.target_ref = 'branch:target:abcdef1234567890'
1430 Session().add(pull_request)
1433 Session().add(pull_request)
1431 Session().commit()
1434 Session().commit()
1432
1435
1433 response = self.app.get(route_path(
1436 response = self.app.get(route_path(
1434 'pullrequest_show',
1437 'pullrequest_show',
1435 repo_name=pull_request.target_repo.scm_instance().name,
1438 repo_name=pull_request.target_repo.scm_instance().name,
1436 pull_request_id=pull_request.pull_request_id))
1439 pull_request_id=pull_request.pull_request_id))
1437 assert response.status_int == 200
1440 assert response.status_int == 200
1438
1441
1439 source = response.assert_response().get_element('.pr-source-info')
1442 source = response.assert_response().get_element('.pr-source-info')
1440 source_parent = source.getparent()
1443 source_parent = source.getparent()
1441 assert len(source_parent) == 1
1444 assert len(source_parent) == 1
1442
1445
1443 target = response.assert_response().get_element('.pr-target-info')
1446 target = response.assert_response().get_element('.pr-target-info')
1444 target_parent = target.getparent()
1447 target_parent = target.getparent()
1445 assert len(target_parent) == 1
1448 assert len(target_parent) == 1
1446
1449
1447 expected_origin_link = route_path(
1450 expected_origin_link = route_path(
1448 'repo_commits',
1451 'repo_commits',
1449 repo_name=pull_request.source_repo.scm_instance().name,
1452 repo_name=pull_request.source_repo.scm_instance().name,
1450 params=dict(branch='origin'))
1453 params=dict(branch='origin'))
1451 expected_target_link = route_path(
1454 expected_target_link = route_path(
1452 'repo_commits',
1455 'repo_commits',
1453 repo_name=pull_request.target_repo.scm_instance().name,
1456 repo_name=pull_request.target_repo.scm_instance().name,
1454 params=dict(branch='target'))
1457 params=dict(branch='target'))
1455 assert source_parent.attrib['href'] == expected_origin_link
1458 assert source_parent.attrib['href'] == expected_origin_link
1456 assert target_parent.attrib['href'] == expected_target_link
1459 assert target_parent.attrib['href'] == expected_target_link
1457
1460
1458 def test_bookmark_is_not_a_link(self, pr_util):
1461 def test_bookmark_is_not_a_link(self, pr_util):
1459 pull_request = pr_util.create_pull_request()
1462 pull_request = pr_util.create_pull_request()
1460 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1463 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1461 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1464 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1462 Session().add(pull_request)
1465 Session().add(pull_request)
1463 Session().commit()
1466 Session().commit()
1464
1467
1465 response = self.app.get(route_path(
1468 response = self.app.get(route_path(
1466 'pullrequest_show',
1469 'pullrequest_show',
1467 repo_name=pull_request.target_repo.scm_instance().name,
1470 repo_name=pull_request.target_repo.scm_instance().name,
1468 pull_request_id=pull_request.pull_request_id))
1471 pull_request_id=pull_request.pull_request_id))
1469 assert response.status_int == 200
1472 assert response.status_int == 200
1470
1473
1471 source = response.assert_response().get_element('.pr-source-info')
1474 source = response.assert_response().get_element('.pr-source-info')
1472 assert source.text.strip() == 'bookmark:origin'
1475 assert source.text.strip() == 'bookmark:origin'
1473 assert source.getparent().attrib.get('href') is None
1476 assert source.getparent().attrib.get('href') is None
1474
1477
1475 target = response.assert_response().get_element('.pr-target-info')
1478 target = response.assert_response().get_element('.pr-target-info')
1476 assert target.text.strip() == 'bookmark:target'
1479 assert target.text.strip() == 'bookmark:target'
1477 assert target.getparent().attrib.get('href') is None
1480 assert target.getparent().attrib.get('href') is None
1478
1481
1479 def test_tag_is_not_a_link(self, pr_util):
1482 def test_tag_is_not_a_link(self, pr_util):
1480 pull_request = pr_util.create_pull_request()
1483 pull_request = pr_util.create_pull_request()
1481 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1484 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1482 pull_request.target_ref = 'tag:target:abcdef1234567890'
1485 pull_request.target_ref = 'tag:target:abcdef1234567890'
1483 Session().add(pull_request)
1486 Session().add(pull_request)
1484 Session().commit()
1487 Session().commit()
1485
1488
1486 response = self.app.get(route_path(
1489 response = self.app.get(route_path(
1487 'pullrequest_show',
1490 'pullrequest_show',
1488 repo_name=pull_request.target_repo.scm_instance().name,
1491 repo_name=pull_request.target_repo.scm_instance().name,
1489 pull_request_id=pull_request.pull_request_id))
1492 pull_request_id=pull_request.pull_request_id))
1490 assert response.status_int == 200
1493 assert response.status_int == 200
1491
1494
1492 source = response.assert_response().get_element('.pr-source-info')
1495 source = response.assert_response().get_element('.pr-source-info')
1493 assert source.text.strip() == 'tag:origin'
1496 assert source.text.strip() == 'tag:origin'
1494 assert source.getparent().attrib.get('href') is None
1497 assert source.getparent().attrib.get('href') is None
1495
1498
1496 target = response.assert_response().get_element('.pr-target-info')
1499 target = response.assert_response().get_element('.pr-target-info')
1497 assert target.text.strip() == 'tag:target'
1500 assert target.text.strip() == 'tag:target'
1498 assert target.getparent().attrib.get('href') is None
1501 assert target.getparent().attrib.get('href') is None
1499
1502
1500 @pytest.mark.parametrize('mergeable', [True, False])
1503 @pytest.mark.parametrize('mergeable', [True, False])
1501 def test_shadow_repository_link(
1504 def test_shadow_repository_link(
1502 self, mergeable, pr_util, http_host_only_stub):
1505 self, mergeable, pr_util, http_host_only_stub):
1503 """
1506 """
1504 Check that the pull request summary page displays a link to the shadow
1507 Check that the pull request summary page displays a link to the shadow
1505 repository if the pull request is mergeable. If it is not mergeable
1508 repository if the pull request is mergeable. If it is not mergeable
1506 the link should not be displayed.
1509 the link should not be displayed.
1507 """
1510 """
1508 pull_request = pr_util.create_pull_request(
1511 pull_request = pr_util.create_pull_request(
1509 mergeable=mergeable, enable_notifications=False)
1512 mergeable=mergeable, enable_notifications=False)
1510 target_repo = pull_request.target_repo.scm_instance()
1513 target_repo = pull_request.target_repo.scm_instance()
1511 pr_id = pull_request.pull_request_id
1514 pr_id = pull_request.pull_request_id
1512 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1515 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1513 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1516 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1514
1517
1515 response = self.app.get(route_path(
1518 response = self.app.get(route_path(
1516 'pullrequest_show',
1519 'pullrequest_show',
1517 repo_name=target_repo.name,
1520 repo_name=target_repo.name,
1518 pull_request_id=pr_id))
1521 pull_request_id=pr_id))
1519
1522
1520 if mergeable:
1523 if mergeable:
1521 response.assert_response().element_value_contains(
1524 response.assert_response().element_value_contains(
1522 'input.pr-mergeinfo', shadow_url)
1525 'input.pr-mergeinfo', shadow_url)
1523 response.assert_response().element_value_contains(
1526 response.assert_response().element_value_contains(
1524 'input.pr-mergeinfo ', 'pr-merge')
1527 'input.pr-mergeinfo ', 'pr-merge')
1525 else:
1528 else:
1526 response.assert_response().no_element_exists('.pr-mergeinfo')
1529 response.assert_response().no_element_exists('.pr-mergeinfo')
1527
1530
1528
1531
1529 @pytest.mark.usefixtures('app')
1532 @pytest.mark.usefixtures('app')
1530 @pytest.mark.backends("git", "hg")
1533 @pytest.mark.backends("git", "hg")
1531 class TestPullrequestsControllerDelete(object):
1534 class TestPullrequestsControllerDelete(object):
1532 def test_pull_request_delete_button_permissions_admin(
1535 def test_pull_request_delete_button_permissions_admin(
1533 self, autologin_user, user_admin, pr_util):
1536 self, autologin_user, user_admin, pr_util):
1534 pull_request = pr_util.create_pull_request(
1537 pull_request = pr_util.create_pull_request(
1535 author=user_admin.username, enable_notifications=False)
1538 author=user_admin.username, enable_notifications=False)
1536
1539
1537 response = self.app.get(route_path(
1540 response = self.app.get(route_path(
1538 'pullrequest_show',
1541 'pullrequest_show',
1539 repo_name=pull_request.target_repo.scm_instance().name,
1542 repo_name=pull_request.target_repo.scm_instance().name,
1540 pull_request_id=pull_request.pull_request_id))
1543 pull_request_id=pull_request.pull_request_id))
1541
1544
1542 response.mustcontain('id="delete_pullrequest"')
1545 response.mustcontain('id="delete_pullrequest"')
1543 response.mustcontain('Confirm to delete this pull request')
1546 response.mustcontain('Confirm to delete this pull request')
1544
1547
1545 def test_pull_request_delete_button_permissions_owner(
1548 def test_pull_request_delete_button_permissions_owner(
1546 self, autologin_regular_user, user_regular, pr_util):
1549 self, autologin_regular_user, user_regular, pr_util):
1547 pull_request = pr_util.create_pull_request(
1550 pull_request = pr_util.create_pull_request(
1548 author=user_regular.username, enable_notifications=False)
1551 author=user_regular.username, enable_notifications=False)
1549
1552
1550 response = self.app.get(route_path(
1553 response = self.app.get(route_path(
1551 'pullrequest_show',
1554 'pullrequest_show',
1552 repo_name=pull_request.target_repo.scm_instance().name,
1555 repo_name=pull_request.target_repo.scm_instance().name,
1553 pull_request_id=pull_request.pull_request_id))
1556 pull_request_id=pull_request.pull_request_id))
1554
1557
1555 response.mustcontain('id="delete_pullrequest"')
1558 response.mustcontain('id="delete_pullrequest"')
1556 response.mustcontain('Confirm to delete this pull request')
1559 response.mustcontain('Confirm to delete this pull request')
1557
1560
1558 def test_pull_request_delete_button_permissions_forbidden(
1561 def test_pull_request_delete_button_permissions_forbidden(
1559 self, autologin_regular_user, user_regular, user_admin, pr_util):
1562 self, autologin_regular_user, user_regular, user_admin, pr_util):
1560 pull_request = pr_util.create_pull_request(
1563 pull_request = pr_util.create_pull_request(
1561 author=user_admin.username, enable_notifications=False)
1564 author=user_admin.username, enable_notifications=False)
1562
1565
1563 response = self.app.get(route_path(
1566 response = self.app.get(route_path(
1564 'pullrequest_show',
1567 'pullrequest_show',
1565 repo_name=pull_request.target_repo.scm_instance().name,
1568 repo_name=pull_request.target_repo.scm_instance().name,
1566 pull_request_id=pull_request.pull_request_id))
1569 pull_request_id=pull_request.pull_request_id))
1567 response.mustcontain(no=['id="delete_pullrequest"'])
1570 response.mustcontain(no=['id="delete_pullrequest"'])
1568 response.mustcontain(no=['Confirm to delete this pull request'])
1571 response.mustcontain(no=['Confirm to delete this pull request'])
1569
1572
1570 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1573 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1571 self, autologin_regular_user, user_regular, user_admin, pr_util,
1574 self, autologin_regular_user, user_regular, user_admin, pr_util,
1572 user_util):
1575 user_util):
1573
1576
1574 pull_request = pr_util.create_pull_request(
1577 pull_request = pr_util.create_pull_request(
1575 author=user_admin.username, enable_notifications=False)
1578 author=user_admin.username, enable_notifications=False)
1576
1579
1577 user_util.grant_user_permission_to_repo(
1580 user_util.grant_user_permission_to_repo(
1578 pull_request.target_repo, user_regular,
1581 pull_request.target_repo, user_regular,
1579 'repository.write')
1582 'repository.write')
1580
1583
1581 response = self.app.get(route_path(
1584 response = self.app.get(route_path(
1582 'pullrequest_show',
1585 'pullrequest_show',
1583 repo_name=pull_request.target_repo.scm_instance().name,
1586 repo_name=pull_request.target_repo.scm_instance().name,
1584 pull_request_id=pull_request.pull_request_id))
1587 pull_request_id=pull_request.pull_request_id))
1585
1588
1586 response.mustcontain('id="open_edit_pullrequest"')
1589 response.mustcontain('id="open_edit_pullrequest"')
1587 response.mustcontain('id="delete_pullrequest"')
1590 response.mustcontain('id="delete_pullrequest"')
1588 response.mustcontain(no=['Confirm to delete this pull request'])
1591 response.mustcontain(no=['Confirm to delete this pull request'])
1589
1592
1590 def test_delete_comment_returns_404_if_comment_does_not_exist(
1593 def test_delete_comment_returns_404_if_comment_does_not_exist(
1591 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1594 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1592
1595
1593 pull_request = pr_util.create_pull_request(
1596 pull_request = pr_util.create_pull_request(
1594 author=user_admin.username, enable_notifications=False)
1597 author=user_admin.username, enable_notifications=False)
1595
1598
1596 self.app.post(
1599 self.app.post(
1597 route_path(
1600 route_path(
1598 'pullrequest_comment_delete',
1601 'pullrequest_comment_delete',
1599 repo_name=pull_request.target_repo.scm_instance().name,
1602 repo_name=pull_request.target_repo.scm_instance().name,
1600 pull_request_id=pull_request.pull_request_id,
1603 pull_request_id=pull_request.pull_request_id,
1601 comment_id=1024404),
1604 comment_id=1024404),
1602 extra_environ=xhr_header,
1605 extra_environ=xhr_header,
1603 params={'csrf_token': csrf_token},
1606 params={'csrf_token': csrf_token},
1604 status=404
1607 status=404
1605 )
1608 )
1606
1609
1607 def test_delete_comment(
1610 def test_delete_comment(
1608 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1611 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1609
1612
1610 pull_request = pr_util.create_pull_request(
1613 pull_request = pr_util.create_pull_request(
1611 author=user_admin.username, enable_notifications=False)
1614 author=user_admin.username, enable_notifications=False)
1612 comment = pr_util.create_comment()
1615 comment = pr_util.create_comment()
1613 comment_id = comment.comment_id
1616 comment_id = comment.comment_id
1614
1617
1615 response = self.app.post(
1618 response = self.app.post(
1616 route_path(
1619 route_path(
1617 'pullrequest_comment_delete',
1620 'pullrequest_comment_delete',
1618 repo_name=pull_request.target_repo.scm_instance().name,
1621 repo_name=pull_request.target_repo.scm_instance().name,
1619 pull_request_id=pull_request.pull_request_id,
1622 pull_request_id=pull_request.pull_request_id,
1620 comment_id=comment_id),
1623 comment_id=comment_id),
1621 extra_environ=xhr_header,
1624 extra_environ=xhr_header,
1622 params={'csrf_token': csrf_token},
1625 params={'csrf_token': csrf_token},
1623 status=200
1626 status=200
1624 )
1627 )
1625 assert response.body == 'true'
1628 assert response.body == 'true'
1626
1629
1627 @pytest.mark.parametrize('url_type', [
1630 @pytest.mark.parametrize('url_type', [
1628 'pullrequest_new',
1631 'pullrequest_new',
1629 'pullrequest_create',
1632 'pullrequest_create',
1630 'pullrequest_update',
1633 'pullrequest_update',
1631 'pullrequest_merge',
1634 'pullrequest_merge',
1632 ])
1635 ])
1633 def test_pull_request_is_forbidden_on_archived_repo(
1636 def test_pull_request_is_forbidden_on_archived_repo(
1634 self, autologin_user, backend, xhr_header, user_util, url_type):
1637 self, autologin_user, backend, xhr_header, user_util, url_type):
1635
1638
1636 # create a temporary repo
1639 # create a temporary repo
1637 source = user_util.create_repo(repo_type=backend.alias)
1640 source = user_util.create_repo(repo_type=backend.alias)
1638 repo_name = source.repo_name
1641 repo_name = source.repo_name
1639 repo = Repository.get_by_repo_name(repo_name)
1642 repo = Repository.get_by_repo_name(repo_name)
1640 repo.archived = True
1643 repo.archived = True
1641 Session().commit()
1644 Session().commit()
1642
1645
1643 response = self.app.get(
1646 response = self.app.get(
1644 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1647 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1645
1648
1646 msg = 'Action not supported for archived repository.'
1649 msg = 'Action not supported for archived repository.'
1647 assert_session_flash(response, msg)
1650 assert_session_flash(response, msg)
1648
1651
1649
1652
1650 def assert_pull_request_status(pull_request, expected_status):
1653 def assert_pull_request_status(pull_request, expected_status):
1651 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1654 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1652 assert status == expected_status
1655 assert status == expected_status
1653
1656
1654
1657
1655 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1658 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1656 @pytest.mark.usefixtures("autologin_user")
1659 @pytest.mark.usefixtures("autologin_user")
1657 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1660 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1658 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1661 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,791 +1,791 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks, channelstream
34 from rhodecode.lib import diffs, codeblocks, channelstream
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
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.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 single_commit = len(c.commit_ranges) == 1
119
119
120 c.changes = OrderedDict()
120 c.changes = OrderedDict()
121 c.lines_added = 0
121 c.lines_added = 0
122 c.lines_deleted = 0
122 c.lines_deleted = 0
123
123
124 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127
127
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
129 c.inline_comments = []
129 c.inline_comments = []
130 c.files = []
130 c.files = []
131
131
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135
135
136 # Single commit
136 # Single commit
137 if single_commit:
137 if single_commit:
138 commit = c.commit_ranges[0]
138 commit = c.commit_ranges[0]
139 c.comments = CommentsModel().get_comments(
139 c.comments = CommentsModel().get_comments(
140 self.db_repo.repo_id,
140 self.db_repo.repo_id,
141 revision=commit.raw_id)
141 revision=commit.raw_id)
142
142
143 # comments from PR
143 # comments from PR
144 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
145 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
146 with_revisions=True)
146 with_revisions=True)
147
147
148 prs = set()
148 prs = set()
149 reviewers = list()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
151 for c_status in statuses:
152
152
153 # extract associated pull-requests from votes
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
155 prs.add(c_status.pull_request)
156
156
157 # extract reviewers
157 # extract reviewers
158 _user_id = c_status.author.user_id
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
160 reviewers.append(
161 StrictAttributeDict({
161 StrictAttributeDict({
162 'user': c_status.author,
162 'user': c_status.author,
163
163
164 # fake attributed for commit, page that we don't have
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
165 # but we share the display with PR page
166 'mandatory': False,
166 'mandatory': False,
167 'reasons': [],
167 'reasons': [],
168 'rule_user_group_data': lambda: None
168 'rule_user_group_data': lambda: None
169 })
169 })
170 )
170 )
171 reviewers_duplicates.add(_user_id)
171 reviewers_duplicates.add(_user_id)
172
172
173 c.reviewers_count = len(reviewers)
173 c.reviewers_count = len(reviewers)
174 c.observers_count = 0
174 c.observers_count = 0
175
175
176 # from associated statuses, check the pull requests, and
176 # from associated statuses, check the pull requests, and
177 # show comments from them
177 # show comments from them
178 for pr in prs:
178 for pr in prs:
179 c.comments.extend(pr.comments)
179 c.comments.extend(pr.comments)
180
180
181 c.unresolved_comments = CommentsModel()\
181 c.unresolved_comments = CommentsModel()\
182 .get_commit_unresolved_todos(commit.raw_id)
182 .get_commit_unresolved_todos(commit.raw_id)
183 c.resolved_comments = CommentsModel()\
183 c.resolved_comments = CommentsModel()\
184 .get_commit_resolved_todos(commit.raw_id)
184 .get_commit_resolved_todos(commit.raw_id)
185
185
186 c.inline_comments_flat = CommentsModel()\
186 c.inline_comments_flat = CommentsModel()\
187 .get_commit_inline_comments(commit.raw_id)
187 .get_commit_inline_comments(commit.raw_id)
188
188
189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
190 statuses, reviewers)
190 statuses, reviewers)
191
191
192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
193
193
194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
195
195
196 for review_obj, member, reasons, mandatory, status in review_statuses:
196 for review_obj, member, reasons, mandatory, status in review_statuses:
197 member_reviewer = h.reviewer_as_json(
197 member_reviewer = h.reviewer_as_json(
198 member, reasons=reasons, mandatory=mandatory, role=None,
198 member, reasons=reasons, mandatory=mandatory, role=None,
199 user_group=None
199 user_group=None
200 )
200 )
201
201
202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
203 member_reviewer['review_status'] = current_review_status
203 member_reviewer['review_status'] = current_review_status
204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
205 member_reviewer['allowed_to_update'] = False
205 member_reviewer['allowed_to_update'] = False
206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
207
207
208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
209
209
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
213
213
214 diff = None
214 diff = None
215 # Iterate over ranges (default commit view is always one commit)
215 # Iterate over ranges (default commit view is always one commit)
216 for commit in c.commit_ranges:
216 for commit in c.commit_ranges:
217 c.changes[commit.raw_id] = []
217 c.changes[commit.raw_id] = []
218
218
219 commit2 = commit
219 commit2 = commit
220 commit1 = commit.first_parent
220 commit1 = commit.first_parent
221
221
222 if method == 'show':
222 if method == 'show':
223 inline_comments = CommentsModel().get_inline_comments(
223 inline_comments = CommentsModel().get_inline_comments(
224 self.db_repo.repo_id, revision=commit.raw_id)
224 self.db_repo.repo_id, revision=commit.raw_id)
225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
226 inline_comments))
226 inline_comments))
227 c.inline_comments = inline_comments
227 c.inline_comments = inline_comments
228
228
229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 self.db_repo)
230 self.db_repo)
231 cache_file_path = diff_cache_exist(
231 cache_file_path = diff_cache_exist(
232 cache_path, 'diff', commit.raw_id,
232 cache_path, 'diff', commit.raw_id,
233 hide_whitespace_changes, diff_context, c.fulldiff)
233 hide_whitespace_changes, diff_context, c.fulldiff)
234
234
235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
236 force_recache = str2bool(self.request.GET.get('force_recache'))
236 force_recache = str2bool(self.request.GET.get('force_recache'))
237
237
238 cached_diff = None
238 cached_diff = None
239 if caching_enabled:
239 if caching_enabled:
240 cached_diff = load_cached_diff(cache_file_path)
240 cached_diff = load_cached_diff(cache_file_path)
241
241
242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
243 if not force_recache and has_proper_diff_cache:
243 if not force_recache and has_proper_diff_cache:
244 diffset = cached_diff['diff']
244 diffset = cached_diff['diff']
245 else:
245 else:
246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
247 commit1, commit2,
247 commit1, commit2,
248 ignore_whitespace=hide_whitespace_changes,
248 ignore_whitespace=hide_whitespace_changes,
249 context=diff_context)
249 context=diff_context)
250
250
251 diff_processor = diffs.DiffProcessor(
251 diff_processor = diffs.DiffProcessor(
252 vcs_diff, format='newdiff', diff_limit=diff_limit,
252 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 file_limit=file_limit, show_full_diff=c.fulldiff)
253 file_limit=file_limit, show_full_diff=c.fulldiff)
254
254
255 _parsed = diff_processor.prepare()
255 _parsed = diff_processor.prepare()
256
256
257 diffset = codeblocks.DiffSet(
257 diffset = codeblocks.DiffSet(
258 repo_name=self.db_repo_name,
258 repo_name=self.db_repo_name,
259 source_node_getter=codeblocks.diffset_node_getter(commit1),
259 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 target_node_getter=codeblocks.diffset_node_getter(commit2))
260 target_node_getter=codeblocks.diffset_node_getter(commit2))
261
261
262 diffset = self.path_filter.render_patchset_filtered(
262 diffset = self.path_filter.render_patchset_filtered(
263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264
264
265 # save cached diff
265 # save cached diff
266 if caching_enabled:
266 if caching_enabled:
267 cache_diff(cache_file_path, diffset, None)
267 cache_diff(cache_file_path, diffset, None)
268
268
269 c.limited_diff = diffset.limited_diff
269 c.limited_diff = diffset.limited_diff
270 c.changes[commit.raw_id] = diffset
270 c.changes[commit.raw_id] = diffset
271 else:
271 else:
272 # TODO(marcink): no cache usage here...
272 # TODO(marcink): no cache usage here...
273 _diff = self.rhodecode_vcs_repo.get_diff(
273 _diff = self.rhodecode_vcs_repo.get_diff(
274 commit1, commit2,
274 commit1, commit2,
275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
276 diff_processor = diffs.DiffProcessor(
276 diff_processor = diffs.DiffProcessor(
277 _diff, format='newdiff', diff_limit=diff_limit,
277 _diff, format='newdiff', diff_limit=diff_limit,
278 file_limit=file_limit, show_full_diff=c.fulldiff)
278 file_limit=file_limit, show_full_diff=c.fulldiff)
279 # downloads/raw we only need RAW diff nothing else
279 # downloads/raw we only need RAW diff nothing else
280 diff = self.path_filter.get_raw_patch(diff_processor)
280 diff = self.path_filter.get_raw_patch(diff_processor)
281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282
282
283 # sort comments by how they were generated
283 # sort comments by how they were generated
284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.at_version_num = None
285 c.at_version_num = None
286
286
287 if len(c.commit_ranges) == 1:
287 if len(c.commit_ranges) == 1:
288 c.commit = c.commit_ranges[0]
288 c.commit = c.commit_ranges[0]
289 c.parent_tmpl = ''.join(
289 c.parent_tmpl = ''.join(
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291
291
292 if method == 'download':
292 if method == 'download':
293 response = Response(diff)
293 response = Response(diff)
294 response.content_type = 'text/plain'
294 response.content_type = 'text/plain'
295 response.content_disposition = (
295 response.content_disposition = (
296 'attachment; filename=%s.diff' % commit_id_range[:12])
296 'attachment; filename=%s.diff' % commit_id_range[:12])
297 return response
297 return response
298 elif method == 'patch':
298 elif method == 'patch':
299 c.diff = safe_unicode(diff)
299 c.diff = safe_unicode(diff)
300 patch = render(
300 patch = render(
301 'rhodecode:templates/changeset/patch_changeset.mako',
301 'rhodecode:templates/changeset/patch_changeset.mako',
302 self._get_template_context(c), self.request)
302 self._get_template_context(c), self.request)
303 response = Response(patch)
303 response = Response(patch)
304 response.content_type = 'text/plain'
304 response.content_type = 'text/plain'
305 return response
305 return response
306 elif method == 'raw':
306 elif method == 'raw':
307 response = Response(diff)
307 response = Response(diff)
308 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
309 return response
309 return response
310 elif method == 'show':
310 elif method == 'show':
311 if len(c.commit_ranges) == 1:
311 if len(c.commit_ranges) == 1:
312 html = render(
312 html = render(
313 'rhodecode:templates/changeset/changeset.mako',
313 'rhodecode:templates/changeset/changeset.mako',
314 self._get_template_context(c), self.request)
314 self._get_template_context(c), self.request)
315 return Response(html)
315 return Response(html)
316 else:
316 else:
317 c.ancestor = None
317 c.ancestor = None
318 c.target_repo = self.db_repo
318 c.target_repo = self.db_repo
319 html = render(
319 html = render(
320 'rhodecode:templates/changeset/changeset_range.mako',
320 'rhodecode:templates/changeset/changeset_range.mako',
321 self._get_template_context(c), self.request)
321 self._get_template_context(c), self.request)
322 return Response(html)
322 return Response(html)
323
323
324 raise HTTPBadRequest()
324 raise HTTPBadRequest()
325
325
326 @LoginRequired()
326 @LoginRequired()
327 @HasRepoPermissionAnyDecorator(
327 @HasRepoPermissionAnyDecorator(
328 'repository.read', 'repository.write', 'repository.admin')
328 'repository.read', 'repository.write', 'repository.admin')
329 @view_config(
329 @view_config(
330 route_name='repo_commit', request_method='GET',
330 route_name='repo_commit', request_method='GET',
331 renderer=None)
331 renderer=None)
332 def repo_commit_show(self):
332 def repo_commit_show(self):
333 commit_id = self.request.matchdict['commit_id']
333 commit_id = self.request.matchdict['commit_id']
334 return self._commit(commit_id, method='show')
334 return self._commit(commit_id, method='show')
335
335
336 @LoginRequired()
336 @LoginRequired()
337 @HasRepoPermissionAnyDecorator(
337 @HasRepoPermissionAnyDecorator(
338 'repository.read', 'repository.write', 'repository.admin')
338 'repository.read', 'repository.write', 'repository.admin')
339 @view_config(
339 @view_config(
340 route_name='repo_commit_raw', request_method='GET',
340 route_name='repo_commit_raw', request_method='GET',
341 renderer=None)
341 renderer=None)
342 @view_config(
342 @view_config(
343 route_name='repo_commit_raw_deprecated', request_method='GET',
343 route_name='repo_commit_raw_deprecated', request_method='GET',
344 renderer=None)
344 renderer=None)
345 def repo_commit_raw(self):
345 def repo_commit_raw(self):
346 commit_id = self.request.matchdict['commit_id']
346 commit_id = self.request.matchdict['commit_id']
347 return self._commit(commit_id, method='raw')
347 return self._commit(commit_id, method='raw')
348
348
349 @LoginRequired()
349 @LoginRequired()
350 @HasRepoPermissionAnyDecorator(
350 @HasRepoPermissionAnyDecorator(
351 'repository.read', 'repository.write', 'repository.admin')
351 'repository.read', 'repository.write', 'repository.admin')
352 @view_config(
352 @view_config(
353 route_name='repo_commit_patch', request_method='GET',
353 route_name='repo_commit_patch', request_method='GET',
354 renderer=None)
354 renderer=None)
355 def repo_commit_patch(self):
355 def repo_commit_patch(self):
356 commit_id = self.request.matchdict['commit_id']
356 commit_id = self.request.matchdict['commit_id']
357 return self._commit(commit_id, method='patch')
357 return self._commit(commit_id, method='patch')
358
358
359 @LoginRequired()
359 @LoginRequired()
360 @HasRepoPermissionAnyDecorator(
360 @HasRepoPermissionAnyDecorator(
361 'repository.read', 'repository.write', 'repository.admin')
361 'repository.read', 'repository.write', 'repository.admin')
362 @view_config(
362 @view_config(
363 route_name='repo_commit_download', request_method='GET',
363 route_name='repo_commit_download', request_method='GET',
364 renderer=None)
364 renderer=None)
365 def repo_commit_download(self):
365 def repo_commit_download(self):
366 commit_id = self.request.matchdict['commit_id']
366 commit_id = self.request.matchdict['commit_id']
367 return self._commit(commit_id, method='download')
367 return self._commit(commit_id, method='download')
368
368
369 @LoginRequired()
369 @LoginRequired()
370 @NotAnonymous()
370 @NotAnonymous()
371 @HasRepoPermissionAnyDecorator(
371 @HasRepoPermissionAnyDecorator(
372 'repository.read', 'repository.write', 'repository.admin')
372 'repository.read', 'repository.write', 'repository.admin')
373 @CSRFRequired()
373 @CSRFRequired()
374 @view_config(
374 @view_config(
375 route_name='repo_commit_comment_create', request_method='POST',
375 route_name='repo_commit_comment_create', request_method='POST',
376 renderer='json_ext')
376 renderer='json_ext')
377 def repo_commit_comment_create(self):
377 def repo_commit_comment_create(self):
378 _ = self.request.translate
378 _ = self.request.translate
379 commit_id = self.request.matchdict['commit_id']
379 commit_id = self.request.matchdict['commit_id']
380
380
381 c = self.load_default_context()
381 c = self.load_default_context()
382 status = self.request.POST.get('changeset_status', None)
382 status = self.request.POST.get('changeset_status', None)
383 text = self.request.POST.get('text')
383 text = self.request.POST.get('text')
384 comment_type = self.request.POST.get('comment_type')
384 comment_type = self.request.POST.get('comment_type')
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386
386
387 if status:
387 if status:
388 text = text or (_('Status change %(transition_icon)s %(status)s')
388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 % {'transition_icon': '>',
389 % {'transition_icon': '>',
390 'status': ChangesetStatus.get_status_lbl(status)})
390 'status': ChangesetStatus.get_status_lbl(status)})
391
391
392 multi_commit_ids = []
392 multi_commit_ids = []
393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
395 if _commit_id not in multi_commit_ids:
395 if _commit_id not in multi_commit_ids:
396 multi_commit_ids.append(_commit_id)
396 multi_commit_ids.append(_commit_id)
397
397
398 commit_ids = multi_commit_ids or [commit_id]
398 commit_ids = multi_commit_ids or [commit_id]
399
399
400 comment = None
400 comment = None
401 for current_id in filter(None, commit_ids):
401 for current_id in filter(None, commit_ids):
402 comment = CommentsModel().create(
402 comment = CommentsModel().create(
403 text=text,
403 text=text,
404 repo=self.db_repo.repo_id,
404 repo=self.db_repo.repo_id,
405 user=self._rhodecode_db_user.user_id,
405 user=self._rhodecode_db_user.user_id,
406 commit_id=current_id,
406 commit_id=current_id,
407 f_path=self.request.POST.get('f_path'),
407 f_path=self.request.POST.get('f_path'),
408 line_no=self.request.POST.get('line'),
408 line_no=self.request.POST.get('line'),
409 status_change=(ChangesetStatus.get_status_lbl(status)
409 status_change=(ChangesetStatus.get_status_lbl(status)
410 if status else None),
410 if status else None),
411 status_change_type=status,
411 status_change_type=status,
412 comment_type=comment_type,
412 comment_type=comment_type,
413 resolves_comment_id=resolves_comment_id,
413 resolves_comment_id=resolves_comment_id,
414 auth_user=self._rhodecode_user
414 auth_user=self._rhodecode_user
415 )
415 )
416 is_inline = bool(comment.f_path and comment.line_no)
416 is_inline = comment.is_inline
417
417
418 # get status if set !
418 # get status if set !
419 if status:
419 if status:
420 # if latest status was from pull request and it's closed
420 # if latest status was from pull request and it's closed
421 # disallow changing status !
421 # disallow changing status !
422 # dont_allow_on_closed_pull_request = True !
422 # dont_allow_on_closed_pull_request = True !
423
423
424 try:
424 try:
425 ChangesetStatusModel().set_status(
425 ChangesetStatusModel().set_status(
426 self.db_repo.repo_id,
426 self.db_repo.repo_id,
427 status,
427 status,
428 self._rhodecode_db_user.user_id,
428 self._rhodecode_db_user.user_id,
429 comment,
429 comment,
430 revision=current_id,
430 revision=current_id,
431 dont_allow_on_closed_pull_request=True
431 dont_allow_on_closed_pull_request=True
432 )
432 )
433 except StatusChangeOnClosedPullRequestError:
433 except StatusChangeOnClosedPullRequestError:
434 msg = _('Changing the status of a commit associated with '
434 msg = _('Changing the status of a commit associated with '
435 'a closed pull request is not allowed')
435 'a closed pull request is not allowed')
436 log.exception(msg)
436 log.exception(msg)
437 h.flash(msg, category='warning')
437 h.flash(msg, category='warning')
438 raise HTTPFound(h.route_path(
438 raise HTTPFound(h.route_path(
439 'repo_commit', repo_name=self.db_repo_name,
439 'repo_commit', repo_name=self.db_repo_name,
440 commit_id=current_id))
440 commit_id=current_id))
441
441
442 commit = self.db_repo.get_commit(current_id)
442 commit = self.db_repo.get_commit(current_id)
443 CommentsModel().trigger_commit_comment_hook(
443 CommentsModel().trigger_commit_comment_hook(
444 self.db_repo, self._rhodecode_user, 'create',
444 self.db_repo, self._rhodecode_user, 'create',
445 data={'comment': comment, 'commit': commit})
445 data={'comment': comment, 'commit': commit})
446
446
447 # finalize, commit and redirect
447 # finalize, commit and redirect
448 Session().commit()
448 Session().commit()
449
449
450 data = {
450 data = {
451 'target_id': h.safeid(h.safe_unicode(
451 'target_id': h.safeid(h.safe_unicode(
452 self.request.POST.get('f_path'))),
452 self.request.POST.get('f_path'))),
453 }
453 }
454 if comment:
454 if comment:
455 c.co = comment
455 c.co = comment
456 c.at_version_num = 0
456 c.at_version_num = 0
457 rendered_comment = render(
457 rendered_comment = render(
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 self._get_template_context(c), self.request)
459 self._get_template_context(c), self.request)
460
460
461 data.update(comment.get_dict())
461 data.update(comment.get_dict())
462 data.update({'rendered_text': rendered_comment})
462 data.update({'rendered_text': rendered_comment})
463
463
464 comment_broadcast_channel = channelstream.comment_channel(
464 comment_broadcast_channel = channelstream.comment_channel(
465 self.db_repo_name, commit_obj=commit)
465 self.db_repo_name, commit_obj=commit)
466
466
467 comment_data = data
467 comment_data = data
468 comment_type = 'inline' if is_inline else 'general'
468 comment_type = 'inline' if is_inline else 'general'
469 channelstream.comment_channelstream_push(
469 channelstream.comment_channelstream_push(
470 self.request, comment_broadcast_channel, self._rhodecode_user,
470 self.request, comment_broadcast_channel, self._rhodecode_user,
471 _('posted a new {} comment').format(comment_type),
471 _('posted a new {} comment').format(comment_type),
472 comment_data=comment_data)
472 comment_data=comment_data)
473
473
474 return data
474 return data
475
475
476 @LoginRequired()
476 @LoginRequired()
477 @NotAnonymous()
477 @NotAnonymous()
478 @HasRepoPermissionAnyDecorator(
478 @HasRepoPermissionAnyDecorator(
479 'repository.read', 'repository.write', 'repository.admin')
479 'repository.read', 'repository.write', 'repository.admin')
480 @CSRFRequired()
480 @CSRFRequired()
481 @view_config(
481 @view_config(
482 route_name='repo_commit_comment_preview', request_method='POST',
482 route_name='repo_commit_comment_preview', request_method='POST',
483 renderer='string', xhr=True)
483 renderer='string', xhr=True)
484 def repo_commit_comment_preview(self):
484 def repo_commit_comment_preview(self):
485 # Technically a CSRF token is not needed as no state changes with this
485 # Technically a CSRF token is not needed as no state changes with this
486 # call. However, as this is a POST is better to have it, so automated
486 # call. However, as this is a POST is better to have it, so automated
487 # tools don't flag it as potential CSRF.
487 # tools don't flag it as potential CSRF.
488 # Post is required because the payload could be bigger than the maximum
488 # Post is required because the payload could be bigger than the maximum
489 # allowed by GET.
489 # allowed by GET.
490
490
491 text = self.request.POST.get('text')
491 text = self.request.POST.get('text')
492 renderer = self.request.POST.get('renderer') or 'rst'
492 renderer = self.request.POST.get('renderer') or 'rst'
493 if text:
493 if text:
494 return h.render(text, renderer=renderer, mentions=True,
494 return h.render(text, renderer=renderer, mentions=True,
495 repo_name=self.db_repo_name)
495 repo_name=self.db_repo_name)
496 return ''
496 return ''
497
497
498 @LoginRequired()
498 @LoginRequired()
499 @HasRepoPermissionAnyDecorator(
499 @HasRepoPermissionAnyDecorator(
500 'repository.read', 'repository.write', 'repository.admin')
500 'repository.read', 'repository.write', 'repository.admin')
501 @CSRFRequired()
501 @CSRFRequired()
502 @view_config(
502 @view_config(
503 route_name='repo_commit_comment_history_view', request_method='POST',
503 route_name='repo_commit_comment_history_view', request_method='POST',
504 renderer='string', xhr=True)
504 renderer='string', xhr=True)
505 def repo_commit_comment_history_view(self):
505 def repo_commit_comment_history_view(self):
506 c = self.load_default_context()
506 c = self.load_default_context()
507
507
508 comment_history_id = self.request.matchdict['comment_history_id']
508 comment_history_id = self.request.matchdict['comment_history_id']
509 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
509 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
510 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
510 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
511
511
512 if is_repo_comment:
512 if is_repo_comment:
513 c.comment_history = comment_history
513 c.comment_history = comment_history
514
514
515 rendered_comment = render(
515 rendered_comment = render(
516 'rhodecode:templates/changeset/comment_history.mako',
516 'rhodecode:templates/changeset/comment_history.mako',
517 self._get_template_context(c)
517 self._get_template_context(c)
518 , self.request)
518 , self.request)
519 return rendered_comment
519 return rendered_comment
520 else:
520 else:
521 log.warning('No permissions for user %s to show comment_history_id: %s',
521 log.warning('No permissions for user %s to show comment_history_id: %s',
522 self._rhodecode_db_user, comment_history_id)
522 self._rhodecode_db_user, comment_history_id)
523 raise HTTPNotFound()
523 raise HTTPNotFound()
524
524
525 @LoginRequired()
525 @LoginRequired()
526 @NotAnonymous()
526 @NotAnonymous()
527 @HasRepoPermissionAnyDecorator(
527 @HasRepoPermissionAnyDecorator(
528 'repository.read', 'repository.write', 'repository.admin')
528 'repository.read', 'repository.write', 'repository.admin')
529 @CSRFRequired()
529 @CSRFRequired()
530 @view_config(
530 @view_config(
531 route_name='repo_commit_comment_attachment_upload', request_method='POST',
531 route_name='repo_commit_comment_attachment_upload', request_method='POST',
532 renderer='json_ext', xhr=True)
532 renderer='json_ext', xhr=True)
533 def repo_commit_comment_attachment_upload(self):
533 def repo_commit_comment_attachment_upload(self):
534 c = self.load_default_context()
534 c = self.load_default_context()
535 upload_key = 'attachment'
535 upload_key = 'attachment'
536
536
537 file_obj = self.request.POST.get(upload_key)
537 file_obj = self.request.POST.get(upload_key)
538
538
539 if file_obj is None:
539 if file_obj is None:
540 self.request.response.status = 400
540 self.request.response.status = 400
541 return {'store_fid': None,
541 return {'store_fid': None,
542 'access_path': None,
542 'access_path': None,
543 'error': '{} data field is missing'.format(upload_key)}
543 'error': '{} data field is missing'.format(upload_key)}
544
544
545 if not hasattr(file_obj, 'filename'):
545 if not hasattr(file_obj, 'filename'):
546 self.request.response.status = 400
546 self.request.response.status = 400
547 return {'store_fid': None,
547 return {'store_fid': None,
548 'access_path': None,
548 'access_path': None,
549 'error': 'filename cannot be read from the data field'}
549 'error': 'filename cannot be read from the data field'}
550
550
551 filename = file_obj.filename
551 filename = file_obj.filename
552 file_display_name = filename
552 file_display_name = filename
553
553
554 metadata = {
554 metadata = {
555 'user_uploaded': {'username': self._rhodecode_user.username,
555 'user_uploaded': {'username': self._rhodecode_user.username,
556 'user_id': self._rhodecode_user.user_id,
556 'user_id': self._rhodecode_user.user_id,
557 'ip': self._rhodecode_user.ip_addr}}
557 'ip': self._rhodecode_user.ip_addr}}
558
558
559 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
559 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
560 allowed_extensions = [
560 allowed_extensions = [
561 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
561 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
562 '.pptx', '.txt', '.xlsx', '.zip']
562 '.pptx', '.txt', '.xlsx', '.zip']
563 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
563 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
564
564
565 try:
565 try:
566 storage = store_utils.get_file_storage(self.request.registry.settings)
566 storage = store_utils.get_file_storage(self.request.registry.settings)
567 store_uid, metadata = storage.save_file(
567 store_uid, metadata = storage.save_file(
568 file_obj.file, filename, extra_metadata=metadata,
568 file_obj.file, filename, extra_metadata=metadata,
569 extensions=allowed_extensions, max_filesize=max_file_size)
569 extensions=allowed_extensions, max_filesize=max_file_size)
570 except FileNotAllowedException:
570 except FileNotAllowedException:
571 self.request.response.status = 400
571 self.request.response.status = 400
572 permitted_extensions = ', '.join(allowed_extensions)
572 permitted_extensions = ', '.join(allowed_extensions)
573 error_msg = 'File `{}` is not allowed. ' \
573 error_msg = 'File `{}` is not allowed. ' \
574 'Only following extensions are permitted: {}'.format(
574 'Only following extensions are permitted: {}'.format(
575 filename, permitted_extensions)
575 filename, permitted_extensions)
576 return {'store_fid': None,
576 return {'store_fid': None,
577 'access_path': None,
577 'access_path': None,
578 'error': error_msg}
578 'error': error_msg}
579 except FileOverSizeException:
579 except FileOverSizeException:
580 self.request.response.status = 400
580 self.request.response.status = 400
581 limit_mb = h.format_byte_size_binary(max_file_size)
581 limit_mb = h.format_byte_size_binary(max_file_size)
582 return {'store_fid': None,
582 return {'store_fid': None,
583 'access_path': None,
583 'access_path': None,
584 'error': 'File {} is exceeding allowed limit of {}.'.format(
584 'error': 'File {} is exceeding allowed limit of {}.'.format(
585 filename, limit_mb)}
585 filename, limit_mb)}
586
586
587 try:
587 try:
588 entry = FileStore.create(
588 entry = FileStore.create(
589 file_uid=store_uid, filename=metadata["filename"],
589 file_uid=store_uid, filename=metadata["filename"],
590 file_hash=metadata["sha256"], file_size=metadata["size"],
590 file_hash=metadata["sha256"], file_size=metadata["size"],
591 file_display_name=file_display_name,
591 file_display_name=file_display_name,
592 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
592 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
593 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
593 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
594 scope_repo_id=self.db_repo.repo_id
594 scope_repo_id=self.db_repo.repo_id
595 )
595 )
596 Session().add(entry)
596 Session().add(entry)
597 Session().commit()
597 Session().commit()
598 log.debug('Stored upload in DB as %s', entry)
598 log.debug('Stored upload in DB as %s', entry)
599 except Exception:
599 except Exception:
600 log.exception('Failed to store file %s', filename)
600 log.exception('Failed to store file %s', filename)
601 self.request.response.status = 400
601 self.request.response.status = 400
602 return {'store_fid': None,
602 return {'store_fid': None,
603 'access_path': None,
603 'access_path': None,
604 'error': 'File {} failed to store in DB.'.format(filename)}
604 'error': 'File {} failed to store in DB.'.format(filename)}
605
605
606 Session().commit()
606 Session().commit()
607
607
608 return {
608 return {
609 'store_fid': store_uid,
609 'store_fid': store_uid,
610 'access_path': h.route_path(
610 'access_path': h.route_path(
611 'download_file', fid=store_uid),
611 'download_file', fid=store_uid),
612 'fqn_access_path': h.route_url(
612 'fqn_access_path': h.route_url(
613 'download_file', fid=store_uid),
613 'download_file', fid=store_uid),
614 'repo_access_path': h.route_path(
614 'repo_access_path': h.route_path(
615 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
615 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
616 'repo_fqn_access_path': h.route_url(
616 'repo_fqn_access_path': h.route_url(
617 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
617 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
618 }
618 }
619
619
620 @LoginRequired()
620 @LoginRequired()
621 @NotAnonymous()
621 @NotAnonymous()
622 @HasRepoPermissionAnyDecorator(
622 @HasRepoPermissionAnyDecorator(
623 'repository.read', 'repository.write', 'repository.admin')
623 'repository.read', 'repository.write', 'repository.admin')
624 @CSRFRequired()
624 @CSRFRequired()
625 @view_config(
625 @view_config(
626 route_name='repo_commit_comment_delete', request_method='POST',
626 route_name='repo_commit_comment_delete', request_method='POST',
627 renderer='json_ext')
627 renderer='json_ext')
628 def repo_commit_comment_delete(self):
628 def repo_commit_comment_delete(self):
629 commit_id = self.request.matchdict['commit_id']
629 commit_id = self.request.matchdict['commit_id']
630 comment_id = self.request.matchdict['comment_id']
630 comment_id = self.request.matchdict['comment_id']
631
631
632 comment = ChangesetComment.get_or_404(comment_id)
632 comment = ChangesetComment.get_or_404(comment_id)
633 if not comment:
633 if not comment:
634 log.debug('Comment with id:%s not found, skipping', comment_id)
634 log.debug('Comment with id:%s not found, skipping', comment_id)
635 # comment already deleted in another call probably
635 # comment already deleted in another call probably
636 return True
636 return True
637
637
638 if comment.immutable:
638 if comment.immutable:
639 # don't allow deleting comments that are immutable
639 # don't allow deleting comments that are immutable
640 raise HTTPForbidden()
640 raise HTTPForbidden()
641
641
642 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
642 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
643 super_admin = h.HasPermissionAny('hg.admin')()
643 super_admin = h.HasPermissionAny('hg.admin')()
644 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
644 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
645 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
645 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
646 comment_repo_admin = is_repo_admin and is_repo_comment
646 comment_repo_admin = is_repo_admin and is_repo_comment
647
647
648 if super_admin or comment_owner or comment_repo_admin:
648 if super_admin or comment_owner or comment_repo_admin:
649 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
649 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
650 Session().commit()
650 Session().commit()
651 return True
651 return True
652 else:
652 else:
653 log.warning('No permissions for user %s to delete comment_id: %s',
653 log.warning('No permissions for user %s to delete comment_id: %s',
654 self._rhodecode_db_user, comment_id)
654 self._rhodecode_db_user, comment_id)
655 raise HTTPNotFound()
655 raise HTTPNotFound()
656
656
657 @LoginRequired()
657 @LoginRequired()
658 @NotAnonymous()
658 @NotAnonymous()
659 @HasRepoPermissionAnyDecorator(
659 @HasRepoPermissionAnyDecorator(
660 'repository.read', 'repository.write', 'repository.admin')
660 'repository.read', 'repository.write', 'repository.admin')
661 @CSRFRequired()
661 @CSRFRequired()
662 @view_config(
662 @view_config(
663 route_name='repo_commit_comment_edit', request_method='POST',
663 route_name='repo_commit_comment_edit', request_method='POST',
664 renderer='json_ext')
664 renderer='json_ext')
665 def repo_commit_comment_edit(self):
665 def repo_commit_comment_edit(self):
666 self.load_default_context()
666 self.load_default_context()
667
667
668 comment_id = self.request.matchdict['comment_id']
668 comment_id = self.request.matchdict['comment_id']
669 comment = ChangesetComment.get_or_404(comment_id)
669 comment = ChangesetComment.get_or_404(comment_id)
670
670
671 if comment.immutable:
671 if comment.immutable:
672 # don't allow deleting comments that are immutable
672 # don't allow deleting comments that are immutable
673 raise HTTPForbidden()
673 raise HTTPForbidden()
674
674
675 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
675 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
676 super_admin = h.HasPermissionAny('hg.admin')()
676 super_admin = h.HasPermissionAny('hg.admin')()
677 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
677 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
678 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
678 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
679 comment_repo_admin = is_repo_admin and is_repo_comment
679 comment_repo_admin = is_repo_admin and is_repo_comment
680
680
681 if super_admin or comment_owner or comment_repo_admin:
681 if super_admin or comment_owner or comment_repo_admin:
682 text = self.request.POST.get('text')
682 text = self.request.POST.get('text')
683 version = self.request.POST.get('version')
683 version = self.request.POST.get('version')
684 if text == comment.text:
684 if text == comment.text:
685 log.warning(
685 log.warning(
686 'Comment(repo): '
686 'Comment(repo): '
687 'Trying to create new version '
687 'Trying to create new version '
688 'with the same comment body {}'.format(
688 'with the same comment body {}'.format(
689 comment_id,
689 comment_id,
690 )
690 )
691 )
691 )
692 raise HTTPNotFound()
692 raise HTTPNotFound()
693
693
694 if version.isdigit():
694 if version.isdigit():
695 version = int(version)
695 version = int(version)
696 else:
696 else:
697 log.warning(
697 log.warning(
698 'Comment(repo): Wrong version type {} {} '
698 'Comment(repo): Wrong version type {} {} '
699 'for comment {}'.format(
699 'for comment {}'.format(
700 version,
700 version,
701 type(version),
701 type(version),
702 comment_id,
702 comment_id,
703 )
703 )
704 )
704 )
705 raise HTTPNotFound()
705 raise HTTPNotFound()
706
706
707 try:
707 try:
708 comment_history = CommentsModel().edit(
708 comment_history = CommentsModel().edit(
709 comment_id=comment_id,
709 comment_id=comment_id,
710 text=text,
710 text=text,
711 auth_user=self._rhodecode_user,
711 auth_user=self._rhodecode_user,
712 version=version,
712 version=version,
713 )
713 )
714 except CommentVersionMismatch:
714 except CommentVersionMismatch:
715 raise HTTPConflict()
715 raise HTTPConflict()
716
716
717 if not comment_history:
717 if not comment_history:
718 raise HTTPNotFound()
718 raise HTTPNotFound()
719
719
720 commit_id = self.request.matchdict['commit_id']
720 commit_id = self.request.matchdict['commit_id']
721 commit = self.db_repo.get_commit(commit_id)
721 commit = self.db_repo.get_commit(commit_id)
722 CommentsModel().trigger_commit_comment_hook(
722 CommentsModel().trigger_commit_comment_hook(
723 self.db_repo, self._rhodecode_user, 'edit',
723 self.db_repo, self._rhodecode_user, 'edit',
724 data={'comment': comment, 'commit': commit})
724 data={'comment': comment, 'commit': commit})
725
725
726 Session().commit()
726 Session().commit()
727 return {
727 return {
728 'comment_history_id': comment_history.comment_history_id,
728 'comment_history_id': comment_history.comment_history_id,
729 'comment_id': comment.comment_id,
729 'comment_id': comment.comment_id,
730 'comment_version': comment_history.version,
730 'comment_version': comment_history.version,
731 'comment_author_username': comment_history.author.username,
731 'comment_author_username': comment_history.author.username,
732 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
732 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
733 'comment_created_on': h.age_component(comment_history.created_on,
733 'comment_created_on': h.age_component(comment_history.created_on,
734 time_is_local=True),
734 time_is_local=True),
735 }
735 }
736 else:
736 else:
737 log.warning('No permissions for user %s to edit comment_id: %s',
737 log.warning('No permissions for user %s to edit comment_id: %s',
738 self._rhodecode_db_user, comment_id)
738 self._rhodecode_db_user, comment_id)
739 raise HTTPNotFound()
739 raise HTTPNotFound()
740
740
741 @LoginRequired()
741 @LoginRequired()
742 @HasRepoPermissionAnyDecorator(
742 @HasRepoPermissionAnyDecorator(
743 'repository.read', 'repository.write', 'repository.admin')
743 'repository.read', 'repository.write', 'repository.admin')
744 @view_config(
744 @view_config(
745 route_name='repo_commit_data', request_method='GET',
745 route_name='repo_commit_data', request_method='GET',
746 renderer='json_ext', xhr=True)
746 renderer='json_ext', xhr=True)
747 def repo_commit_data(self):
747 def repo_commit_data(self):
748 commit_id = self.request.matchdict['commit_id']
748 commit_id = self.request.matchdict['commit_id']
749 self.load_default_context()
749 self.load_default_context()
750
750
751 try:
751 try:
752 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
752 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
753 except CommitDoesNotExistError as e:
753 except CommitDoesNotExistError as e:
754 return EmptyCommit(message=str(e))
754 return EmptyCommit(message=str(e))
755
755
756 @LoginRequired()
756 @LoginRequired()
757 @HasRepoPermissionAnyDecorator(
757 @HasRepoPermissionAnyDecorator(
758 'repository.read', 'repository.write', 'repository.admin')
758 'repository.read', 'repository.write', 'repository.admin')
759 @view_config(
759 @view_config(
760 route_name='repo_commit_children', request_method='GET',
760 route_name='repo_commit_children', request_method='GET',
761 renderer='json_ext', xhr=True)
761 renderer='json_ext', xhr=True)
762 def repo_commit_children(self):
762 def repo_commit_children(self):
763 commit_id = self.request.matchdict['commit_id']
763 commit_id = self.request.matchdict['commit_id']
764 self.load_default_context()
764 self.load_default_context()
765
765
766 try:
766 try:
767 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
767 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
768 children = commit.children
768 children = commit.children
769 except CommitDoesNotExistError:
769 except CommitDoesNotExistError:
770 children = []
770 children = []
771
771
772 result = {"results": children}
772 result = {"results": children}
773 return result
773 return result
774
774
775 @LoginRequired()
775 @LoginRequired()
776 @HasRepoPermissionAnyDecorator(
776 @HasRepoPermissionAnyDecorator(
777 'repository.read', 'repository.write', 'repository.admin')
777 'repository.read', 'repository.write', 'repository.admin')
778 @view_config(
778 @view_config(
779 route_name='repo_commit_parents', request_method='GET',
779 route_name='repo_commit_parents', request_method='GET',
780 renderer='json_ext')
780 renderer='json_ext')
781 def repo_commit_parents(self):
781 def repo_commit_parents(self):
782 commit_id = self.request.matchdict['commit_id']
782 commit_id = self.request.matchdict['commit_id']
783 self.load_default_context()
783 self.load_default_context()
784
784
785 try:
785 try:
786 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
786 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
787 parents = commit.parents
787 parents = commit.parents
788 except CommitDoesNotExistError:
788 except CommitDoesNotExistError:
789 parents = []
789 parents = []
790 result = {"results": parents}
790 result = {"results": parents}
791 return result
791 return result
@@ -1,1813 +1,1816 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason, Reference
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
49 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 PullRequestReviewers)
51 PullRequestReviewers)
51 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
55
56
56 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
57
58
58
59
59 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60
61
61 def load_default_context(self):
62 def load_default_context(self):
62 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
66 c.renderer = 'plain'
67 c.renderer = 'plain'
67 return c
68 return c
68
69
69 def _get_pull_requests_list(
70 def _get_pull_requests_list(
70 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
71
72
72 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
73 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
75 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
76
77
77 # pagination
78 # pagination
78
79
79 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
80 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
81 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 statuses=statuses, offset=start, length=limit,
83 statuses=statuses, offset=start, length=limit,
83 order_by=order_by, order_dir=order_dir)
84 order_by=order_by, order_dir=order_dir)
84 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 opened_by=opened_by)
87 opened_by=opened_by)
87 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
88 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by,
92 order_dir=order_dir)
93 order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 statuses=statuses, opened_by=opened_by)
96 statuses=statuses, opened_by=opened_by)
96 else:
97 else:
97 pull_requests = PullRequestModel().get_all(
98 pull_requests = PullRequestModel().get_all(
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 statuses=statuses, offset=start, length=limit,
100 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
101 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
102 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
104 opened_by=opened_by)
104
105
105 data = []
106 data = []
106 comments_model = CommentsModel()
107 comments_model = CommentsModel()
107 for pr in pull_requests:
108 for pr in pull_requests:
108 comments_count = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr, count_only=True)
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
110
111
111 data.append({
112 data.append({
112 'name': _render('pullrequest_name',
113 'name': _render('pullrequest_name',
113 pr.pull_request_id, pr.pull_request_state,
114 pr.pull_request_id, pr.pull_request_state,
114 pr.work_in_progress, pr.target_repo.repo_name,
115 pr.work_in_progress, pr.target_repo.repo_name,
115 short=True),
116 short=True),
116 'name_raw': pr.pull_request_id,
117 'name_raw': pr.pull_request_id,
117 'status': _render('pullrequest_status',
118 'status': _render('pullrequest_status',
118 pr.calculated_review_status()),
119 pr.calculated_review_status()),
119 'title': _render('pullrequest_title', pr.title, pr.description),
120 'title': _render('pullrequest_title', pr.title, pr.description),
120 'description': h.escape(pr.description),
121 'description': h.escape(pr.description),
121 'updated_on': _render('pullrequest_updated_on',
122 'updated_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.updated_on)),
123 h.datetime_to_time(pr.updated_on)),
123 'updated_on_raw': h.datetime_to_time(pr.updated_on),
124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
124 'created_on': _render('pullrequest_updated_on',
125 'created_on': _render('pullrequest_updated_on',
125 h.datetime_to_time(pr.created_on)),
126 h.datetime_to_time(pr.created_on)),
126 'created_on_raw': h.datetime_to_time(pr.created_on),
127 'created_on_raw': h.datetime_to_time(pr.created_on),
127 'state': pr.pull_request_state,
128 'state': pr.pull_request_state,
128 'author': _render('pullrequest_author',
129 'author': _render('pullrequest_author',
129 pr.author.full_contact, ),
130 pr.author.full_contact, ),
130 'author_raw': pr.author.full_name,
131 'author_raw': pr.author.full_name,
131 'comments': _render('pullrequest_comments', comments_count),
132 'comments': _render('pullrequest_comments', comments_count),
132 'comments_raw': comments_count,
133 'comments_raw': comments_count,
133 'closed': pr.is_closed(),
134 'closed': pr.is_closed(),
134 })
135 })
135
136
136 data = ({
137 data = ({
137 'draw': draw,
138 'draw': draw,
138 'data': data,
139 'data': data,
139 'recordsTotal': pull_requests_total_count,
140 'recordsTotal': pull_requests_total_count,
140 'recordsFiltered': pull_requests_total_count,
141 'recordsFiltered': pull_requests_total_count,
141 })
142 })
142 return data
143 return data
143
144
144 @LoginRequired()
145 @LoginRequired()
145 @HasRepoPermissionAnyDecorator(
146 @HasRepoPermissionAnyDecorator(
146 'repository.read', 'repository.write', 'repository.admin')
147 'repository.read', 'repository.write', 'repository.admin')
147 @view_config(
148 @view_config(
148 route_name='pullrequest_show_all', request_method='GET',
149 route_name='pullrequest_show_all', request_method='GET',
149 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
150 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
150 def pull_request_list(self):
151 def pull_request_list(self):
151 c = self.load_default_context()
152 c = self.load_default_context()
152
153
153 req_get = self.request.GET
154 req_get = self.request.GET
154 c.source = str2bool(req_get.get('source'))
155 c.source = str2bool(req_get.get('source'))
155 c.closed = str2bool(req_get.get('closed'))
156 c.closed = str2bool(req_get.get('closed'))
156 c.my = str2bool(req_get.get('my'))
157 c.my = str2bool(req_get.get('my'))
157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159
160
160 c.active = 'open'
161 c.active = 'open'
161 if c.my:
162 if c.my:
162 c.active = 'my'
163 c.active = 'my'
163 if c.closed:
164 if c.closed:
164 c.active = 'closed'
165 c.active = 'closed'
165 if c.awaiting_review and not c.source:
166 if c.awaiting_review and not c.source:
166 c.active = 'awaiting'
167 c.active = 'awaiting'
167 if c.source and not c.awaiting_review:
168 if c.source and not c.awaiting_review:
168 c.active = 'source'
169 c.active = 'source'
169 if c.awaiting_my_review:
170 if c.awaiting_my_review:
170 c.active = 'awaiting_my'
171 c.active = 'awaiting_my'
171
172
172 return self._get_template_context(c)
173 return self._get_template_context(c)
173
174
174 @LoginRequired()
175 @LoginRequired()
175 @HasRepoPermissionAnyDecorator(
176 @HasRepoPermissionAnyDecorator(
176 'repository.read', 'repository.write', 'repository.admin')
177 'repository.read', 'repository.write', 'repository.admin')
177 @view_config(
178 @view_config(
178 route_name='pullrequest_show_all_data', request_method='GET',
179 route_name='pullrequest_show_all_data', request_method='GET',
179 renderer='json_ext', xhr=True)
180 renderer='json_ext', xhr=True)
180 def pull_request_list_data(self):
181 def pull_request_list_data(self):
181 self.load_default_context()
182 self.load_default_context()
182
183
183 # additional filters
184 # additional filters
184 req_get = self.request.GET
185 req_get = self.request.GET
185 source = str2bool(req_get.get('source'))
186 source = str2bool(req_get.get('source'))
186 closed = str2bool(req_get.get('closed'))
187 closed = str2bool(req_get.get('closed'))
187 my = str2bool(req_get.get('my'))
188 my = str2bool(req_get.get('my'))
188 awaiting_review = str2bool(req_get.get('awaiting_review'))
189 awaiting_review = str2bool(req_get.get('awaiting_review'))
189 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
190 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
190
191
191 filter_type = 'awaiting_review' if awaiting_review \
192 filter_type = 'awaiting_review' if awaiting_review \
192 else 'awaiting_my_review' if awaiting_my_review \
193 else 'awaiting_my_review' if awaiting_my_review \
193 else None
194 else None
194
195
195 opened_by = None
196 opened_by = None
196 if my:
197 if my:
197 opened_by = [self._rhodecode_user.user_id]
198 opened_by = [self._rhodecode_user.user_id]
198
199
199 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
200 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
200 if closed:
201 if closed:
201 statuses = [PullRequest.STATUS_CLOSED]
202 statuses = [PullRequest.STATUS_CLOSED]
202
203
203 data = self._get_pull_requests_list(
204 data = self._get_pull_requests_list(
204 repo_name=self.db_repo_name, source=source,
205 repo_name=self.db_repo_name, source=source,
205 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
206 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
206
207
207 return data
208 return data
208
209
209 def _is_diff_cache_enabled(self, target_repo):
210 def _is_diff_cache_enabled(self, target_repo):
210 caching_enabled = self._get_general_setting(
211 caching_enabled = self._get_general_setting(
211 target_repo, 'rhodecode_diff_cache')
212 target_repo, 'rhodecode_diff_cache')
212 log.debug('Diff caching enabled: %s', caching_enabled)
213 log.debug('Diff caching enabled: %s', caching_enabled)
213 return caching_enabled
214 return caching_enabled
214
215
215 def _get_diffset(self, source_repo_name, source_repo,
216 def _get_diffset(self, source_repo_name, source_repo,
216 ancestor_commit,
217 ancestor_commit,
217 source_ref_id, target_ref_id,
218 source_ref_id, target_ref_id,
218 target_commit, source_commit, diff_limit, file_limit,
219 target_commit, source_commit, diff_limit, file_limit,
219 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
220 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
220
221
221 if use_ancestor:
222 if use_ancestor:
222 # we might want to not use it for versions
223 # we might want to not use it for versions
223 target_ref_id = ancestor_commit.raw_id
224 target_ref_id = ancestor_commit.raw_id
224
225
225 vcs_diff = PullRequestModel().get_diff(
226 vcs_diff = PullRequestModel().get_diff(
226 source_repo, source_ref_id, target_ref_id,
227 source_repo, source_ref_id, target_ref_id,
227 hide_whitespace_changes, diff_context)
228 hide_whitespace_changes, diff_context)
228
229
229 diff_processor = diffs.DiffProcessor(
230 diff_processor = diffs.DiffProcessor(
230 vcs_diff, format='newdiff', diff_limit=diff_limit,
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
231 file_limit=file_limit, show_full_diff=fulldiff)
232 file_limit=file_limit, show_full_diff=fulldiff)
232
233
233 _parsed = diff_processor.prepare()
234 _parsed = diff_processor.prepare()
234
235
235 diffset = codeblocks.DiffSet(
236 diffset = codeblocks.DiffSet(
236 repo_name=self.db_repo_name,
237 repo_name=self.db_repo_name,
237 source_repo_name=source_repo_name,
238 source_repo_name=source_repo_name,
238 source_node_getter=codeblocks.diffset_node_getter(target_commit),
239 source_node_getter=codeblocks.diffset_node_getter(target_commit),
239 target_node_getter=codeblocks.diffset_node_getter(source_commit),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit),
240 )
241 )
241 diffset = self.path_filter.render_patchset_filtered(
242 diffset = self.path_filter.render_patchset_filtered(
242 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
243 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
243
244
244 return diffset
245 return diffset
245
246
246 def _get_range_diffset(self, source_scm, source_repo,
247 def _get_range_diffset(self, source_scm, source_repo,
247 commit1, commit2, diff_limit, file_limit,
248 commit1, commit2, diff_limit, file_limit,
248 fulldiff, hide_whitespace_changes, diff_context):
249 fulldiff, hide_whitespace_changes, diff_context):
249 vcs_diff = source_scm.get_diff(
250 vcs_diff = source_scm.get_diff(
250 commit1, commit2,
251 commit1, commit2,
251 ignore_whitespace=hide_whitespace_changes,
252 ignore_whitespace=hide_whitespace_changes,
252 context=diff_context)
253 context=diff_context)
253
254
254 diff_processor = diffs.DiffProcessor(
255 diff_processor = diffs.DiffProcessor(
255 vcs_diff, format='newdiff', diff_limit=diff_limit,
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
256 file_limit=file_limit, show_full_diff=fulldiff)
257 file_limit=file_limit, show_full_diff=fulldiff)
257
258
258 _parsed = diff_processor.prepare()
259 _parsed = diff_processor.prepare()
259
260
260 diffset = codeblocks.DiffSet(
261 diffset = codeblocks.DiffSet(
261 repo_name=source_repo.repo_name,
262 repo_name=source_repo.repo_name,
262 source_node_getter=codeblocks.diffset_node_getter(commit1),
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
263 target_node_getter=codeblocks.diffset_node_getter(commit2))
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
264
265
265 diffset = self.path_filter.render_patchset_filtered(
266 diffset = self.path_filter.render_patchset_filtered(
266 diffset, _parsed, commit1.raw_id, commit2.raw_id)
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
267
268
268 return diffset
269 return diffset
269
270
270 def register_comments_vars(self, c, pull_request, versions):
271 def register_comments_vars(self, c, pull_request, versions):
271 comments_model = CommentsModel()
272 comments_model = CommentsModel()
272
273
273 # GENERAL COMMENTS with versions #
274 # GENERAL COMMENTS with versions #
274 q = comments_model._all_general_comments_of_pull_request(pull_request)
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
275 q = q.order_by(ChangesetComment.comment_id.asc())
276 q = q.order_by(ChangesetComment.comment_id.asc())
276 general_comments = q
277 general_comments = q
277
278
278 # pick comments we want to render at current version
279 # pick comments we want to render at current version
279 c.comment_versions = comments_model.aggregate_comments(
280 c.comment_versions = comments_model.aggregate_comments(
280 general_comments, versions, c.at_version_num)
281 general_comments, versions, c.at_version_num)
281
282
282 # INLINE COMMENTS with versions #
283 # INLINE COMMENTS with versions #
283 q = comments_model._all_inline_comments_of_pull_request(pull_request)
284 q = comments_model._all_inline_comments_of_pull_request(pull_request)
284 q = q.order_by(ChangesetComment.comment_id.asc())
285 q = q.order_by(ChangesetComment.comment_id.asc())
285 inline_comments = q
286 inline_comments = q
286
287
287 c.inline_versions = comments_model.aggregate_comments(
288 c.inline_versions = comments_model.aggregate_comments(
288 inline_comments, versions, c.at_version_num, inline=True)
289 inline_comments, versions, c.at_version_num, inline=True)
289
290
290 # Comments inline+general
291 # Comments inline+general
291 if c.at_version:
292 if c.at_version:
292 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
293 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
293 c.comments = c.comment_versions[c.at_version_num]['display']
294 c.comments = c.comment_versions[c.at_version_num]['display']
294 else:
295 else:
295 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
296 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
296 c.comments = c.comment_versions[c.at_version_num]['until']
297 c.comments = c.comment_versions[c.at_version_num]['until']
297
298
298 return general_comments, inline_comments
299 return general_comments, inline_comments
299
300
300 @LoginRequired()
301 @LoginRequired()
301 @HasRepoPermissionAnyDecorator(
302 @HasRepoPermissionAnyDecorator(
302 'repository.read', 'repository.write', 'repository.admin')
303 'repository.read', 'repository.write', 'repository.admin')
303 @view_config(
304 @view_config(
304 route_name='pullrequest_show', request_method='GET',
305 route_name='pullrequest_show', request_method='GET',
305 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
306 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
306 def pull_request_show(self):
307 def pull_request_show(self):
307 _ = self.request.translate
308 _ = self.request.translate
308 c = self.load_default_context()
309 c = self.load_default_context()
309
310
310 pull_request = PullRequest.get_or_404(
311 pull_request = PullRequest.get_or_404(
311 self.request.matchdict['pull_request_id'])
312 self.request.matchdict['pull_request_id'])
312 pull_request_id = pull_request.pull_request_id
313 pull_request_id = pull_request.pull_request_id
313
314
314 c.state_progressing = pull_request.is_state_changing()
315 c.state_progressing = pull_request.is_state_changing()
315 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
316 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
316
317
317 _new_state = {
318 _new_state = {
318 'created': PullRequest.STATE_CREATED,
319 'created': PullRequest.STATE_CREATED,
319 }.get(self.request.GET.get('force_state'))
320 }.get(self.request.GET.get('force_state'))
320
321
321 if c.is_super_admin and _new_state:
322 if c.is_super_admin and _new_state:
322 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
323 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
323 h.flash(
324 h.flash(
324 _('Pull Request state was force changed to `{}`').format(_new_state),
325 _('Pull Request state was force changed to `{}`').format(_new_state),
325 category='success')
326 category='success')
326 Session().commit()
327 Session().commit()
327
328
328 raise HTTPFound(h.route_path(
329 raise HTTPFound(h.route_path(
329 'pullrequest_show', repo_name=self.db_repo_name,
330 'pullrequest_show', repo_name=self.db_repo_name,
330 pull_request_id=pull_request_id))
331 pull_request_id=pull_request_id))
331
332
332 version = self.request.GET.get('version')
333 version = self.request.GET.get('version')
333 from_version = self.request.GET.get('from_version') or version
334 from_version = self.request.GET.get('from_version') or version
334 merge_checks = self.request.GET.get('merge_checks')
335 merge_checks = self.request.GET.get('merge_checks')
335 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
336 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
336 force_refresh = str2bool(self.request.GET.get('force_refresh'))
337 force_refresh = str2bool(self.request.GET.get('force_refresh'))
337 c.range_diff_on = self.request.GET.get('range-diff') == "1"
338 c.range_diff_on = self.request.GET.get('range-diff') == "1"
338
339
339 # fetch global flags of ignore ws or context lines
340 # fetch global flags of ignore ws or context lines
340 diff_context = diffs.get_diff_context(self.request)
341 diff_context = diffs.get_diff_context(self.request)
341 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
342 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
342
343
343 (pull_request_latest,
344 (pull_request_latest,
344 pull_request_at_ver,
345 pull_request_at_ver,
345 pull_request_display_obj,
346 pull_request_display_obj,
346 at_version) = PullRequestModel().get_pr_version(
347 at_version) = PullRequestModel().get_pr_version(
347 pull_request_id, version=version)
348 pull_request_id, version=version)
348
349
349 pr_closed = pull_request_latest.is_closed()
350 pr_closed = pull_request_latest.is_closed()
350
351
351 if pr_closed and (version or from_version):
352 if pr_closed and (version or from_version):
352 # not allow to browse versions for closed PR
353 # not allow to browse versions for closed PR
353 raise HTTPFound(h.route_path(
354 raise HTTPFound(h.route_path(
354 'pullrequest_show', repo_name=self.db_repo_name,
355 'pullrequest_show', repo_name=self.db_repo_name,
355 pull_request_id=pull_request_id))
356 pull_request_id=pull_request_id))
356
357
357 versions = pull_request_display_obj.versions()
358 versions = pull_request_display_obj.versions()
358 # used to store per-commit range diffs
359 # used to store per-commit range diffs
359 c.changes = collections.OrderedDict()
360 c.changes = collections.OrderedDict()
360
361
361 c.at_version = at_version
362 c.at_version = at_version
362 c.at_version_num = (at_version
363 c.at_version_num = (at_version
363 if at_version and at_version != PullRequest.LATEST_VER
364 if at_version and at_version != PullRequest.LATEST_VER
364 else None)
365 else None)
365
366
366 c.at_version_index = ChangesetComment.get_index_from_version(
367 c.at_version_index = ChangesetComment.get_index_from_version(
367 c.at_version_num, versions)
368 c.at_version_num, versions)
368
369
369 (prev_pull_request_latest,
370 (prev_pull_request_latest,
370 prev_pull_request_at_ver,
371 prev_pull_request_at_ver,
371 prev_pull_request_display_obj,
372 prev_pull_request_display_obj,
372 prev_at_version) = PullRequestModel().get_pr_version(
373 prev_at_version) = PullRequestModel().get_pr_version(
373 pull_request_id, version=from_version)
374 pull_request_id, version=from_version)
374
375
375 c.from_version = prev_at_version
376 c.from_version = prev_at_version
376 c.from_version_num = (prev_at_version
377 c.from_version_num = (prev_at_version
377 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
378 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
378 else None)
379 else None)
379 c.from_version_index = ChangesetComment.get_index_from_version(
380 c.from_version_index = ChangesetComment.get_index_from_version(
380 c.from_version_num, versions)
381 c.from_version_num, versions)
381
382
382 # define if we're in COMPARE mode or VIEW at version mode
383 # define if we're in COMPARE mode or VIEW at version mode
383 compare = at_version != prev_at_version
384 compare = at_version != prev_at_version
384
385
385 # pull_requests repo_name we opened it against
386 # pull_requests repo_name we opened it against
386 # ie. target_repo must match
387 # ie. target_repo must match
387 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
388 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
388 log.warning('Mismatch between the current repo: %s, and target %s',
389 log.warning('Mismatch between the current repo: %s, and target %s',
389 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
390 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
390 raise HTTPNotFound()
391 raise HTTPNotFound()
391
392
392 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
393 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
393
394
394 c.pull_request = pull_request_display_obj
395 c.pull_request = pull_request_display_obj
395 c.renderer = pull_request_at_ver.description_renderer or c.renderer
396 c.renderer = pull_request_at_ver.description_renderer or c.renderer
396 c.pull_request_latest = pull_request_latest
397 c.pull_request_latest = pull_request_latest
397
398
398 # inject latest version
399 # inject latest version
399 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
400 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
400 c.versions = versions + [latest_ver]
401 c.versions = versions + [latest_ver]
401
402
402 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
403 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
403 c.allowed_to_change_status = False
404 c.allowed_to_change_status = False
404 c.allowed_to_update = False
405 c.allowed_to_update = False
405 c.allowed_to_merge = False
406 c.allowed_to_merge = False
406 c.allowed_to_delete = False
407 c.allowed_to_delete = False
407 c.allowed_to_comment = False
408 c.allowed_to_comment = False
408 c.allowed_to_close = False
409 c.allowed_to_close = False
409 else:
410 else:
410 can_change_status = PullRequestModel().check_user_change_status(
411 can_change_status = PullRequestModel().check_user_change_status(
411 pull_request_at_ver, self._rhodecode_user)
412 pull_request_at_ver, self._rhodecode_user)
412 c.allowed_to_change_status = can_change_status and not pr_closed
413 c.allowed_to_change_status = can_change_status and not pr_closed
413
414
414 c.allowed_to_update = PullRequestModel().check_user_update(
415 c.allowed_to_update = PullRequestModel().check_user_update(
415 pull_request_latest, self._rhodecode_user) and not pr_closed
416 pull_request_latest, self._rhodecode_user) and not pr_closed
416 c.allowed_to_merge = PullRequestModel().check_user_merge(
417 c.allowed_to_merge = PullRequestModel().check_user_merge(
417 pull_request_latest, self._rhodecode_user) and not pr_closed
418 pull_request_latest, self._rhodecode_user) and not pr_closed
418 c.allowed_to_delete = PullRequestModel().check_user_delete(
419 c.allowed_to_delete = PullRequestModel().check_user_delete(
419 pull_request_latest, self._rhodecode_user) and not pr_closed
420 pull_request_latest, self._rhodecode_user) and not pr_closed
420 c.allowed_to_comment = not pr_closed
421 c.allowed_to_comment = not pr_closed
421 c.allowed_to_close = c.allowed_to_merge and not pr_closed
422 c.allowed_to_close = c.allowed_to_merge and not pr_closed
422
423
423 c.forbid_adding_reviewers = False
424 c.forbid_adding_reviewers = False
424 c.forbid_author_to_review = False
425 c.forbid_author_to_review = False
425 c.forbid_commit_author_to_review = False
426 c.forbid_commit_author_to_review = False
426
427
427 if pull_request_latest.reviewer_data and \
428 if pull_request_latest.reviewer_data and \
428 'rules' in pull_request_latest.reviewer_data:
429 'rules' in pull_request_latest.reviewer_data:
429 rules = pull_request_latest.reviewer_data['rules'] or {}
430 rules = pull_request_latest.reviewer_data['rules'] or {}
430 try:
431 try:
431 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
432 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
432 c.forbid_author_to_review = rules.get('forbid_author_to_review')
433 c.forbid_author_to_review = rules.get('forbid_author_to_review')
433 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
434 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
434 except Exception:
435 except Exception:
435 pass
436 pass
436
437
437 # check merge capabilities
438 # check merge capabilities
438 _merge_check = MergeCheck.validate(
439 _merge_check = MergeCheck.validate(
439 pull_request_latest, auth_user=self._rhodecode_user,
440 pull_request_latest, auth_user=self._rhodecode_user,
440 translator=self.request.translate,
441 translator=self.request.translate,
441 force_shadow_repo_refresh=force_refresh)
442 force_shadow_repo_refresh=force_refresh)
442
443
443 c.pr_merge_errors = _merge_check.error_details
444 c.pr_merge_errors = _merge_check.error_details
444 c.pr_merge_possible = not _merge_check.failed
445 c.pr_merge_possible = not _merge_check.failed
445 c.pr_merge_message = _merge_check.merge_msg
446 c.pr_merge_message = _merge_check.merge_msg
446 c.pr_merge_source_commit = _merge_check.source_commit
447 c.pr_merge_source_commit = _merge_check.source_commit
447 c.pr_merge_target_commit = _merge_check.target_commit
448 c.pr_merge_target_commit = _merge_check.target_commit
448
449
449 c.pr_merge_info = MergeCheck.get_merge_conditions(
450 c.pr_merge_info = MergeCheck.get_merge_conditions(
450 pull_request_latest, translator=self.request.translate)
451 pull_request_latest, translator=self.request.translate)
451
452
452 c.pull_request_review_status = _merge_check.review_status
453 c.pull_request_review_status = _merge_check.review_status
453 if merge_checks:
454 if merge_checks:
454 self.request.override_renderer = \
455 self.request.override_renderer = \
455 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
456 return self._get_template_context(c)
457 return self._get_template_context(c)
457
458
458 c.reviewers_count = pull_request.reviewers_count
459 c.reviewers_count = pull_request.reviewers_count
459 c.observers_count = pull_request.observers_count
460 c.observers_count = pull_request.observers_count
460
461
461 # reviewers and statuses
462 # reviewers and statuses
462 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
464 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
465
466
466 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 member_reviewer = h.reviewer_as_json(
468 member_reviewer = h.reviewer_as_json(
468 member, reasons=reasons, mandatory=mandatory,
469 member, reasons=reasons, mandatory=mandatory,
469 role=review_obj.role,
470 role=review_obj.role,
470 user_group=review_obj.rule_user_group_data()
471 user_group=review_obj.rule_user_group_data()
471 )
472 )
472
473
473 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
474 member_reviewer['review_status'] = current_review_status
475 member_reviewer['review_status'] = current_review_status
475 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
476 member_reviewer['allowed_to_update'] = c.allowed_to_update
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
477 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
478
479
479 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
480
481
481 for observer_obj, member in pull_request_at_ver.observers():
482 for observer_obj, member in pull_request_at_ver.observers():
482 member_observer = h.reviewer_as_json(
483 member_observer = h.reviewer_as_json(
483 member, reasons=[], mandatory=False,
484 member, reasons=[], mandatory=False,
484 role=observer_obj.role,
485 role=observer_obj.role,
485 user_group=observer_obj.rule_user_group_data()
486 user_group=observer_obj.rule_user_group_data()
486 )
487 )
487 member_observer['allowed_to_update'] = c.allowed_to_update
488 member_observer['allowed_to_update'] = c.allowed_to_update
488 c.pull_request_set_observers_data_json['observers'].append(member_observer)
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
489
490
490 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
491
492
492 general_comments, inline_comments = \
493 general_comments, inline_comments = \
493 self.register_comments_vars(c, pull_request_latest, versions)
494 self.register_comments_vars(c, pull_request_latest, versions)
494
495
495 # TODOs
496 # TODOs
496 c.unresolved_comments = CommentsModel() \
497 c.unresolved_comments = CommentsModel() \
497 .get_pull_request_unresolved_todos(pull_request_latest)
498 .get_pull_request_unresolved_todos(pull_request_latest)
498 c.resolved_comments = CommentsModel() \
499 c.resolved_comments = CommentsModel() \
499 .get_pull_request_resolved_todos(pull_request_latest)
500 .get_pull_request_resolved_todos(pull_request_latest)
500
501
501 # if we use version, then do not show later comments
502 # if we use version, then do not show later comments
502 # than current version
503 # than current version
503 display_inline_comments = collections.defaultdict(
504 display_inline_comments = collections.defaultdict(
504 lambda: collections.defaultdict(list))
505 lambda: collections.defaultdict(list))
505 for co in inline_comments:
506 for co in inline_comments:
506 if c.at_version_num:
507 if c.at_version_num:
507 # pick comments that are at least UPTO given version, so we
508 # pick comments that are at least UPTO given version, so we
508 # don't render comments for higher version
509 # don't render comments for higher version
509 should_render = co.pull_request_version_id and \
510 should_render = co.pull_request_version_id and \
510 co.pull_request_version_id <= c.at_version_num
511 co.pull_request_version_id <= c.at_version_num
511 else:
512 else:
512 # showing all, for 'latest'
513 # showing all, for 'latest'
513 should_render = True
514 should_render = True
514
515
515 if should_render:
516 if should_render:
516 display_inline_comments[co.f_path][co.line_no].append(co)
517 display_inline_comments[co.f_path][co.line_no].append(co)
517
518
518 # load diff data into template context, if we use compare mode then
519 # load diff data into template context, if we use compare mode then
519 # diff is calculated based on changes between versions of PR
520 # diff is calculated based on changes between versions of PR
520
521
521 source_repo = pull_request_at_ver.source_repo
522 source_repo = pull_request_at_ver.source_repo
522 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
523 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
523
524
524 target_repo = pull_request_at_ver.target_repo
525 target_repo = pull_request_at_ver.target_repo
525 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
526 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
526
527
527 if compare:
528 if compare:
528 # in compare switch the diff base to latest commit from prev version
529 # in compare switch the diff base to latest commit from prev version
529 target_ref_id = prev_pull_request_display_obj.revisions[0]
530 target_ref_id = prev_pull_request_display_obj.revisions[0]
530
531
531 # despite opening commits for bookmarks/branches/tags, we always
532 # despite opening commits for bookmarks/branches/tags, we always
532 # convert this to rev to prevent changes after bookmark or branch change
533 # convert this to rev to prevent changes after bookmark or branch change
533 c.source_ref_type = 'rev'
534 c.source_ref_type = 'rev'
534 c.source_ref = source_ref_id
535 c.source_ref = source_ref_id
535
536
536 c.target_ref_type = 'rev'
537 c.target_ref_type = 'rev'
537 c.target_ref = target_ref_id
538 c.target_ref = target_ref_id
538
539
539 c.source_repo = source_repo
540 c.source_repo = source_repo
540 c.target_repo = target_repo
541 c.target_repo = target_repo
541
542
542 c.commit_ranges = []
543 c.commit_ranges = []
543 source_commit = EmptyCommit()
544 source_commit = EmptyCommit()
544 target_commit = EmptyCommit()
545 target_commit = EmptyCommit()
545 c.missing_requirements = False
546 c.missing_requirements = False
546
547
547 source_scm = source_repo.scm_instance()
548 source_scm = source_repo.scm_instance()
548 target_scm = target_repo.scm_instance()
549 target_scm = target_repo.scm_instance()
549
550
550 shadow_scm = None
551 shadow_scm = None
551 try:
552 try:
552 shadow_scm = pull_request_latest.get_shadow_repo()
553 shadow_scm = pull_request_latest.get_shadow_repo()
553 except Exception:
554 except Exception:
554 log.debug('Failed to get shadow repo', exc_info=True)
555 log.debug('Failed to get shadow repo', exc_info=True)
555 # try first the existing source_repo, and then shadow
556 # try first the existing source_repo, and then shadow
556 # repo if we can obtain one
557 # repo if we can obtain one
557 commits_source_repo = source_scm
558 commits_source_repo = source_scm
558 if shadow_scm:
559 if shadow_scm:
559 commits_source_repo = shadow_scm
560 commits_source_repo = shadow_scm
560
561
561 c.commits_source_repo = commits_source_repo
562 c.commits_source_repo = commits_source_repo
562 c.ancestor = None # set it to None, to hide it from PR view
563 c.ancestor = None # set it to None, to hide it from PR view
563
564
564 # empty version means latest, so we keep this to prevent
565 # empty version means latest, so we keep this to prevent
565 # double caching
566 # double caching
566 version_normalized = version or PullRequest.LATEST_VER
567 version_normalized = version or PullRequest.LATEST_VER
567 from_version_normalized = from_version or PullRequest.LATEST_VER
568 from_version_normalized = from_version or PullRequest.LATEST_VER
568
569
569 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
570 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
570 cache_file_path = diff_cache_exist(
571 cache_file_path = diff_cache_exist(
571 cache_path, 'pull_request', pull_request_id, version_normalized,
572 cache_path, 'pull_request', pull_request_id, version_normalized,
572 from_version_normalized, source_ref_id, target_ref_id,
573 from_version_normalized, source_ref_id, target_ref_id,
573 hide_whitespace_changes, diff_context, c.fulldiff)
574 hide_whitespace_changes, diff_context, c.fulldiff)
574
575
575 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
576 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
576 force_recache = self.get_recache_flag()
577 force_recache = self.get_recache_flag()
577
578
578 cached_diff = None
579 cached_diff = None
579 if caching_enabled:
580 if caching_enabled:
580 cached_diff = load_cached_diff(cache_file_path)
581 cached_diff = load_cached_diff(cache_file_path)
581
582
582 has_proper_commit_cache = (
583 has_proper_commit_cache = (
583 cached_diff and cached_diff.get('commits')
584 cached_diff and cached_diff.get('commits')
584 and len(cached_diff.get('commits', [])) == 5
585 and len(cached_diff.get('commits', [])) == 5
585 and cached_diff.get('commits')[0]
586 and cached_diff.get('commits')[0]
586 and cached_diff.get('commits')[3])
587 and cached_diff.get('commits')[3])
587
588
588 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
589 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
589 diff_commit_cache = \
590 diff_commit_cache = \
590 (ancestor_commit, commit_cache, missing_requirements,
591 (ancestor_commit, commit_cache, missing_requirements,
591 source_commit, target_commit) = cached_diff['commits']
592 source_commit, target_commit) = cached_diff['commits']
592 else:
593 else:
593 # NOTE(marcink): we reach potentially unreachable errors when a PR has
594 # NOTE(marcink): we reach potentially unreachable errors when a PR has
594 # merge errors resulting in potentially hidden commits in the shadow repo.
595 # merge errors resulting in potentially hidden commits in the shadow repo.
595 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
596 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
596 and _merge_check.merge_response
597 and _merge_check.merge_response
597 maybe_unreachable = maybe_unreachable \
598 maybe_unreachable = maybe_unreachable \
598 and _merge_check.merge_response.metadata.get('unresolved_files')
599 and _merge_check.merge_response.metadata.get('unresolved_files')
599 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
600 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
600 diff_commit_cache = \
601 diff_commit_cache = \
601 (ancestor_commit, commit_cache, missing_requirements,
602 (ancestor_commit, commit_cache, missing_requirements,
602 source_commit, target_commit) = self.get_commits(
603 source_commit, target_commit) = self.get_commits(
603 commits_source_repo,
604 commits_source_repo,
604 pull_request_at_ver,
605 pull_request_at_ver,
605 source_commit,
606 source_commit,
606 source_ref_id,
607 source_ref_id,
607 source_scm,
608 source_scm,
608 target_commit,
609 target_commit,
609 target_ref_id,
610 target_ref_id,
610 target_scm,
611 target_scm,
611 maybe_unreachable=maybe_unreachable)
612 maybe_unreachable=maybe_unreachable)
612
613
613 # register our commit range
614 # register our commit range
614 for comm in commit_cache.values():
615 for comm in commit_cache.values():
615 c.commit_ranges.append(comm)
616 c.commit_ranges.append(comm)
616
617
617 c.missing_requirements = missing_requirements
618 c.missing_requirements = missing_requirements
618 c.ancestor_commit = ancestor_commit
619 c.ancestor_commit = ancestor_commit
619 c.statuses = source_repo.statuses(
620 c.statuses = source_repo.statuses(
620 [x.raw_id for x in c.commit_ranges])
621 [x.raw_id for x in c.commit_ranges])
621
622
622 # auto collapse if we have more than limit
623 # auto collapse if we have more than limit
623 collapse_limit = diffs.DiffProcessor._collapse_commits_over
624 collapse_limit = diffs.DiffProcessor._collapse_commits_over
624 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
625 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
625 c.compare_mode = compare
626 c.compare_mode = compare
626
627
627 # diff_limit is the old behavior, will cut off the whole diff
628 # diff_limit is the old behavior, will cut off the whole diff
628 # if the limit is applied otherwise will just hide the
629 # if the limit is applied otherwise will just hide the
629 # big files from the front-end
630 # big files from the front-end
630 diff_limit = c.visual.cut_off_limit_diff
631 diff_limit = c.visual.cut_off_limit_diff
631 file_limit = c.visual.cut_off_limit_file
632 file_limit = c.visual.cut_off_limit_file
632
633
633 c.missing_commits = False
634 c.missing_commits = False
634 if (c.missing_requirements
635 if (c.missing_requirements
635 or isinstance(source_commit, EmptyCommit)
636 or isinstance(source_commit, EmptyCommit)
636 or source_commit == target_commit):
637 or source_commit == target_commit):
637
638
638 c.missing_commits = True
639 c.missing_commits = True
639 else:
640 else:
640 c.inline_comments = display_inline_comments
641 c.inline_comments = display_inline_comments
641
642
642 use_ancestor = True
643 use_ancestor = True
643 if from_version_normalized != version_normalized:
644 if from_version_normalized != version_normalized:
644 use_ancestor = False
645 use_ancestor = False
645
646
646 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
647 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
647 if not force_recache and has_proper_diff_cache:
648 if not force_recache and has_proper_diff_cache:
648 c.diffset = cached_diff['diff']
649 c.diffset = cached_diff['diff']
649 else:
650 else:
650 try:
651 try:
651 c.diffset = self._get_diffset(
652 c.diffset = self._get_diffset(
652 c.source_repo.repo_name, commits_source_repo,
653 c.source_repo.repo_name, commits_source_repo,
653 c.ancestor_commit,
654 c.ancestor_commit,
654 source_ref_id, target_ref_id,
655 source_ref_id, target_ref_id,
655 target_commit, source_commit,
656 target_commit, source_commit,
656 diff_limit, file_limit, c.fulldiff,
657 diff_limit, file_limit, c.fulldiff,
657 hide_whitespace_changes, diff_context,
658 hide_whitespace_changes, diff_context,
658 use_ancestor=use_ancestor
659 use_ancestor=use_ancestor
659 )
660 )
660
661
661 # save cached diff
662 # save cached diff
662 if caching_enabled:
663 if caching_enabled:
663 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
664 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
664 except CommitDoesNotExistError:
665 except CommitDoesNotExistError:
665 log.exception('Failed to generate diffset')
666 log.exception('Failed to generate diffset')
666 c.missing_commits = True
667 c.missing_commits = True
667
668
668 if not c.missing_commits:
669 if not c.missing_commits:
669
670
670 c.limited_diff = c.diffset.limited_diff
671 c.limited_diff = c.diffset.limited_diff
671
672
672 # calculate removed files that are bound to comments
673 # calculate removed files that are bound to comments
673 comment_deleted_files = [
674 comment_deleted_files = [
674 fname for fname in display_inline_comments
675 fname for fname in display_inline_comments
675 if fname not in c.diffset.file_stats]
676 if fname not in c.diffset.file_stats]
676
677
677 c.deleted_files_comments = collections.defaultdict(dict)
678 c.deleted_files_comments = collections.defaultdict(dict)
678 for fname, per_line_comments in display_inline_comments.items():
679 for fname, per_line_comments in display_inline_comments.items():
679 if fname in comment_deleted_files:
680 if fname in comment_deleted_files:
680 c.deleted_files_comments[fname]['stats'] = 0
681 c.deleted_files_comments[fname]['stats'] = 0
681 c.deleted_files_comments[fname]['comments'] = list()
682 c.deleted_files_comments[fname]['comments'] = list()
682 for lno, comments in per_line_comments.items():
683 for lno, comments in per_line_comments.items():
683 c.deleted_files_comments[fname]['comments'].extend(comments)
684 c.deleted_files_comments[fname]['comments'].extend(comments)
684
685
685 # maybe calculate the range diff
686 # maybe calculate the range diff
686 if c.range_diff_on:
687 if c.range_diff_on:
687 # TODO(marcink): set whitespace/context
688 # TODO(marcink): set whitespace/context
688 context_lcl = 3
689 context_lcl = 3
689 ign_whitespace_lcl = False
690 ign_whitespace_lcl = False
690
691
691 for commit in c.commit_ranges:
692 for commit in c.commit_ranges:
692 commit2 = commit
693 commit2 = commit
693 commit1 = commit.first_parent
694 commit1 = commit.first_parent
694
695
695 range_diff_cache_file_path = diff_cache_exist(
696 range_diff_cache_file_path = diff_cache_exist(
696 cache_path, 'diff', commit.raw_id,
697 cache_path, 'diff', commit.raw_id,
697 ign_whitespace_lcl, context_lcl, c.fulldiff)
698 ign_whitespace_lcl, context_lcl, c.fulldiff)
698
699
699 cached_diff = None
700 cached_diff = None
700 if caching_enabled:
701 if caching_enabled:
701 cached_diff = load_cached_diff(range_diff_cache_file_path)
702 cached_diff = load_cached_diff(range_diff_cache_file_path)
702
703
703 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
704 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
704 if not force_recache and has_proper_diff_cache:
705 if not force_recache and has_proper_diff_cache:
705 diffset = cached_diff['diff']
706 diffset = cached_diff['diff']
706 else:
707 else:
707 diffset = self._get_range_diffset(
708 diffset = self._get_range_diffset(
708 commits_source_repo, source_repo,
709 commits_source_repo, source_repo,
709 commit1, commit2, diff_limit, file_limit,
710 commit1, commit2, diff_limit, file_limit,
710 c.fulldiff, ign_whitespace_lcl, context_lcl
711 c.fulldiff, ign_whitespace_lcl, context_lcl
711 )
712 )
712
713
713 # save cached diff
714 # save cached diff
714 if caching_enabled:
715 if caching_enabled:
715 cache_diff(range_diff_cache_file_path, diffset, None)
716 cache_diff(range_diff_cache_file_path, diffset, None)
716
717
717 c.changes[commit.raw_id] = diffset
718 c.changes[commit.raw_id] = diffset
718
719
719 # this is a hack to properly display links, when creating PR, the
720 # this is a hack to properly display links, when creating PR, the
720 # compare view and others uses different notation, and
721 # compare view and others uses different notation, and
721 # compare_commits.mako renders links based on the target_repo.
722 # compare_commits.mako renders links based on the target_repo.
722 # We need to swap that here to generate it properly on the html side
723 # We need to swap that here to generate it properly on the html side
723 c.target_repo = c.source_repo
724 c.target_repo = c.source_repo
724
725
725 c.commit_statuses = ChangesetStatus.STATUSES
726 c.commit_statuses = ChangesetStatus.STATUSES
726
727
727 c.show_version_changes = not pr_closed
728 c.show_version_changes = not pr_closed
728 if c.show_version_changes:
729 if c.show_version_changes:
729 cur_obj = pull_request_at_ver
730 cur_obj = pull_request_at_ver
730 prev_obj = prev_pull_request_at_ver
731 prev_obj = prev_pull_request_at_ver
731
732
732 old_commit_ids = prev_obj.revisions
733 old_commit_ids = prev_obj.revisions
733 new_commit_ids = cur_obj.revisions
734 new_commit_ids = cur_obj.revisions
734 commit_changes = PullRequestModel()._calculate_commit_id_changes(
735 commit_changes = PullRequestModel()._calculate_commit_id_changes(
735 old_commit_ids, new_commit_ids)
736 old_commit_ids, new_commit_ids)
736 c.commit_changes_summary = commit_changes
737 c.commit_changes_summary = commit_changes
737
738
738 # calculate the diff for commits between versions
739 # calculate the diff for commits between versions
739 c.commit_changes = []
740 c.commit_changes = []
740
741
741 def mark(cs, fw):
742 def mark(cs, fw):
742 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
743 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
743
744
744 for c_type, raw_id in mark(commit_changes.added, 'a') \
745 for c_type, raw_id in mark(commit_changes.added, 'a') \
745 + mark(commit_changes.removed, 'r') \
746 + mark(commit_changes.removed, 'r') \
746 + mark(commit_changes.common, 'c'):
747 + mark(commit_changes.common, 'c'):
747
748
748 if raw_id in commit_cache:
749 if raw_id in commit_cache:
749 commit = commit_cache[raw_id]
750 commit = commit_cache[raw_id]
750 else:
751 else:
751 try:
752 try:
752 commit = commits_source_repo.get_commit(raw_id)
753 commit = commits_source_repo.get_commit(raw_id)
753 except CommitDoesNotExistError:
754 except CommitDoesNotExistError:
754 # in case we fail extracting still use "dummy" commit
755 # in case we fail extracting still use "dummy" commit
755 # for display in commit diff
756 # for display in commit diff
756 commit = h.AttributeDict(
757 commit = h.AttributeDict(
757 {'raw_id': raw_id,
758 {'raw_id': raw_id,
758 'message': 'EMPTY or MISSING COMMIT'})
759 'message': 'EMPTY or MISSING COMMIT'})
759 c.commit_changes.append([c_type, commit])
760 c.commit_changes.append([c_type, commit])
760
761
761 # current user review statuses for each version
762 # current user review statuses for each version
762 c.review_versions = {}
763 c.review_versions = {}
763 is_reviewer = PullRequestModel().is_user_reviewer(
764 is_reviewer = PullRequestModel().is_user_reviewer(
764 pull_request, self._rhodecode_user)
765 pull_request, self._rhodecode_user)
765 if is_reviewer:
766 if is_reviewer:
766 for co in general_comments:
767 for co in general_comments:
767 if co.author.user_id == self._rhodecode_user.user_id:
768 if co.author.user_id == self._rhodecode_user.user_id:
768 status = co.status_change
769 status = co.status_change
769 if status:
770 if status:
770 _ver_pr = status[0].comment.pull_request_version_id
771 _ver_pr = status[0].comment.pull_request_version_id
771 c.review_versions[_ver_pr] = status[0]
772 c.review_versions[_ver_pr] = status[0]
772
773
773 return self._get_template_context(c)
774 return self._get_template_context(c)
774
775
775 def get_commits(
776 def get_commits(
776 self, commits_source_repo, pull_request_at_ver, source_commit,
777 self, commits_source_repo, pull_request_at_ver, source_commit,
777 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
778 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
778 maybe_unreachable=False):
779 maybe_unreachable=False):
779
780
780 commit_cache = collections.OrderedDict()
781 commit_cache = collections.OrderedDict()
781 missing_requirements = False
782 missing_requirements = False
782
783
783 try:
784 try:
784 pre_load = ["author", "date", "message", "branch", "parents"]
785 pre_load = ["author", "date", "message", "branch", "parents"]
785
786
786 pull_request_commits = pull_request_at_ver.revisions
787 pull_request_commits = pull_request_at_ver.revisions
787 log.debug('Loading %s commits from %s',
788 log.debug('Loading %s commits from %s',
788 len(pull_request_commits), commits_source_repo)
789 len(pull_request_commits), commits_source_repo)
789
790
790 for rev in pull_request_commits:
791 for rev in pull_request_commits:
791 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
792 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
792 maybe_unreachable=maybe_unreachable)
793 maybe_unreachable=maybe_unreachable)
793 commit_cache[comm.raw_id] = comm
794 commit_cache[comm.raw_id] = comm
794
795
795 # Order here matters, we first need to get target, and then
796 # Order here matters, we first need to get target, and then
796 # the source
797 # the source
797 target_commit = commits_source_repo.get_commit(
798 target_commit = commits_source_repo.get_commit(
798 commit_id=safe_str(target_ref_id))
799 commit_id=safe_str(target_ref_id))
799
800
800 source_commit = commits_source_repo.get_commit(
801 source_commit = commits_source_repo.get_commit(
801 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
802 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
802 except CommitDoesNotExistError:
803 except CommitDoesNotExistError:
803 log.warning('Failed to get commit from `{}` repo'.format(
804 log.warning('Failed to get commit from `{}` repo'.format(
804 commits_source_repo), exc_info=True)
805 commits_source_repo), exc_info=True)
805 except RepositoryRequirementError:
806 except RepositoryRequirementError:
806 log.warning('Failed to get all required data from repo', exc_info=True)
807 log.warning('Failed to get all required data from repo', exc_info=True)
807 missing_requirements = True
808 missing_requirements = True
808
809
809 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
810 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
810
811
811 try:
812 try:
812 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
813 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
813 except Exception:
814 except Exception:
814 ancestor_commit = None
815 ancestor_commit = None
815
816
816 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
817 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
817
818
818 def assure_not_empty_repo(self):
819 def assure_not_empty_repo(self):
819 _ = self.request.translate
820 _ = self.request.translate
820
821
821 try:
822 try:
822 self.db_repo.scm_instance().get_commit()
823 self.db_repo.scm_instance().get_commit()
823 except EmptyRepositoryError:
824 except EmptyRepositoryError:
824 h.flash(h.literal(_('There are no commits yet')),
825 h.flash(h.literal(_('There are no commits yet')),
825 category='warning')
826 category='warning')
826 raise HTTPFound(
827 raise HTTPFound(
827 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
828 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
828
829
829 @LoginRequired()
830 @LoginRequired()
830 @NotAnonymous()
831 @NotAnonymous()
831 @HasRepoPermissionAnyDecorator(
832 @HasRepoPermissionAnyDecorator(
832 'repository.read', 'repository.write', 'repository.admin')
833 'repository.read', 'repository.write', 'repository.admin')
833 @view_config(
834 @view_config(
834 route_name='pullrequest_new', request_method='GET',
835 route_name='pullrequest_new', request_method='GET',
835 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
836 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
836 def pull_request_new(self):
837 def pull_request_new(self):
837 _ = self.request.translate
838 _ = self.request.translate
838 c = self.load_default_context()
839 c = self.load_default_context()
839
840
840 self.assure_not_empty_repo()
841 self.assure_not_empty_repo()
841 source_repo = self.db_repo
842 source_repo = self.db_repo
842
843
843 commit_id = self.request.GET.get('commit')
844 commit_id = self.request.GET.get('commit')
844 branch_ref = self.request.GET.get('branch')
845 branch_ref = self.request.GET.get('branch')
845 bookmark_ref = self.request.GET.get('bookmark')
846 bookmark_ref = self.request.GET.get('bookmark')
846
847
847 try:
848 try:
848 source_repo_data = PullRequestModel().generate_repo_data(
849 source_repo_data = PullRequestModel().generate_repo_data(
849 source_repo, commit_id=commit_id,
850 source_repo, commit_id=commit_id,
850 branch=branch_ref, bookmark=bookmark_ref,
851 branch=branch_ref, bookmark=bookmark_ref,
851 translator=self.request.translate)
852 translator=self.request.translate)
852 except CommitDoesNotExistError as e:
853 except CommitDoesNotExistError as e:
853 log.exception(e)
854 log.exception(e)
854 h.flash(_('Commit does not exist'), 'error')
855 h.flash(_('Commit does not exist'), 'error')
855 raise HTTPFound(
856 raise HTTPFound(
856 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
857 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
857
858
858 default_target_repo = source_repo
859 default_target_repo = source_repo
859
860
860 if source_repo.parent and c.has_origin_repo_read_perm:
861 if source_repo.parent and c.has_origin_repo_read_perm:
861 parent_vcs_obj = source_repo.parent.scm_instance()
862 parent_vcs_obj = source_repo.parent.scm_instance()
862 if parent_vcs_obj and not parent_vcs_obj.is_empty():
863 if parent_vcs_obj and not parent_vcs_obj.is_empty():
863 # change default if we have a parent repo
864 # change default if we have a parent repo
864 default_target_repo = source_repo.parent
865 default_target_repo = source_repo.parent
865
866
866 target_repo_data = PullRequestModel().generate_repo_data(
867 target_repo_data = PullRequestModel().generate_repo_data(
867 default_target_repo, translator=self.request.translate)
868 default_target_repo, translator=self.request.translate)
868
869
869 selected_source_ref = source_repo_data['refs']['selected_ref']
870 selected_source_ref = source_repo_data['refs']['selected_ref']
870 title_source_ref = ''
871 title_source_ref = ''
871 if selected_source_ref:
872 if selected_source_ref:
872 title_source_ref = selected_source_ref.split(':', 2)[1]
873 title_source_ref = selected_source_ref.split(':', 2)[1]
873 c.default_title = PullRequestModel().generate_pullrequest_title(
874 c.default_title = PullRequestModel().generate_pullrequest_title(
874 source=source_repo.repo_name,
875 source=source_repo.repo_name,
875 source_ref=title_source_ref,
876 source_ref=title_source_ref,
876 target=default_target_repo.repo_name
877 target=default_target_repo.repo_name
877 )
878 )
878
879
879 c.default_repo_data = {
880 c.default_repo_data = {
880 'source_repo_name': source_repo.repo_name,
881 'source_repo_name': source_repo.repo_name,
881 'source_refs_json': json.dumps(source_repo_data),
882 'source_refs_json': json.dumps(source_repo_data),
882 'target_repo_name': default_target_repo.repo_name,
883 'target_repo_name': default_target_repo.repo_name,
883 'target_refs_json': json.dumps(target_repo_data),
884 'target_refs_json': json.dumps(target_repo_data),
884 }
885 }
885 c.default_source_ref = selected_source_ref
886 c.default_source_ref = selected_source_ref
886
887
887 return self._get_template_context(c)
888 return self._get_template_context(c)
888
889
889 @LoginRequired()
890 @LoginRequired()
890 @NotAnonymous()
891 @NotAnonymous()
891 @HasRepoPermissionAnyDecorator(
892 @HasRepoPermissionAnyDecorator(
892 'repository.read', 'repository.write', 'repository.admin')
893 'repository.read', 'repository.write', 'repository.admin')
893 @view_config(
894 @view_config(
894 route_name='pullrequest_repo_refs', request_method='GET',
895 route_name='pullrequest_repo_refs', request_method='GET',
895 renderer='json_ext', xhr=True)
896 renderer='json_ext', xhr=True)
896 def pull_request_repo_refs(self):
897 def pull_request_repo_refs(self):
897 self.load_default_context()
898 self.load_default_context()
898 target_repo_name = self.request.matchdict['target_repo_name']
899 target_repo_name = self.request.matchdict['target_repo_name']
899 repo = Repository.get_by_repo_name(target_repo_name)
900 repo = Repository.get_by_repo_name(target_repo_name)
900 if not repo:
901 if not repo:
901 raise HTTPNotFound()
902 raise HTTPNotFound()
902
903
903 target_perm = HasRepoPermissionAny(
904 target_perm = HasRepoPermissionAny(
904 'repository.read', 'repository.write', 'repository.admin')(
905 'repository.read', 'repository.write', 'repository.admin')(
905 target_repo_name)
906 target_repo_name)
906 if not target_perm:
907 if not target_perm:
907 raise HTTPNotFound()
908 raise HTTPNotFound()
908
909
909 return PullRequestModel().generate_repo_data(
910 return PullRequestModel().generate_repo_data(
910 repo, translator=self.request.translate)
911 repo, translator=self.request.translate)
911
912
912 @LoginRequired()
913 @LoginRequired()
913 @NotAnonymous()
914 @NotAnonymous()
914 @HasRepoPermissionAnyDecorator(
915 @HasRepoPermissionAnyDecorator(
915 'repository.read', 'repository.write', 'repository.admin')
916 'repository.read', 'repository.write', 'repository.admin')
916 @view_config(
917 @view_config(
917 route_name='pullrequest_repo_targets', request_method='GET',
918 route_name='pullrequest_repo_targets', request_method='GET',
918 renderer='json_ext', xhr=True)
919 renderer='json_ext', xhr=True)
919 def pullrequest_repo_targets(self):
920 def pullrequest_repo_targets(self):
920 _ = self.request.translate
921 _ = self.request.translate
921 filter_query = self.request.GET.get('query')
922 filter_query = self.request.GET.get('query')
922
923
923 # get the parents
924 # get the parents
924 parent_target_repos = []
925 parent_target_repos = []
925 if self.db_repo.parent:
926 if self.db_repo.parent:
926 parents_query = Repository.query() \
927 parents_query = Repository.query() \
927 .order_by(func.length(Repository.repo_name)) \
928 .order_by(func.length(Repository.repo_name)) \
928 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
929 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
929
930
930 if filter_query:
931 if filter_query:
931 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
932 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
932 parents_query = parents_query.filter(
933 parents_query = parents_query.filter(
933 Repository.repo_name.ilike(ilike_expression))
934 Repository.repo_name.ilike(ilike_expression))
934 parents = parents_query.limit(20).all()
935 parents = parents_query.limit(20).all()
935
936
936 for parent in parents:
937 for parent in parents:
937 parent_vcs_obj = parent.scm_instance()
938 parent_vcs_obj = parent.scm_instance()
938 if parent_vcs_obj and not parent_vcs_obj.is_empty():
939 if parent_vcs_obj and not parent_vcs_obj.is_empty():
939 parent_target_repos.append(parent)
940 parent_target_repos.append(parent)
940
941
941 # get other forks, and repo itself
942 # get other forks, and repo itself
942 query = Repository.query() \
943 query = Repository.query() \
943 .order_by(func.length(Repository.repo_name)) \
944 .order_by(func.length(Repository.repo_name)) \
944 .filter(
945 .filter(
945 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
946 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
946 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
947 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
947 ) \
948 ) \
948 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
949 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
949
950
950 if filter_query:
951 if filter_query:
951 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
952 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
952 query = query.filter(Repository.repo_name.ilike(ilike_expression))
953 query = query.filter(Repository.repo_name.ilike(ilike_expression))
953
954
954 limit = max(20 - len(parent_target_repos), 5) # not less then 5
955 limit = max(20 - len(parent_target_repos), 5) # not less then 5
955 target_repos = query.limit(limit).all()
956 target_repos = query.limit(limit).all()
956
957
957 all_target_repos = target_repos + parent_target_repos
958 all_target_repos = target_repos + parent_target_repos
958
959
959 repos = []
960 repos = []
960 # This checks permissions to the repositories
961 # This checks permissions to the repositories
961 for obj in ScmModel().get_repos(all_target_repos):
962 for obj in ScmModel().get_repos(all_target_repos):
962 repos.append({
963 repos.append({
963 'id': obj['name'],
964 'id': obj['name'],
964 'text': obj['name'],
965 'text': obj['name'],
965 'type': 'repo',
966 'type': 'repo',
966 'repo_id': obj['dbrepo']['repo_id'],
967 'repo_id': obj['dbrepo']['repo_id'],
967 'repo_type': obj['dbrepo']['repo_type'],
968 'repo_type': obj['dbrepo']['repo_type'],
968 'private': obj['dbrepo']['private'],
969 'private': obj['dbrepo']['private'],
969
970
970 })
971 })
971
972
972 data = {
973 data = {
973 'more': False,
974 'more': False,
974 'results': [{
975 'results': [{
975 'text': _('Repositories'),
976 'text': _('Repositories'),
976 'children': repos
977 'children': repos
977 }] if repos else []
978 }] if repos else []
978 }
979 }
979 return data
980 return data
980
981
981 def _get_existing_ids(self, post_data):
982 def _get_existing_ids(self, post_data):
982 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
983 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
983
984
984 @LoginRequired()
985 @LoginRequired()
985 @NotAnonymous()
986 @NotAnonymous()
986 @HasRepoPermissionAnyDecorator(
987 @HasRepoPermissionAnyDecorator(
987 'repository.read', 'repository.write', 'repository.admin')
988 'repository.read', 'repository.write', 'repository.admin')
988 @view_config(
989 @view_config(
989 route_name='pullrequest_comments', request_method='POST',
990 route_name='pullrequest_comments', request_method='POST',
990 renderer='string_html', xhr=True)
991 renderer='string_html', xhr=True)
991 def pullrequest_comments(self):
992 def pullrequest_comments(self):
992 self.load_default_context()
993 self.load_default_context()
993
994
994 pull_request = PullRequest.get_or_404(
995 pull_request = PullRequest.get_or_404(
995 self.request.matchdict['pull_request_id'])
996 self.request.matchdict['pull_request_id'])
996 pull_request_id = pull_request.pull_request_id
997 pull_request_id = pull_request.pull_request_id
997 version = self.request.GET.get('version')
998 version = self.request.GET.get('version')
998
999
999 _render = self.request.get_partial_renderer(
1000 _render = self.request.get_partial_renderer(
1000 'rhodecode:templates/base/sidebar.mako')
1001 'rhodecode:templates/base/sidebar.mako')
1001 c = _render.get_call_context()
1002 c = _render.get_call_context()
1002
1003
1003 (pull_request_latest,
1004 (pull_request_latest,
1004 pull_request_at_ver,
1005 pull_request_at_ver,
1005 pull_request_display_obj,
1006 pull_request_display_obj,
1006 at_version) = PullRequestModel().get_pr_version(
1007 at_version) = PullRequestModel().get_pr_version(
1007 pull_request_id, version=version)
1008 pull_request_id, version=version)
1008 versions = pull_request_display_obj.versions()
1009 versions = pull_request_display_obj.versions()
1009 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1010 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1010 c.versions = versions + [latest_ver]
1011 c.versions = versions + [latest_ver]
1011
1012
1012 c.at_version = at_version
1013 c.at_version = at_version
1013 c.at_version_num = (at_version
1014 c.at_version_num = (at_version
1014 if at_version and at_version != PullRequest.LATEST_VER
1015 if at_version and at_version != PullRequest.LATEST_VER
1015 else None)
1016 else None)
1016
1017
1017 self.register_comments_vars(c, pull_request_latest, versions)
1018 self.register_comments_vars(c, pull_request_latest, versions)
1018 all_comments = c.inline_comments_flat + c.comments
1019 all_comments = c.inline_comments_flat + c.comments
1019
1020
1020 existing_ids = self._get_existing_ids(self.request.POST)
1021 existing_ids = self._get_existing_ids(self.request.POST)
1021 return _render('comments_table', all_comments, len(all_comments),
1022 return _render('comments_table', all_comments, len(all_comments),
1022 existing_ids=existing_ids)
1023 existing_ids=existing_ids)
1023
1024
1024 @LoginRequired()
1025 @LoginRequired()
1025 @NotAnonymous()
1026 @NotAnonymous()
1026 @HasRepoPermissionAnyDecorator(
1027 @HasRepoPermissionAnyDecorator(
1027 'repository.read', 'repository.write', 'repository.admin')
1028 'repository.read', 'repository.write', 'repository.admin')
1028 @view_config(
1029 @view_config(
1029 route_name='pullrequest_todos', request_method='POST',
1030 route_name='pullrequest_todos', request_method='POST',
1030 renderer='string_html', xhr=True)
1031 renderer='string_html', xhr=True)
1031 def pullrequest_todos(self):
1032 def pullrequest_todos(self):
1032 self.load_default_context()
1033 self.load_default_context()
1033
1034
1034 pull_request = PullRequest.get_or_404(
1035 pull_request = PullRequest.get_or_404(
1035 self.request.matchdict['pull_request_id'])
1036 self.request.matchdict['pull_request_id'])
1036 pull_request_id = pull_request.pull_request_id
1037 pull_request_id = pull_request.pull_request_id
1037 version = self.request.GET.get('version')
1038 version = self.request.GET.get('version')
1038
1039
1039 _render = self.request.get_partial_renderer(
1040 _render = self.request.get_partial_renderer(
1040 'rhodecode:templates/base/sidebar.mako')
1041 'rhodecode:templates/base/sidebar.mako')
1041 c = _render.get_call_context()
1042 c = _render.get_call_context()
1042 (pull_request_latest,
1043 (pull_request_latest,
1043 pull_request_at_ver,
1044 pull_request_at_ver,
1044 pull_request_display_obj,
1045 pull_request_display_obj,
1045 at_version) = PullRequestModel().get_pr_version(
1046 at_version) = PullRequestModel().get_pr_version(
1046 pull_request_id, version=version)
1047 pull_request_id, version=version)
1047 versions = pull_request_display_obj.versions()
1048 versions = pull_request_display_obj.versions()
1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1049 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1049 c.versions = versions + [latest_ver]
1050 c.versions = versions + [latest_ver]
1050
1051
1051 c.at_version = at_version
1052 c.at_version = at_version
1052 c.at_version_num = (at_version
1053 c.at_version_num = (at_version
1053 if at_version and at_version != PullRequest.LATEST_VER
1054 if at_version and at_version != PullRequest.LATEST_VER
1054 else None)
1055 else None)
1055
1056
1056 c.unresolved_comments = CommentsModel() \
1057 c.unresolved_comments = CommentsModel() \
1057 .get_pull_request_unresolved_todos(pull_request)
1058 .get_pull_request_unresolved_todos(pull_request)
1058 c.resolved_comments = CommentsModel() \
1059 c.resolved_comments = CommentsModel() \
1059 .get_pull_request_resolved_todos(pull_request)
1060 .get_pull_request_resolved_todos(pull_request)
1060
1061
1061 all_comments = c.unresolved_comments + c.resolved_comments
1062 all_comments = c.unresolved_comments + c.resolved_comments
1062 existing_ids = self._get_existing_ids(self.request.POST)
1063 existing_ids = self._get_existing_ids(self.request.POST)
1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 todo_comments=True, existing_ids=existing_ids)
1065 todo_comments=True, existing_ids=existing_ids)
1065
1066
1066 @LoginRequired()
1067 @LoginRequired()
1067 @NotAnonymous()
1068 @NotAnonymous()
1068 @HasRepoPermissionAnyDecorator(
1069 @HasRepoPermissionAnyDecorator(
1069 'repository.read', 'repository.write', 'repository.admin')
1070 'repository.read', 'repository.write', 'repository.admin')
1070 @CSRFRequired()
1071 @CSRFRequired()
1071 @view_config(
1072 @view_config(
1072 route_name='pullrequest_create', request_method='POST',
1073 route_name='pullrequest_create', request_method='POST',
1073 renderer=None)
1074 renderer=None)
1074 def pull_request_create(self):
1075 def pull_request_create(self):
1075 _ = self.request.translate
1076 _ = self.request.translate
1076 self.assure_not_empty_repo()
1077 self.assure_not_empty_repo()
1077 self.load_default_context()
1078 self.load_default_context()
1078
1079
1079 controls = peppercorn.parse(self.request.POST.items())
1080 controls = peppercorn.parse(self.request.POST.items())
1080
1081
1081 try:
1082 try:
1082 form = PullRequestForm(
1083 form = PullRequestForm(
1083 self.request.translate, self.db_repo.repo_id)()
1084 self.request.translate, self.db_repo.repo_id)()
1084 _form = form.to_python(controls)
1085 _form = form.to_python(controls)
1085 except formencode.Invalid as errors:
1086 except formencode.Invalid as errors:
1086 if errors.error_dict.get('revisions'):
1087 if errors.error_dict.get('revisions'):
1087 msg = 'Revisions: %s' % errors.error_dict['revisions']
1088 msg = 'Revisions: %s' % errors.error_dict['revisions']
1088 elif errors.error_dict.get('pullrequest_title'):
1089 elif errors.error_dict.get('pullrequest_title'):
1089 msg = errors.error_dict.get('pullrequest_title')
1090 msg = errors.error_dict.get('pullrequest_title')
1090 else:
1091 else:
1091 msg = _('Error creating pull request: {}').format(errors)
1092 msg = _('Error creating pull request: {}').format(errors)
1092 log.exception(msg)
1093 log.exception(msg)
1093 h.flash(msg, 'error')
1094 h.flash(msg, 'error')
1094
1095
1095 # would rather just go back to form ...
1096 # would rather just go back to form ...
1096 raise HTTPFound(
1097 raise HTTPFound(
1097 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1098 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1098
1099
1099 source_repo = _form['source_repo']
1100 source_repo = _form['source_repo']
1100 source_ref = _form['source_ref']
1101 source_ref = _form['source_ref']
1101 target_repo = _form['target_repo']
1102 target_repo = _form['target_repo']
1102 target_ref = _form['target_ref']
1103 target_ref = _form['target_ref']
1103 commit_ids = _form['revisions'][::-1]
1104 commit_ids = _form['revisions'][::-1]
1104 common_ancestor_id = _form['common_ancestor']
1105 common_ancestor_id = _form['common_ancestor']
1105
1106
1106 # find the ancestor for this pr
1107 # find the ancestor for this pr
1107 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1108 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1108 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1109 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1109
1110
1110 if not (source_db_repo or target_db_repo):
1111 if not (source_db_repo or target_db_repo):
1111 h.flash(_('source_repo or target repo not found'), category='error')
1112 h.flash(_('source_repo or target repo not found'), category='error')
1112 raise HTTPFound(
1113 raise HTTPFound(
1113 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1114 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1114
1115
1115 # re-check permissions again here
1116 # re-check permissions again here
1116 # source_repo we must have read permissions
1117 # source_repo we must have read permissions
1117
1118
1118 source_perm = HasRepoPermissionAny(
1119 source_perm = HasRepoPermissionAny(
1119 'repository.read', 'repository.write', 'repository.admin')(
1120 'repository.read', 'repository.write', 'repository.admin')(
1120 source_db_repo.repo_name)
1121 source_db_repo.repo_name)
1121 if not source_perm:
1122 if not source_perm:
1122 msg = _('Not Enough permissions to source repo `{}`.'.format(
1123 msg = _('Not Enough permissions to source repo `{}`.'.format(
1123 source_db_repo.repo_name))
1124 source_db_repo.repo_name))
1124 h.flash(msg, category='error')
1125 h.flash(msg, category='error')
1125 # copy the args back to redirect
1126 # copy the args back to redirect
1126 org_query = self.request.GET.mixed()
1127 org_query = self.request.GET.mixed()
1127 raise HTTPFound(
1128 raise HTTPFound(
1128 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1129 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1129 _query=org_query))
1130 _query=org_query))
1130
1131
1131 # target repo we must have read permissions, and also later on
1132 # target repo we must have read permissions, and also later on
1132 # we want to check branch permissions here
1133 # we want to check branch permissions here
1133 target_perm = HasRepoPermissionAny(
1134 target_perm = HasRepoPermissionAny(
1134 'repository.read', 'repository.write', 'repository.admin')(
1135 'repository.read', 'repository.write', 'repository.admin')(
1135 target_db_repo.repo_name)
1136 target_db_repo.repo_name)
1136 if not target_perm:
1137 if not target_perm:
1137 msg = _('Not Enough permissions to target repo `{}`.'.format(
1138 msg = _('Not Enough permissions to target repo `{}`.'.format(
1138 target_db_repo.repo_name))
1139 target_db_repo.repo_name))
1139 h.flash(msg, category='error')
1140 h.flash(msg, category='error')
1140 # copy the args back to redirect
1141 # copy the args back to redirect
1141 org_query = self.request.GET.mixed()
1142 org_query = self.request.GET.mixed()
1142 raise HTTPFound(
1143 raise HTTPFound(
1143 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1144 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1144 _query=org_query))
1145 _query=org_query))
1145
1146
1146 source_scm = source_db_repo.scm_instance()
1147 source_scm = source_db_repo.scm_instance()
1147 target_scm = target_db_repo.scm_instance()
1148 target_scm = target_db_repo.scm_instance()
1148
1149
1149 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1150 source_ref_obj = unicode_to_reference(source_ref)
1150 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1151 target_ref_obj = unicode_to_reference(target_ref)
1152
1153 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1154 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1151
1155
1152 ancestor = source_scm.get_common_ancestor(
1156 ancestor = source_scm.get_common_ancestor(
1153 source_commit.raw_id, target_commit.raw_id, target_scm)
1157 source_commit.raw_id, target_commit.raw_id, target_scm)
1154
1158
1155 source_ref_type, source_ref_name, source_commit_id = _form['target_ref'].split(':')
1156 target_ref_type, target_ref_name, target_commit_id = _form['source_ref'].split(':')
1157 # recalculate target ref based on ancestor
1159 # recalculate target ref based on ancestor
1158 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1160 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1159
1161
1160 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1162 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1161 PullRequestModel().get_reviewer_functions()
1163 PullRequestModel().get_reviewer_functions()
1162
1164
1163 # recalculate reviewers logic, to make sure we can validate this
1165 # recalculate reviewers logic, to make sure we can validate this
1164 reviewer_rules = get_default_reviewers_data(
1166 reviewer_rules = get_default_reviewers_data(
1165 self._rhodecode_db_user,
1167 self._rhodecode_db_user,
1166 source_db_repo,
1168 source_db_repo,
1167 Reference(source_ref_type, source_ref_name, source_commit_id),
1169 source_ref_obj,
1168 target_db_repo,
1170 target_db_repo,
1169 Reference(target_ref_type, target_ref_name, target_commit_id),
1171 target_ref_obj,
1170 include_diff_info=False)
1172 include_diff_info=False)
1171
1173
1172 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1174 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1173 observers = validate_observers(_form['observer_members'], reviewer_rules)
1175 observers = validate_observers(_form['observer_members'], reviewer_rules)
1174
1176
1175 pullrequest_title = _form['pullrequest_title']
1177 pullrequest_title = _form['pullrequest_title']
1176 title_source_ref = source_ref.split(':', 2)[1]
1178 title_source_ref = source_ref_obj.name
1177 if not pullrequest_title:
1179 if not pullrequest_title:
1178 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1180 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1179 source=source_repo,
1181 source=source_repo,
1180 source_ref=title_source_ref,
1182 source_ref=title_source_ref,
1181 target=target_repo
1183 target=target_repo
1182 )
1184 )
1183
1185
1184 description = _form['pullrequest_desc']
1186 description = _form['pullrequest_desc']
1185 description_renderer = _form['description_renderer']
1187 description_renderer = _form['description_renderer']
1186
1188
1187 try:
1189 try:
1188 pull_request = PullRequestModel().create(
1190 pull_request = PullRequestModel().create(
1189 created_by=self._rhodecode_user.user_id,
1191 created_by=self._rhodecode_user.user_id,
1190 source_repo=source_repo,
1192 source_repo=source_repo,
1191 source_ref=source_ref,
1193 source_ref=source_ref,
1192 target_repo=target_repo,
1194 target_repo=target_repo,
1193 target_ref=target_ref,
1195 target_ref=target_ref,
1194 revisions=commit_ids,
1196 revisions=commit_ids,
1195 common_ancestor_id=common_ancestor_id,
1197 common_ancestor_id=common_ancestor_id,
1196 reviewers=reviewers,
1198 reviewers=reviewers,
1197 observers=observers,
1199 observers=observers,
1198 title=pullrequest_title,
1200 title=pullrequest_title,
1199 description=description,
1201 description=description,
1200 description_renderer=description_renderer,
1202 description_renderer=description_renderer,
1201 reviewer_data=reviewer_rules,
1203 reviewer_data=reviewer_rules,
1202 auth_user=self._rhodecode_user
1204 auth_user=self._rhodecode_user
1203 )
1205 )
1204 Session().commit()
1206 Session().commit()
1205
1207
1206 h.flash(_('Successfully opened new pull request'),
1208 h.flash(_('Successfully opened new pull request'),
1207 category='success')
1209 category='success')
1208 except Exception:
1210 except Exception:
1209 msg = _('Error occurred during creation of this pull request.')
1211 msg = _('Error occurred during creation of this pull request.')
1210 log.exception(msg)
1212 log.exception(msg)
1211 h.flash(msg, category='error')
1213 h.flash(msg, category='error')
1212
1214
1213 # copy the args back to redirect
1215 # copy the args back to redirect
1214 org_query = self.request.GET.mixed()
1216 org_query = self.request.GET.mixed()
1215 raise HTTPFound(
1217 raise HTTPFound(
1216 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1218 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1217 _query=org_query))
1219 _query=org_query))
1218
1220
1219 raise HTTPFound(
1221 raise HTTPFound(
1220 h.route_path('pullrequest_show', repo_name=target_repo,
1222 h.route_path('pullrequest_show', repo_name=target_repo,
1221 pull_request_id=pull_request.pull_request_id))
1223 pull_request_id=pull_request.pull_request_id))
1222
1224
1223 @LoginRequired()
1225 @LoginRequired()
1224 @NotAnonymous()
1226 @NotAnonymous()
1225 @HasRepoPermissionAnyDecorator(
1227 @HasRepoPermissionAnyDecorator(
1226 'repository.read', 'repository.write', 'repository.admin')
1228 'repository.read', 'repository.write', 'repository.admin')
1227 @CSRFRequired()
1229 @CSRFRequired()
1228 @view_config(
1230 @view_config(
1229 route_name='pullrequest_update', request_method='POST',
1231 route_name='pullrequest_update', request_method='POST',
1230 renderer='json_ext')
1232 renderer='json_ext')
1231 def pull_request_update(self):
1233 def pull_request_update(self):
1232 pull_request = PullRequest.get_or_404(
1234 pull_request = PullRequest.get_or_404(
1233 self.request.matchdict['pull_request_id'])
1235 self.request.matchdict['pull_request_id'])
1234 _ = self.request.translate
1236 _ = self.request.translate
1235
1237
1236 c = self.load_default_context()
1238 c = self.load_default_context()
1237 redirect_url = None
1239 redirect_url = None
1238
1240
1239 if pull_request.is_closed():
1241 if pull_request.is_closed():
1240 log.debug('update: forbidden because pull request is closed')
1242 log.debug('update: forbidden because pull request is closed')
1241 msg = _(u'Cannot update closed pull requests.')
1243 msg = _(u'Cannot update closed pull requests.')
1242 h.flash(msg, category='error')
1244 h.flash(msg, category='error')
1243 return {'response': True,
1245 return {'response': True,
1244 'redirect_url': redirect_url}
1246 'redirect_url': redirect_url}
1245
1247
1246 is_state_changing = pull_request.is_state_changing()
1248 is_state_changing = pull_request.is_state_changing()
1247 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1249 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1248
1250
1249 # only owner or admin can update it
1251 # only owner or admin can update it
1250 allowed_to_update = PullRequestModel().check_user_update(
1252 allowed_to_update = PullRequestModel().check_user_update(
1251 pull_request, self._rhodecode_user)
1253 pull_request, self._rhodecode_user)
1252
1254
1253 if allowed_to_update:
1255 if allowed_to_update:
1254 controls = peppercorn.parse(self.request.POST.items())
1256 controls = peppercorn.parse(self.request.POST.items())
1255 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1257 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1256
1258
1257 if 'review_members' in controls:
1259 if 'review_members' in controls:
1258 self._update_reviewers(
1260 self._update_reviewers(
1259 c,
1261 c,
1260 pull_request, controls['review_members'],
1262 pull_request, controls['review_members'],
1261 pull_request.reviewer_data,
1263 pull_request.reviewer_data,
1262 PullRequestReviewers.ROLE_REVIEWER)
1264 PullRequestReviewers.ROLE_REVIEWER)
1263 elif 'observer_members' in controls:
1265 elif 'observer_members' in controls:
1264 self._update_reviewers(
1266 self._update_reviewers(
1265 c,
1267 c,
1266 pull_request, controls['observer_members'],
1268 pull_request, controls['observer_members'],
1267 pull_request.reviewer_data,
1269 pull_request.reviewer_data,
1268 PullRequestReviewers.ROLE_OBSERVER)
1270 PullRequestReviewers.ROLE_OBSERVER)
1269 elif str2bool(self.request.POST.get('update_commits', 'false')):
1271 elif str2bool(self.request.POST.get('update_commits', 'false')):
1270 if is_state_changing:
1272 if is_state_changing:
1271 log.debug('commits update: forbidden because pull request is in state %s',
1273 log.debug('commits update: forbidden because pull request is in state %s',
1272 pull_request.pull_request_state)
1274 pull_request.pull_request_state)
1273 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1275 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1274 u'Current state is: `{}`').format(
1276 u'Current state is: `{}`').format(
1275 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1277 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1276 h.flash(msg, category='error')
1278 h.flash(msg, category='error')
1277 return {'response': True,
1279 return {'response': True,
1278 'redirect_url': redirect_url}
1280 'redirect_url': redirect_url}
1279
1281
1280 self._update_commits(c, pull_request)
1282 self._update_commits(c, pull_request)
1281 if force_refresh:
1283 if force_refresh:
1282 redirect_url = h.route_path(
1284 redirect_url = h.route_path(
1283 'pullrequest_show', repo_name=self.db_repo_name,
1285 'pullrequest_show', repo_name=self.db_repo_name,
1284 pull_request_id=pull_request.pull_request_id,
1286 pull_request_id=pull_request.pull_request_id,
1285 _query={"force_refresh": 1})
1287 _query={"force_refresh": 1})
1286 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1288 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1287 self._edit_pull_request(pull_request)
1289 self._edit_pull_request(pull_request)
1288 else:
1290 else:
1289 log.error('Unhandled update data.')
1291 log.error('Unhandled update data.')
1290 raise HTTPBadRequest()
1292 raise HTTPBadRequest()
1291
1293
1292 return {'response': True,
1294 return {'response': True,
1293 'redirect_url': redirect_url}
1295 'redirect_url': redirect_url}
1294 raise HTTPForbidden()
1296 raise HTTPForbidden()
1295
1297
1296 def _edit_pull_request(self, pull_request):
1298 def _edit_pull_request(self, pull_request):
1297 """
1299 """
1298 Edit title and description
1300 Edit title and description
1299 """
1301 """
1300 _ = self.request.translate
1302 _ = self.request.translate
1301
1303
1302 try:
1304 try:
1303 PullRequestModel().edit(
1305 PullRequestModel().edit(
1304 pull_request,
1306 pull_request,
1305 self.request.POST.get('title'),
1307 self.request.POST.get('title'),
1306 self.request.POST.get('description'),
1308 self.request.POST.get('description'),
1307 self.request.POST.get('description_renderer'),
1309 self.request.POST.get('description_renderer'),
1308 self._rhodecode_user)
1310 self._rhodecode_user)
1309 except ValueError:
1311 except ValueError:
1310 msg = _(u'Cannot update closed pull requests.')
1312 msg = _(u'Cannot update closed pull requests.')
1311 h.flash(msg, category='error')
1313 h.flash(msg, category='error')
1312 return
1314 return
1313 else:
1315 else:
1314 Session().commit()
1316 Session().commit()
1315
1317
1316 msg = _(u'Pull request title & description updated.')
1318 msg = _(u'Pull request title & description updated.')
1317 h.flash(msg, category='success')
1319 h.flash(msg, category='success')
1318 return
1320 return
1319
1321
1320 def _update_commits(self, c, pull_request):
1322 def _update_commits(self, c, pull_request):
1321 _ = self.request.translate
1323 _ = self.request.translate
1322
1324
1323 with pull_request.set_state(PullRequest.STATE_UPDATING):
1325 with pull_request.set_state(PullRequest.STATE_UPDATING):
1324 resp = PullRequestModel().update_commits(
1326 resp = PullRequestModel().update_commits(
1325 pull_request, self._rhodecode_db_user)
1327 pull_request, self._rhodecode_db_user)
1326
1328
1327 if resp.executed:
1329 if resp.executed:
1328
1330
1329 if resp.target_changed and resp.source_changed:
1331 if resp.target_changed and resp.source_changed:
1330 changed = 'target and source repositories'
1332 changed = 'target and source repositories'
1331 elif resp.target_changed and not resp.source_changed:
1333 elif resp.target_changed and not resp.source_changed:
1332 changed = 'target repository'
1334 changed = 'target repository'
1333 elif not resp.target_changed and resp.source_changed:
1335 elif not resp.target_changed and resp.source_changed:
1334 changed = 'source repository'
1336 changed = 'source repository'
1335 else:
1337 else:
1336 changed = 'nothing'
1338 changed = 'nothing'
1337
1339
1338 msg = _(u'Pull request updated to "{source_commit_id}" with '
1340 msg = _(u'Pull request updated to "{source_commit_id}" with '
1339 u'{count_added} added, {count_removed} removed commits. '
1341 u'{count_added} added, {count_removed} removed commits. '
1340 u'Source of changes: {change_source}.')
1342 u'Source of changes: {change_source}.')
1341 msg = msg.format(
1343 msg = msg.format(
1342 source_commit_id=pull_request.source_ref_parts.commit_id,
1344 source_commit_id=pull_request.source_ref_parts.commit_id,
1343 count_added=len(resp.changes.added),
1345 count_added=len(resp.changes.added),
1344 count_removed=len(resp.changes.removed),
1346 count_removed=len(resp.changes.removed),
1345 change_source=changed)
1347 change_source=changed)
1346 h.flash(msg, category='success')
1348 h.flash(msg, category='success')
1347 channelstream.pr_update_channelstream_push(
1349 channelstream.pr_update_channelstream_push(
1348 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1350 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1349 else:
1351 else:
1350 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1352 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1351 warning_reasons = [
1353 warning_reasons = [
1352 UpdateFailureReason.NO_CHANGE,
1354 UpdateFailureReason.NO_CHANGE,
1353 UpdateFailureReason.WRONG_REF_TYPE,
1355 UpdateFailureReason.WRONG_REF_TYPE,
1354 ]
1356 ]
1355 category = 'warning' if resp.reason in warning_reasons else 'error'
1357 category = 'warning' if resp.reason in warning_reasons else 'error'
1356 h.flash(msg, category=category)
1358 h.flash(msg, category=category)
1357
1359
1358 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1360 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1359 _ = self.request.translate
1361 _ = self.request.translate
1360
1362
1361 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1363 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1362 PullRequestModel().get_reviewer_functions()
1364 PullRequestModel().get_reviewer_functions()
1363
1365
1364 if role == PullRequestReviewers.ROLE_REVIEWER:
1366 if role == PullRequestReviewers.ROLE_REVIEWER:
1365 try:
1367 try:
1366 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1368 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1367 except ValueError as e:
1369 except ValueError as e:
1368 log.error('Reviewers Validation: {}'.format(e))
1370 log.error('Reviewers Validation: {}'.format(e))
1369 h.flash(e, category='error')
1371 h.flash(e, category='error')
1370 return
1372 return
1371
1373
1372 old_calculated_status = pull_request.calculated_review_status()
1374 old_calculated_status = pull_request.calculated_review_status()
1373 PullRequestModel().update_reviewers(
1375 PullRequestModel().update_reviewers(
1374 pull_request, reviewers, self._rhodecode_user)
1376 pull_request, reviewers, self._rhodecode_db_user)
1375
1377
1376 Session().commit()
1378 Session().commit()
1377
1379
1378 msg = _('Pull request reviewers updated.')
1380 msg = _('Pull request reviewers updated.')
1379 h.flash(msg, category='success')
1381 h.flash(msg, category='success')
1380 channelstream.pr_update_channelstream_push(
1382 channelstream.pr_update_channelstream_push(
1381 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1383 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1382
1384
1383 # trigger status changed if change in reviewers changes the status
1385 # trigger status changed if change in reviewers changes the status
1384 calculated_status = pull_request.calculated_review_status()
1386 calculated_status = pull_request.calculated_review_status()
1385 if old_calculated_status != calculated_status:
1387 if old_calculated_status != calculated_status:
1386 PullRequestModel().trigger_pull_request_hook(
1388 PullRequestModel().trigger_pull_request_hook(
1387 pull_request, self._rhodecode_user, 'review_status_change',
1389 pull_request, self._rhodecode_user, 'review_status_change',
1388 data={'status': calculated_status})
1390 data={'status': calculated_status})
1389
1391
1390 elif role == PullRequestReviewers.ROLE_OBSERVER:
1392 elif role == PullRequestReviewers.ROLE_OBSERVER:
1391 try:
1393 try:
1392 observers = validate_observers(review_members, reviewer_rules)
1394 observers = validate_observers(review_members, reviewer_rules)
1393 except ValueError as e:
1395 except ValueError as e:
1394 log.error('Observers Validation: {}'.format(e))
1396 log.error('Observers Validation: {}'.format(e))
1395 h.flash(e, category='error')
1397 h.flash(e, category='error')
1396 return
1398 return
1397
1399
1398 PullRequestModel().update_observers(
1400 PullRequestModel().update_observers(
1399 pull_request, observers, self._rhodecode_user)
1401 pull_request, observers, self._rhodecode_db_user)
1400
1402
1401 Session().commit()
1403 Session().commit()
1402 msg = _('Pull request observers updated.')
1404 msg = _('Pull request observers updated.')
1403 h.flash(msg, category='success')
1405 h.flash(msg, category='success')
1404 channelstream.pr_update_channelstream_push(
1406 channelstream.pr_update_channelstream_push(
1405 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1406
1408
1407 @LoginRequired()
1409 @LoginRequired()
1408 @NotAnonymous()
1410 @NotAnonymous()
1409 @HasRepoPermissionAnyDecorator(
1411 @HasRepoPermissionAnyDecorator(
1410 'repository.read', 'repository.write', 'repository.admin')
1412 'repository.read', 'repository.write', 'repository.admin')
1411 @CSRFRequired()
1413 @CSRFRequired()
1412 @view_config(
1414 @view_config(
1413 route_name='pullrequest_merge', request_method='POST',
1415 route_name='pullrequest_merge', request_method='POST',
1414 renderer='json_ext')
1416 renderer='json_ext')
1415 def pull_request_merge(self):
1417 def pull_request_merge(self):
1416 """
1418 """
1417 Merge will perform a server-side merge of the specified
1419 Merge will perform a server-side merge of the specified
1418 pull request, if the pull request is approved and mergeable.
1420 pull request, if the pull request is approved and mergeable.
1419 After successful merging, the pull request is automatically
1421 After successful merging, the pull request is automatically
1420 closed, with a relevant comment.
1422 closed, with a relevant comment.
1421 """
1423 """
1422 pull_request = PullRequest.get_or_404(
1424 pull_request = PullRequest.get_or_404(
1423 self.request.matchdict['pull_request_id'])
1425 self.request.matchdict['pull_request_id'])
1424 _ = self.request.translate
1426 _ = self.request.translate
1425
1427
1426 if pull_request.is_state_changing():
1428 if pull_request.is_state_changing():
1427 log.debug('show: forbidden because pull request is in state %s',
1429 log.debug('show: forbidden because pull request is in state %s',
1428 pull_request.pull_request_state)
1430 pull_request.pull_request_state)
1429 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1431 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1430 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1432 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1431 pull_request.pull_request_state)
1433 pull_request.pull_request_state)
1432 h.flash(msg, category='error')
1434 h.flash(msg, category='error')
1433 raise HTTPFound(
1435 raise HTTPFound(
1434 h.route_path('pullrequest_show',
1436 h.route_path('pullrequest_show',
1435 repo_name=pull_request.target_repo.repo_name,
1437 repo_name=pull_request.target_repo.repo_name,
1436 pull_request_id=pull_request.pull_request_id))
1438 pull_request_id=pull_request.pull_request_id))
1437
1439
1438 self.load_default_context()
1440 self.load_default_context()
1439
1441
1440 with pull_request.set_state(PullRequest.STATE_UPDATING):
1442 with pull_request.set_state(PullRequest.STATE_UPDATING):
1441 check = MergeCheck.validate(
1443 check = MergeCheck.validate(
1442 pull_request, auth_user=self._rhodecode_user,
1444 pull_request, auth_user=self._rhodecode_user,
1443 translator=self.request.translate)
1445 translator=self.request.translate)
1444 merge_possible = not check.failed
1446 merge_possible = not check.failed
1445
1447
1446 for err_type, error_msg in check.errors:
1448 for err_type, error_msg in check.errors:
1447 h.flash(error_msg, category=err_type)
1449 h.flash(error_msg, category=err_type)
1448
1450
1449 if merge_possible:
1451 if merge_possible:
1450 log.debug("Pre-conditions checked, trying to merge.")
1452 log.debug("Pre-conditions checked, trying to merge.")
1451 extras = vcs_operation_context(
1453 extras = vcs_operation_context(
1452 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1454 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1453 username=self._rhodecode_db_user.username, action='push',
1455 username=self._rhodecode_db_user.username, action='push',
1454 scm=pull_request.target_repo.repo_type)
1456 scm=pull_request.target_repo.repo_type)
1455 with pull_request.set_state(PullRequest.STATE_UPDATING):
1457 with pull_request.set_state(PullRequest.STATE_UPDATING):
1456 self._merge_pull_request(
1458 self._merge_pull_request(
1457 pull_request, self._rhodecode_db_user, extras)
1459 pull_request, self._rhodecode_db_user, extras)
1458 else:
1460 else:
1459 log.debug("Pre-conditions failed, NOT merging.")
1461 log.debug("Pre-conditions failed, NOT merging.")
1460
1462
1461 raise HTTPFound(
1463 raise HTTPFound(
1462 h.route_path('pullrequest_show',
1464 h.route_path('pullrequest_show',
1463 repo_name=pull_request.target_repo.repo_name,
1465 repo_name=pull_request.target_repo.repo_name,
1464 pull_request_id=pull_request.pull_request_id))
1466 pull_request_id=pull_request.pull_request_id))
1465
1467
1466 def _merge_pull_request(self, pull_request, user, extras):
1468 def _merge_pull_request(self, pull_request, user, extras):
1467 _ = self.request.translate
1469 _ = self.request.translate
1468 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1470 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1469
1471
1470 if merge_resp.executed:
1472 if merge_resp.executed:
1471 log.debug("The merge was successful, closing the pull request.")
1473 log.debug("The merge was successful, closing the pull request.")
1472 PullRequestModel().close_pull_request(
1474 PullRequestModel().close_pull_request(
1473 pull_request.pull_request_id, user)
1475 pull_request.pull_request_id, user)
1474 Session().commit()
1476 Session().commit()
1475 msg = _('Pull request was successfully merged and closed.')
1477 msg = _('Pull request was successfully merged and closed.')
1476 h.flash(msg, category='success')
1478 h.flash(msg, category='success')
1477 else:
1479 else:
1478 log.debug(
1480 log.debug(
1479 "The merge was not successful. Merge response: %s", merge_resp)
1481 "The merge was not successful. Merge response: %s", merge_resp)
1480 msg = merge_resp.merge_status_message
1482 msg = merge_resp.merge_status_message
1481 h.flash(msg, category='error')
1483 h.flash(msg, category='error')
1482
1484
1483 @LoginRequired()
1485 @LoginRequired()
1484 @NotAnonymous()
1486 @NotAnonymous()
1485 @HasRepoPermissionAnyDecorator(
1487 @HasRepoPermissionAnyDecorator(
1486 'repository.read', 'repository.write', 'repository.admin')
1488 'repository.read', 'repository.write', 'repository.admin')
1487 @CSRFRequired()
1489 @CSRFRequired()
1488 @view_config(
1490 @view_config(
1489 route_name='pullrequest_delete', request_method='POST',
1491 route_name='pullrequest_delete', request_method='POST',
1490 renderer='json_ext')
1492 renderer='json_ext')
1491 def pull_request_delete(self):
1493 def pull_request_delete(self):
1492 _ = self.request.translate
1494 _ = self.request.translate
1493
1495
1494 pull_request = PullRequest.get_or_404(
1496 pull_request = PullRequest.get_or_404(
1495 self.request.matchdict['pull_request_id'])
1497 self.request.matchdict['pull_request_id'])
1496 self.load_default_context()
1498 self.load_default_context()
1497
1499
1498 pr_closed = pull_request.is_closed()
1500 pr_closed = pull_request.is_closed()
1499 allowed_to_delete = PullRequestModel().check_user_delete(
1501 allowed_to_delete = PullRequestModel().check_user_delete(
1500 pull_request, self._rhodecode_user) and not pr_closed
1502 pull_request, self._rhodecode_user) and not pr_closed
1501
1503
1502 # only owner can delete it !
1504 # only owner can delete it !
1503 if allowed_to_delete:
1505 if allowed_to_delete:
1504 PullRequestModel().delete(pull_request, self._rhodecode_user)
1506 PullRequestModel().delete(pull_request, self._rhodecode_user)
1505 Session().commit()
1507 Session().commit()
1506 h.flash(_('Successfully deleted pull request'),
1508 h.flash(_('Successfully deleted pull request'),
1507 category='success')
1509 category='success')
1508 raise HTTPFound(h.route_path('pullrequest_show_all',
1510 raise HTTPFound(h.route_path('pullrequest_show_all',
1509 repo_name=self.db_repo_name))
1511 repo_name=self.db_repo_name))
1510
1512
1511 log.warning('user %s tried to delete pull request without access',
1513 log.warning('user %s tried to delete pull request without access',
1512 self._rhodecode_user)
1514 self._rhodecode_user)
1513 raise HTTPNotFound()
1515 raise HTTPNotFound()
1514
1516
1515 @LoginRequired()
1517 @LoginRequired()
1516 @NotAnonymous()
1518 @NotAnonymous()
1517 @HasRepoPermissionAnyDecorator(
1519 @HasRepoPermissionAnyDecorator(
1518 'repository.read', 'repository.write', 'repository.admin')
1520 'repository.read', 'repository.write', 'repository.admin')
1519 @CSRFRequired()
1521 @CSRFRequired()
1520 @view_config(
1522 @view_config(
1521 route_name='pullrequest_comment_create', request_method='POST',
1523 route_name='pullrequest_comment_create', request_method='POST',
1522 renderer='json_ext')
1524 renderer='json_ext')
1523 def pull_request_comment_create(self):
1525 def pull_request_comment_create(self):
1524 _ = self.request.translate
1526 _ = self.request.translate
1525
1527
1526 pull_request = PullRequest.get_or_404(
1528 pull_request = PullRequest.get_or_404(
1527 self.request.matchdict['pull_request_id'])
1529 self.request.matchdict['pull_request_id'])
1528 pull_request_id = pull_request.pull_request_id
1530 pull_request_id = pull_request.pull_request_id
1529
1531
1530 if pull_request.is_closed():
1532 if pull_request.is_closed():
1531 log.debug('comment: forbidden because pull request is closed')
1533 log.debug('comment: forbidden because pull request is closed')
1532 raise HTTPForbidden()
1534 raise HTTPForbidden()
1533
1535
1534 allowed_to_comment = PullRequestModel().check_user_comment(
1536 allowed_to_comment = PullRequestModel().check_user_comment(
1535 pull_request, self._rhodecode_user)
1537 pull_request, self._rhodecode_user)
1536 if not allowed_to_comment:
1538 if not allowed_to_comment:
1537 log.debug('comment: forbidden because pull request is from forbidden repo')
1539 log.debug('comment: forbidden because pull request is from forbidden repo')
1538 raise HTTPForbidden()
1540 raise HTTPForbidden()
1539
1541
1540 c = self.load_default_context()
1542 c = self.load_default_context()
1541
1543
1542 status = self.request.POST.get('changeset_status', None)
1544 status = self.request.POST.get('changeset_status', None)
1543 text = self.request.POST.get('text')
1545 text = self.request.POST.get('text')
1544 comment_type = self.request.POST.get('comment_type')
1546 comment_type = self.request.POST.get('comment_type')
1545 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1547 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1546 close_pull_request = self.request.POST.get('close_pull_request')
1548 close_pull_request = self.request.POST.get('close_pull_request')
1547
1549
1548 # the logic here should work like following, if we submit close
1550 # the logic here should work like following, if we submit close
1549 # pr comment, use `close_pull_request_with_comment` function
1551 # pr comment, use `close_pull_request_with_comment` function
1550 # else handle regular comment logic
1552 # else handle regular comment logic
1551
1553
1552 if close_pull_request:
1554 if close_pull_request:
1553 # only owner or admin or person with write permissions
1555 # only owner or admin or person with write permissions
1554 allowed_to_close = PullRequestModel().check_user_update(
1556 allowed_to_close = PullRequestModel().check_user_update(
1555 pull_request, self._rhodecode_user)
1557 pull_request, self._rhodecode_user)
1556 if not allowed_to_close:
1558 if not allowed_to_close:
1557 log.debug('comment: forbidden because not allowed to close '
1559 log.debug('comment: forbidden because not allowed to close '
1558 'pull request %s', pull_request_id)
1560 'pull request %s', pull_request_id)
1559 raise HTTPForbidden()
1561 raise HTTPForbidden()
1560
1562
1561 # This also triggers `review_status_change`
1563 # This also triggers `review_status_change`
1562 comment, status = PullRequestModel().close_pull_request_with_comment(
1564 comment, status = PullRequestModel().close_pull_request_with_comment(
1563 pull_request, self._rhodecode_user, self.db_repo, message=text,
1565 pull_request, self._rhodecode_user, self.db_repo, message=text,
1564 auth_user=self._rhodecode_user)
1566 auth_user=self._rhodecode_user)
1565 Session().flush()
1567 Session().flush()
1568 is_inline = comment.is_inline
1566
1569
1567 PullRequestModel().trigger_pull_request_hook(
1570 PullRequestModel().trigger_pull_request_hook(
1568 pull_request, self._rhodecode_user, 'comment',
1571 pull_request, self._rhodecode_user, 'comment',
1569 data={'comment': comment})
1572 data={'comment': comment})
1570
1573
1571 else:
1574 else:
1572 # regular comment case, could be inline, or one with status.
1575 # regular comment case, could be inline, or one with status.
1573 # for that one we check also permissions
1576 # for that one we check also permissions
1574
1577
1575 allowed_to_change_status = PullRequestModel().check_user_change_status(
1578 allowed_to_change_status = PullRequestModel().check_user_change_status(
1576 pull_request, self._rhodecode_user)
1579 pull_request, self._rhodecode_user)
1577
1580
1578 if status and allowed_to_change_status:
1581 if status and allowed_to_change_status:
1579 message = (_('Status change %(transition_icon)s %(status)s')
1582 message = (_('Status change %(transition_icon)s %(status)s')
1580 % {'transition_icon': '>',
1583 % {'transition_icon': '>',
1581 'status': ChangesetStatus.get_status_lbl(status)})
1584 'status': ChangesetStatus.get_status_lbl(status)})
1582 text = text or message
1585 text = text or message
1583
1586
1584 comment = CommentsModel().create(
1587 comment = CommentsModel().create(
1585 text=text,
1588 text=text,
1586 repo=self.db_repo.repo_id,
1589 repo=self.db_repo.repo_id,
1587 user=self._rhodecode_user.user_id,
1590 user=self._rhodecode_user.user_id,
1588 pull_request=pull_request,
1591 pull_request=pull_request,
1589 f_path=self.request.POST.get('f_path'),
1592 f_path=self.request.POST.get('f_path'),
1590 line_no=self.request.POST.get('line'),
1593 line_no=self.request.POST.get('line'),
1591 status_change=(ChangesetStatus.get_status_lbl(status)
1594 status_change=(ChangesetStatus.get_status_lbl(status)
1592 if status and allowed_to_change_status else None),
1595 if status and allowed_to_change_status else None),
1593 status_change_type=(status
1596 status_change_type=(status
1594 if status and allowed_to_change_status else None),
1597 if status and allowed_to_change_status else None),
1595 comment_type=comment_type,
1598 comment_type=comment_type,
1596 resolves_comment_id=resolves_comment_id,
1599 resolves_comment_id=resolves_comment_id,
1597 auth_user=self._rhodecode_user
1600 auth_user=self._rhodecode_user
1598 )
1601 )
1599 is_inline = bool(comment.f_path and comment.line_no)
1602 is_inline = comment.is_inline
1600
1603
1601 if allowed_to_change_status:
1604 if allowed_to_change_status:
1602 # calculate old status before we change it
1605 # calculate old status before we change it
1603 old_calculated_status = pull_request.calculated_review_status()
1606 old_calculated_status = pull_request.calculated_review_status()
1604
1607
1605 # get status if set !
1608 # get status if set !
1606 if status:
1609 if status:
1607 ChangesetStatusModel().set_status(
1610 ChangesetStatusModel().set_status(
1608 self.db_repo.repo_id,
1611 self.db_repo.repo_id,
1609 status,
1612 status,
1610 self._rhodecode_user.user_id,
1613 self._rhodecode_user.user_id,
1611 comment,
1614 comment,
1612 pull_request=pull_request
1615 pull_request=pull_request
1613 )
1616 )
1614
1617
1615 Session().flush()
1618 Session().flush()
1616 # this is somehow required to get access to some relationship
1619 # this is somehow required to get access to some relationship
1617 # loaded on comment
1620 # loaded on comment
1618 Session().refresh(comment)
1621 Session().refresh(comment)
1619
1622
1620 PullRequestModel().trigger_pull_request_hook(
1623 PullRequestModel().trigger_pull_request_hook(
1621 pull_request, self._rhodecode_user, 'comment',
1624 pull_request, self._rhodecode_user, 'comment',
1622 data={'comment': comment})
1625 data={'comment': comment})
1623
1626
1624 # we now calculate the status of pull request, and based on that
1627 # we now calculate the status of pull request, and based on that
1625 # calculation we set the commits status
1628 # calculation we set the commits status
1626 calculated_status = pull_request.calculated_review_status()
1629 calculated_status = pull_request.calculated_review_status()
1627 if old_calculated_status != calculated_status:
1630 if old_calculated_status != calculated_status:
1628 PullRequestModel().trigger_pull_request_hook(
1631 PullRequestModel().trigger_pull_request_hook(
1629 pull_request, self._rhodecode_user, 'review_status_change',
1632 pull_request, self._rhodecode_user, 'review_status_change',
1630 data={'status': calculated_status})
1633 data={'status': calculated_status})
1631
1634
1632 Session().commit()
1635 Session().commit()
1633
1636
1634 data = {
1637 data = {
1635 'target_id': h.safeid(h.safe_unicode(
1638 'target_id': h.safeid(h.safe_unicode(
1636 self.request.POST.get('f_path'))),
1639 self.request.POST.get('f_path'))),
1637 }
1640 }
1638 if comment:
1641 if comment:
1639 c.co = comment
1642 c.co = comment
1640 c.at_version_num = None
1643 c.at_version_num = None
1641 rendered_comment = render(
1644 rendered_comment = render(
1642 'rhodecode:templates/changeset/changeset_comment_block.mako',
1645 'rhodecode:templates/changeset/changeset_comment_block.mako',
1643 self._get_template_context(c), self.request)
1646 self._get_template_context(c), self.request)
1644
1647
1645 data.update(comment.get_dict())
1648 data.update(comment.get_dict())
1646 data.update({'rendered_text': rendered_comment})
1649 data.update({'rendered_text': rendered_comment})
1647
1650
1648 comment_broadcast_channel = channelstream.comment_channel(
1651 comment_broadcast_channel = channelstream.comment_channel(
1649 self.db_repo_name, pull_request_obj=pull_request)
1652 self.db_repo_name, pull_request_obj=pull_request)
1650
1653
1651 comment_data = data
1654 comment_data = data
1652 comment_type = 'inline' if is_inline else 'general'
1655 comment_type = 'inline' if is_inline else 'general'
1653 channelstream.comment_channelstream_push(
1656 channelstream.comment_channelstream_push(
1654 self.request, comment_broadcast_channel, self._rhodecode_user,
1657 self.request, comment_broadcast_channel, self._rhodecode_user,
1655 _('posted a new {} comment').format(comment_type),
1658 _('posted a new {} comment').format(comment_type),
1656 comment_data=comment_data)
1659 comment_data=comment_data)
1657
1660
1658 return data
1661 return data
1659
1662
1660 @LoginRequired()
1663 @LoginRequired()
1661 @NotAnonymous()
1664 @NotAnonymous()
1662 @HasRepoPermissionAnyDecorator(
1665 @HasRepoPermissionAnyDecorator(
1663 'repository.read', 'repository.write', 'repository.admin')
1666 'repository.read', 'repository.write', 'repository.admin')
1664 @CSRFRequired()
1667 @CSRFRequired()
1665 @view_config(
1668 @view_config(
1666 route_name='pullrequest_comment_delete', request_method='POST',
1669 route_name='pullrequest_comment_delete', request_method='POST',
1667 renderer='json_ext')
1670 renderer='json_ext')
1668 def pull_request_comment_delete(self):
1671 def pull_request_comment_delete(self):
1669 pull_request = PullRequest.get_or_404(
1672 pull_request = PullRequest.get_or_404(
1670 self.request.matchdict['pull_request_id'])
1673 self.request.matchdict['pull_request_id'])
1671
1674
1672 comment = ChangesetComment.get_or_404(
1675 comment = ChangesetComment.get_or_404(
1673 self.request.matchdict['comment_id'])
1676 self.request.matchdict['comment_id'])
1674 comment_id = comment.comment_id
1677 comment_id = comment.comment_id
1675
1678
1676 if comment.immutable:
1679 if comment.immutable:
1677 # don't allow deleting comments that are immutable
1680 # don't allow deleting comments that are immutable
1678 raise HTTPForbidden()
1681 raise HTTPForbidden()
1679
1682
1680 if pull_request.is_closed():
1683 if pull_request.is_closed():
1681 log.debug('comment: forbidden because pull request is closed')
1684 log.debug('comment: forbidden because pull request is closed')
1682 raise HTTPForbidden()
1685 raise HTTPForbidden()
1683
1686
1684 if not comment:
1687 if not comment:
1685 log.debug('Comment with id:%s not found, skipping', comment_id)
1688 log.debug('Comment with id:%s not found, skipping', comment_id)
1686 # comment already deleted in another call probably
1689 # comment already deleted in another call probably
1687 return True
1690 return True
1688
1691
1689 if comment.pull_request.is_closed():
1692 if comment.pull_request.is_closed():
1690 # don't allow deleting comments on closed pull request
1693 # don't allow deleting comments on closed pull request
1691 raise HTTPForbidden()
1694 raise HTTPForbidden()
1692
1695
1693 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1696 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1694 super_admin = h.HasPermissionAny('hg.admin')()
1697 super_admin = h.HasPermissionAny('hg.admin')()
1695 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1698 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1696 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1699 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1697 comment_repo_admin = is_repo_admin and is_repo_comment
1700 comment_repo_admin = is_repo_admin and is_repo_comment
1698
1701
1699 if super_admin or comment_owner or comment_repo_admin:
1702 if super_admin or comment_owner or comment_repo_admin:
1700 old_calculated_status = comment.pull_request.calculated_review_status()
1703 old_calculated_status = comment.pull_request.calculated_review_status()
1701 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1704 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1702 Session().commit()
1705 Session().commit()
1703 calculated_status = comment.pull_request.calculated_review_status()
1706 calculated_status = comment.pull_request.calculated_review_status()
1704 if old_calculated_status != calculated_status:
1707 if old_calculated_status != calculated_status:
1705 PullRequestModel().trigger_pull_request_hook(
1708 PullRequestModel().trigger_pull_request_hook(
1706 comment.pull_request, self._rhodecode_user, 'review_status_change',
1709 comment.pull_request, self._rhodecode_user, 'review_status_change',
1707 data={'status': calculated_status})
1710 data={'status': calculated_status})
1708 return True
1711 return True
1709 else:
1712 else:
1710 log.warning('No permissions for user %s to delete comment_id: %s',
1713 log.warning('No permissions for user %s to delete comment_id: %s',
1711 self._rhodecode_db_user, comment_id)
1714 self._rhodecode_db_user, comment_id)
1712 raise HTTPNotFound()
1715 raise HTTPNotFound()
1713
1716
1714 @LoginRequired()
1717 @LoginRequired()
1715 @NotAnonymous()
1718 @NotAnonymous()
1716 @HasRepoPermissionAnyDecorator(
1719 @HasRepoPermissionAnyDecorator(
1717 'repository.read', 'repository.write', 'repository.admin')
1720 'repository.read', 'repository.write', 'repository.admin')
1718 @CSRFRequired()
1721 @CSRFRequired()
1719 @view_config(
1722 @view_config(
1720 route_name='pullrequest_comment_edit', request_method='POST',
1723 route_name='pullrequest_comment_edit', request_method='POST',
1721 renderer='json_ext')
1724 renderer='json_ext')
1722 def pull_request_comment_edit(self):
1725 def pull_request_comment_edit(self):
1723 self.load_default_context()
1726 self.load_default_context()
1724
1727
1725 pull_request = PullRequest.get_or_404(
1728 pull_request = PullRequest.get_or_404(
1726 self.request.matchdict['pull_request_id']
1729 self.request.matchdict['pull_request_id']
1727 )
1730 )
1728 comment = ChangesetComment.get_or_404(
1731 comment = ChangesetComment.get_or_404(
1729 self.request.matchdict['comment_id']
1732 self.request.matchdict['comment_id']
1730 )
1733 )
1731 comment_id = comment.comment_id
1734 comment_id = comment.comment_id
1732
1735
1733 if comment.immutable:
1736 if comment.immutable:
1734 # don't allow deleting comments that are immutable
1737 # don't allow deleting comments that are immutable
1735 raise HTTPForbidden()
1738 raise HTTPForbidden()
1736
1739
1737 if pull_request.is_closed():
1740 if pull_request.is_closed():
1738 log.debug('comment: forbidden because pull request is closed')
1741 log.debug('comment: forbidden because pull request is closed')
1739 raise HTTPForbidden()
1742 raise HTTPForbidden()
1740
1743
1741 if not comment:
1744 if not comment:
1742 log.debug('Comment with id:%s not found, skipping', comment_id)
1745 log.debug('Comment with id:%s not found, skipping', comment_id)
1743 # comment already deleted in another call probably
1746 # comment already deleted in another call probably
1744 return True
1747 return True
1745
1748
1746 if comment.pull_request.is_closed():
1749 if comment.pull_request.is_closed():
1747 # don't allow deleting comments on closed pull request
1750 # don't allow deleting comments on closed pull request
1748 raise HTTPForbidden()
1751 raise HTTPForbidden()
1749
1752
1750 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1753 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1751 super_admin = h.HasPermissionAny('hg.admin')()
1754 super_admin = h.HasPermissionAny('hg.admin')()
1752 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1755 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1753 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1756 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1754 comment_repo_admin = is_repo_admin and is_repo_comment
1757 comment_repo_admin = is_repo_admin and is_repo_comment
1755
1758
1756 if super_admin or comment_owner or comment_repo_admin:
1759 if super_admin or comment_owner or comment_repo_admin:
1757 text = self.request.POST.get('text')
1760 text = self.request.POST.get('text')
1758 version = self.request.POST.get('version')
1761 version = self.request.POST.get('version')
1759 if text == comment.text:
1762 if text == comment.text:
1760 log.warning(
1763 log.warning(
1761 'Comment(PR): '
1764 'Comment(PR): '
1762 'Trying to create new version '
1765 'Trying to create new version '
1763 'with the same comment body {}'.format(
1766 'with the same comment body {}'.format(
1764 comment_id,
1767 comment_id,
1765 )
1768 )
1766 )
1769 )
1767 raise HTTPNotFound()
1770 raise HTTPNotFound()
1768
1771
1769 if version.isdigit():
1772 if version.isdigit():
1770 version = int(version)
1773 version = int(version)
1771 else:
1774 else:
1772 log.warning(
1775 log.warning(
1773 'Comment(PR): Wrong version type {} {} '
1776 'Comment(PR): Wrong version type {} {} '
1774 'for comment {}'.format(
1777 'for comment {}'.format(
1775 version,
1778 version,
1776 type(version),
1779 type(version),
1777 comment_id,
1780 comment_id,
1778 )
1781 )
1779 )
1782 )
1780 raise HTTPNotFound()
1783 raise HTTPNotFound()
1781
1784
1782 try:
1785 try:
1783 comment_history = CommentsModel().edit(
1786 comment_history = CommentsModel().edit(
1784 comment_id=comment_id,
1787 comment_id=comment_id,
1785 text=text,
1788 text=text,
1786 auth_user=self._rhodecode_user,
1789 auth_user=self._rhodecode_user,
1787 version=version,
1790 version=version,
1788 )
1791 )
1789 except CommentVersionMismatch:
1792 except CommentVersionMismatch:
1790 raise HTTPConflict()
1793 raise HTTPConflict()
1791
1794
1792 if not comment_history:
1795 if not comment_history:
1793 raise HTTPNotFound()
1796 raise HTTPNotFound()
1794
1797
1795 Session().commit()
1798 Session().commit()
1796
1799
1797 PullRequestModel().trigger_pull_request_hook(
1800 PullRequestModel().trigger_pull_request_hook(
1798 pull_request, self._rhodecode_user, 'comment_edit',
1801 pull_request, self._rhodecode_user, 'comment_edit',
1799 data={'comment': comment})
1802 data={'comment': comment})
1800
1803
1801 return {
1804 return {
1802 'comment_history_id': comment_history.comment_history_id,
1805 'comment_history_id': comment_history.comment_history_id,
1803 'comment_id': comment.comment_id,
1806 'comment_id': comment.comment_id,
1804 'comment_version': comment_history.version,
1807 'comment_version': comment_history.version,
1805 'comment_author_username': comment_history.author.username,
1808 'comment_author_username': comment_history.author.username,
1806 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1809 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1807 'comment_created_on': h.age_component(comment_history.created_on,
1810 'comment_created_on': h.age_component(comment_history.created_on,
1808 time_is_local=True),
1811 time_is_local=True),
1809 }
1812 }
1810 else:
1813 else:
1811 log.warning('No permissions for user %s to edit comment_id: %s',
1814 log.warning('No permissions for user %s to edit comment_id: %s',
1812 self._rhodecode_db_user, comment_id)
1815 self._rhodecode_db_user, comment_id)
1813 raise HTTPNotFound()
1816 raise HTTPNotFound()
@@ -1,1925 +1,1948 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 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 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24 import os
24 import os
25 import re
25 import re
26 import time
26 import time
27 import shutil
27 import shutil
28 import datetime
28 import datetime
29 import fnmatch
29 import fnmatch
30 import itertools
30 import itertools
31 import logging
31 import logging
32 import collections
32 import collections
33 import warnings
33 import warnings
34
34
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36
36
37 from pyramid import compat
37 from pyramid import compat
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.translation import lazy_ugettext
40 from rhodecode.translation import lazy_ugettext
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
42 from rhodecode.lib.vcs import connection
42 from rhodecode.lib.vcs import connection
43 from rhodecode.lib.vcs.utils import author_name, author_email
43 from rhodecode.lib.vcs.utils import author_name, author_email
44 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.conf import settings
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
50 RepositoryError)
50 RepositoryError)
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 FILEMODE_DEFAULT = 0o100644
56 FILEMODE_DEFAULT = 0o100644
57 FILEMODE_EXECUTABLE = 0o100755
57 FILEMODE_EXECUTABLE = 0o100755
58 EMPTY_COMMIT_ID = '0' * 40
58 EMPTY_COMMIT_ID = '0' * 40
59
59
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
61
61
62
62
63 class Reference(_Reference):
63 class Reference(_Reference):
64
64
65 @property
65 @property
66 def branch(self):
66 def branch(self):
67 if self.type == 'branch':
67 if self.type == 'branch':
68 return self.name
68 return self.name
69
69
70 @property
70 @property
71 def bookmark(self):
71 def bookmark(self):
72 if self.type == 'book':
72 if self.type == 'book':
73 return self.name
73 return self.name
74
74
75
75
76 def unicode_to_reference(raw):
77 """
78 Convert a unicode (or string) to a reference object.
79 If unicode evaluates to False it returns None.
80 """
81 if raw:
82 refs = raw.split(':')
83 return Reference(*refs)
84 else:
85 return None
86
87
88 def reference_to_unicode(ref):
89 """
90 Convert a reference object to unicode.
91 If reference is None it returns None.
92 """
93 if ref:
94 return u':'.join(ref)
95 else:
96 return None
97
98
76 class MergeFailureReason(object):
99 class MergeFailureReason(object):
77 """
100 """
78 Enumeration with all the reasons why the server side merge could fail.
101 Enumeration with all the reasons why the server side merge could fail.
79
102
80 DO NOT change the number of the reasons, as they may be stored in the
103 DO NOT change the number of the reasons, as they may be stored in the
81 database.
104 database.
82
105
83 Changing the name of a reason is acceptable and encouraged to deprecate old
106 Changing the name of a reason is acceptable and encouraged to deprecate old
84 reasons.
107 reasons.
85 """
108 """
86
109
87 # Everything went well.
110 # Everything went well.
88 NONE = 0
111 NONE = 0
89
112
90 # An unexpected exception was raised. Check the logs for more details.
113 # An unexpected exception was raised. Check the logs for more details.
91 UNKNOWN = 1
114 UNKNOWN = 1
92
115
93 # The merge was not successful, there are conflicts.
116 # The merge was not successful, there are conflicts.
94 MERGE_FAILED = 2
117 MERGE_FAILED = 2
95
118
96 # The merge succeeded but we could not push it to the target repository.
119 # The merge succeeded but we could not push it to the target repository.
97 PUSH_FAILED = 3
120 PUSH_FAILED = 3
98
121
99 # The specified target is not a head in the target repository.
122 # The specified target is not a head in the target repository.
100 TARGET_IS_NOT_HEAD = 4
123 TARGET_IS_NOT_HEAD = 4
101
124
102 # The source repository contains more branches than the target. Pushing
125 # The source repository contains more branches than the target. Pushing
103 # the merge will create additional branches in the target.
126 # the merge will create additional branches in the target.
104 HG_SOURCE_HAS_MORE_BRANCHES = 5
127 HG_SOURCE_HAS_MORE_BRANCHES = 5
105
128
106 # The target reference has multiple heads. That does not allow to correctly
129 # The target reference has multiple heads. That does not allow to correctly
107 # identify the target location. This could only happen for mercurial
130 # identify the target location. This could only happen for mercurial
108 # branches.
131 # branches.
109 HG_TARGET_HAS_MULTIPLE_HEADS = 6
132 HG_TARGET_HAS_MULTIPLE_HEADS = 6
110
133
111 # The target repository is locked
134 # The target repository is locked
112 TARGET_IS_LOCKED = 7
135 TARGET_IS_LOCKED = 7
113
136
114 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
137 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
115 # A involved commit could not be found.
138 # A involved commit could not be found.
116 _DEPRECATED_MISSING_COMMIT = 8
139 _DEPRECATED_MISSING_COMMIT = 8
117
140
118 # The target repo reference is missing.
141 # The target repo reference is missing.
119 MISSING_TARGET_REF = 9
142 MISSING_TARGET_REF = 9
120
143
121 # The source repo reference is missing.
144 # The source repo reference is missing.
122 MISSING_SOURCE_REF = 10
145 MISSING_SOURCE_REF = 10
123
146
124 # The merge was not successful, there are conflicts related to sub
147 # The merge was not successful, there are conflicts related to sub
125 # repositories.
148 # repositories.
126 SUBREPO_MERGE_FAILED = 11
149 SUBREPO_MERGE_FAILED = 11
127
150
128
151
129 class UpdateFailureReason(object):
152 class UpdateFailureReason(object):
130 """
153 """
131 Enumeration with all the reasons why the pull request update could fail.
154 Enumeration with all the reasons why the pull request update could fail.
132
155
133 DO NOT change the number of the reasons, as they may be stored in the
156 DO NOT change the number of the reasons, as they may be stored in the
134 database.
157 database.
135
158
136 Changing the name of a reason is acceptable and encouraged to deprecate old
159 Changing the name of a reason is acceptable and encouraged to deprecate old
137 reasons.
160 reasons.
138 """
161 """
139
162
140 # Everything went well.
163 # Everything went well.
141 NONE = 0
164 NONE = 0
142
165
143 # An unexpected exception was raised. Check the logs for more details.
166 # An unexpected exception was raised. Check the logs for more details.
144 UNKNOWN = 1
167 UNKNOWN = 1
145
168
146 # The pull request is up to date.
169 # The pull request is up to date.
147 NO_CHANGE = 2
170 NO_CHANGE = 2
148
171
149 # The pull request has a reference type that is not supported for update.
172 # The pull request has a reference type that is not supported for update.
150 WRONG_REF_TYPE = 3
173 WRONG_REF_TYPE = 3
151
174
152 # Update failed because the target reference is missing.
175 # Update failed because the target reference is missing.
153 MISSING_TARGET_REF = 4
176 MISSING_TARGET_REF = 4
154
177
155 # Update failed because the source reference is missing.
178 # Update failed because the source reference is missing.
156 MISSING_SOURCE_REF = 5
179 MISSING_SOURCE_REF = 5
157
180
158
181
159 class MergeResponse(object):
182 class MergeResponse(object):
160
183
161 # uses .format(**metadata) for variables
184 # uses .format(**metadata) for variables
162 MERGE_STATUS_MESSAGES = {
185 MERGE_STATUS_MESSAGES = {
163 MergeFailureReason.NONE: lazy_ugettext(
186 MergeFailureReason.NONE: lazy_ugettext(
164 u'This pull request can be automatically merged.'),
187 u'This pull request can be automatically merged.'),
165 MergeFailureReason.UNKNOWN: lazy_ugettext(
188 MergeFailureReason.UNKNOWN: lazy_ugettext(
166 u'This pull request cannot be merged because of an unhandled exception. '
189 u'This pull request cannot be merged because of an unhandled exception. '
167 u'{exception}'),
190 u'{exception}'),
168 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
191 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
169 u'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
192 u'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
170 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
193 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
171 u'This pull request could not be merged because push to '
194 u'This pull request could not be merged because push to '
172 u'target:`{target}@{merge_commit}` failed.'),
195 u'target:`{target}@{merge_commit}` failed.'),
173 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
196 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
174 u'This pull request cannot be merged because the target '
197 u'This pull request cannot be merged because the target '
175 u'`{target_ref.name}` is not a head.'),
198 u'`{target_ref.name}` is not a head.'),
176 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
199 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
177 u'This pull request cannot be merged because the source contains '
200 u'This pull request cannot be merged because the source contains '
178 u'more branches than the target.'),
201 u'more branches than the target.'),
179 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
202 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
180 u'This pull request cannot be merged because the target `{target_ref.name}` '
203 u'This pull request cannot be merged because the target `{target_ref.name}` '
181 u'has multiple heads: `{heads}`.'),
204 u'has multiple heads: `{heads}`.'),
182 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
205 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
183 u'This pull request cannot be merged because the target repository is '
206 u'This pull request cannot be merged because the target repository is '
184 u'locked by {locked_by}.'),
207 u'locked by {locked_by}.'),
185
208
186 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
209 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
187 u'This pull request cannot be merged because the target '
210 u'This pull request cannot be merged because the target '
188 u'reference `{target_ref.name}` is missing.'),
211 u'reference `{target_ref.name}` is missing.'),
189 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
212 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
190 u'This pull request cannot be merged because the source '
213 u'This pull request cannot be merged because the source '
191 u'reference `{source_ref.name}` is missing.'),
214 u'reference `{source_ref.name}` is missing.'),
192 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
215 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
193 u'This pull request cannot be merged because of conflicts related '
216 u'This pull request cannot be merged because of conflicts related '
194 u'to sub repositories.'),
217 u'to sub repositories.'),
195
218
196 # Deprecations
219 # Deprecations
197 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
220 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
198 u'This pull request cannot be merged because the target or the '
221 u'This pull request cannot be merged because the target or the '
199 u'source reference is missing.'),
222 u'source reference is missing.'),
200
223
201 }
224 }
202
225
203 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
226 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
204 self.possible = possible
227 self.possible = possible
205 self.executed = executed
228 self.executed = executed
206 self.merge_ref = merge_ref
229 self.merge_ref = merge_ref
207 self.failure_reason = failure_reason
230 self.failure_reason = failure_reason
208 self.metadata = metadata or {}
231 self.metadata = metadata or {}
209
232
210 def __repr__(self):
233 def __repr__(self):
211 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
234 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
212
235
213 def __eq__(self, other):
236 def __eq__(self, other):
214 same_instance = isinstance(other, self.__class__)
237 same_instance = isinstance(other, self.__class__)
215 return same_instance \
238 return same_instance \
216 and self.possible == other.possible \
239 and self.possible == other.possible \
217 and self.executed == other.executed \
240 and self.executed == other.executed \
218 and self.failure_reason == other.failure_reason
241 and self.failure_reason == other.failure_reason
219
242
220 @property
243 @property
221 def label(self):
244 def label(self):
222 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
245 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
223 not k.startswith('_'))
246 not k.startswith('_'))
224 return label_dict.get(self.failure_reason)
247 return label_dict.get(self.failure_reason)
225
248
226 @property
249 @property
227 def merge_status_message(self):
250 def merge_status_message(self):
228 """
251 """
229 Return a human friendly error message for the given merge status code.
252 Return a human friendly error message for the given merge status code.
230 """
253 """
231 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
254 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
232
255
233 try:
256 try:
234 return msg.format(**self.metadata)
257 return msg.format(**self.metadata)
235 except Exception:
258 except Exception:
236 log.exception('Failed to format %s message', self)
259 log.exception('Failed to format %s message', self)
237 return msg
260 return msg
238
261
239 def asdict(self):
262 def asdict(self):
240 data = {}
263 data = {}
241 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
264 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
242 'merge_status_message']:
265 'merge_status_message']:
243 data[k] = getattr(self, k)
266 data[k] = getattr(self, k)
244 return data
267 return data
245
268
246
269
247 class TargetRefMissing(ValueError):
270 class TargetRefMissing(ValueError):
248 pass
271 pass
249
272
250
273
251 class SourceRefMissing(ValueError):
274 class SourceRefMissing(ValueError):
252 pass
275 pass
253
276
254
277
255 class BaseRepository(object):
278 class BaseRepository(object):
256 """
279 """
257 Base Repository for final backends
280 Base Repository for final backends
258
281
259 .. attribute:: DEFAULT_BRANCH_NAME
282 .. attribute:: DEFAULT_BRANCH_NAME
260
283
261 name of default branch (i.e. "trunk" for svn, "master" for git etc.
284 name of default branch (i.e. "trunk" for svn, "master" for git etc.
262
285
263 .. attribute:: commit_ids
286 .. attribute:: commit_ids
264
287
265 list of all available commit ids, in ascending order
288 list of all available commit ids, in ascending order
266
289
267 .. attribute:: path
290 .. attribute:: path
268
291
269 absolute path to the repository
292 absolute path to the repository
270
293
271 .. attribute:: bookmarks
294 .. attribute:: bookmarks
272
295
273 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
296 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
274 there are no bookmarks or the backend implementation does not support
297 there are no bookmarks or the backend implementation does not support
275 bookmarks.
298 bookmarks.
276
299
277 .. attribute:: tags
300 .. attribute:: tags
278
301
279 Mapping from name to :term:`Commit ID` of the tag.
302 Mapping from name to :term:`Commit ID` of the tag.
280
303
281 """
304 """
282
305
283 DEFAULT_BRANCH_NAME = None
306 DEFAULT_BRANCH_NAME = None
284 DEFAULT_CONTACT = u"Unknown"
307 DEFAULT_CONTACT = u"Unknown"
285 DEFAULT_DESCRIPTION = u"unknown"
308 DEFAULT_DESCRIPTION = u"unknown"
286 EMPTY_COMMIT_ID = '0' * 40
309 EMPTY_COMMIT_ID = '0' * 40
287
310
288 path = None
311 path = None
289
312
290 _is_empty = None
313 _is_empty = None
291 _commit_ids = {}
314 _commit_ids = {}
292
315
293 def __init__(self, repo_path, config=None, create=False, **kwargs):
316 def __init__(self, repo_path, config=None, create=False, **kwargs):
294 """
317 """
295 Initializes repository. Raises RepositoryError if repository could
318 Initializes repository. Raises RepositoryError if repository could
296 not be find at the given ``repo_path`` or directory at ``repo_path``
319 not be find at the given ``repo_path`` or directory at ``repo_path``
297 exists and ``create`` is set to True.
320 exists and ``create`` is set to True.
298
321
299 :param repo_path: local path of the repository
322 :param repo_path: local path of the repository
300 :param config: repository configuration
323 :param config: repository configuration
301 :param create=False: if set to True, would try to create repository.
324 :param create=False: if set to True, would try to create repository.
302 :param src_url=None: if set, should be proper url from which repository
325 :param src_url=None: if set, should be proper url from which repository
303 would be cloned; requires ``create`` parameter to be set to True -
326 would be cloned; requires ``create`` parameter to be set to True -
304 raises RepositoryError if src_url is set and create evaluates to
327 raises RepositoryError if src_url is set and create evaluates to
305 False
328 False
306 """
329 """
307 raise NotImplementedError
330 raise NotImplementedError
308
331
309 def __repr__(self):
332 def __repr__(self):
310 return '<%s at %s>' % (self.__class__.__name__, self.path)
333 return '<%s at %s>' % (self.__class__.__name__, self.path)
311
334
312 def __len__(self):
335 def __len__(self):
313 return self.count()
336 return self.count()
314
337
315 def __eq__(self, other):
338 def __eq__(self, other):
316 same_instance = isinstance(other, self.__class__)
339 same_instance = isinstance(other, self.__class__)
317 return same_instance and other.path == self.path
340 return same_instance and other.path == self.path
318
341
319 def __ne__(self, other):
342 def __ne__(self, other):
320 return not self.__eq__(other)
343 return not self.__eq__(other)
321
344
322 def get_create_shadow_cache_pr_path(self, db_repo):
345 def get_create_shadow_cache_pr_path(self, db_repo):
323 path = db_repo.cached_diffs_dir
346 path = db_repo.cached_diffs_dir
324 if not os.path.exists(path):
347 if not os.path.exists(path):
325 os.makedirs(path, 0o755)
348 os.makedirs(path, 0o755)
326 return path
349 return path
327
350
328 @classmethod
351 @classmethod
329 def get_default_config(cls, default=None):
352 def get_default_config(cls, default=None):
330 config = Config()
353 config = Config()
331 if default and isinstance(default, list):
354 if default and isinstance(default, list):
332 for section, key, val in default:
355 for section, key, val in default:
333 config.set(section, key, val)
356 config.set(section, key, val)
334 return config
357 return config
335
358
336 @LazyProperty
359 @LazyProperty
337 def _remote(self):
360 def _remote(self):
338 raise NotImplementedError
361 raise NotImplementedError
339
362
340 def _heads(self, branch=None):
363 def _heads(self, branch=None):
341 return []
364 return []
342
365
343 @LazyProperty
366 @LazyProperty
344 def EMPTY_COMMIT(self):
367 def EMPTY_COMMIT(self):
345 return EmptyCommit(self.EMPTY_COMMIT_ID)
368 return EmptyCommit(self.EMPTY_COMMIT_ID)
346
369
347 @LazyProperty
370 @LazyProperty
348 def alias(self):
371 def alias(self):
349 for k, v in settings.BACKENDS.items():
372 for k, v in settings.BACKENDS.items():
350 if v.split('.')[-1] == str(self.__class__.__name__):
373 if v.split('.')[-1] == str(self.__class__.__name__):
351 return k
374 return k
352
375
353 @LazyProperty
376 @LazyProperty
354 def name(self):
377 def name(self):
355 return safe_unicode(os.path.basename(self.path))
378 return safe_unicode(os.path.basename(self.path))
356
379
357 @LazyProperty
380 @LazyProperty
358 def description(self):
381 def description(self):
359 raise NotImplementedError
382 raise NotImplementedError
360
383
361 def refs(self):
384 def refs(self):
362 """
385 """
363 returns a `dict` with branches, bookmarks, tags, and closed_branches
386 returns a `dict` with branches, bookmarks, tags, and closed_branches
364 for this repository
387 for this repository
365 """
388 """
366 return dict(
389 return dict(
367 branches=self.branches,
390 branches=self.branches,
368 branches_closed=self.branches_closed,
391 branches_closed=self.branches_closed,
369 tags=self.tags,
392 tags=self.tags,
370 bookmarks=self.bookmarks
393 bookmarks=self.bookmarks
371 )
394 )
372
395
373 @LazyProperty
396 @LazyProperty
374 def branches(self):
397 def branches(self):
375 """
398 """
376 A `dict` which maps branch names to commit ids.
399 A `dict` which maps branch names to commit ids.
377 """
400 """
378 raise NotImplementedError
401 raise NotImplementedError
379
402
380 @LazyProperty
403 @LazyProperty
381 def branches_closed(self):
404 def branches_closed(self):
382 """
405 """
383 A `dict` which maps tags names to commit ids.
406 A `dict` which maps tags names to commit ids.
384 """
407 """
385 raise NotImplementedError
408 raise NotImplementedError
386
409
387 @LazyProperty
410 @LazyProperty
388 def bookmarks(self):
411 def bookmarks(self):
389 """
412 """
390 A `dict` which maps tags names to commit ids.
413 A `dict` which maps tags names to commit ids.
391 """
414 """
392 raise NotImplementedError
415 raise NotImplementedError
393
416
394 @LazyProperty
417 @LazyProperty
395 def tags(self):
418 def tags(self):
396 """
419 """
397 A `dict` which maps tags names to commit ids.
420 A `dict` which maps tags names to commit ids.
398 """
421 """
399 raise NotImplementedError
422 raise NotImplementedError
400
423
401 @LazyProperty
424 @LazyProperty
402 def size(self):
425 def size(self):
403 """
426 """
404 Returns combined size in bytes for all repository files
427 Returns combined size in bytes for all repository files
405 """
428 """
406 tip = self.get_commit()
429 tip = self.get_commit()
407 return tip.size
430 return tip.size
408
431
409 def size_at_commit(self, commit_id):
432 def size_at_commit(self, commit_id):
410 commit = self.get_commit(commit_id)
433 commit = self.get_commit(commit_id)
411 return commit.size
434 return commit.size
412
435
413 def _check_for_empty(self):
436 def _check_for_empty(self):
414 no_commits = len(self._commit_ids) == 0
437 no_commits = len(self._commit_ids) == 0
415 if no_commits:
438 if no_commits:
416 # check on remote to be sure
439 # check on remote to be sure
417 return self._remote.is_empty()
440 return self._remote.is_empty()
418 else:
441 else:
419 return False
442 return False
420
443
421 def is_empty(self):
444 def is_empty(self):
422 if rhodecode.is_test:
445 if rhodecode.is_test:
423 return self._check_for_empty()
446 return self._check_for_empty()
424
447
425 if self._is_empty is None:
448 if self._is_empty is None:
426 # cache empty for production, but not tests
449 # cache empty for production, but not tests
427 self._is_empty = self._check_for_empty()
450 self._is_empty = self._check_for_empty()
428
451
429 return self._is_empty
452 return self._is_empty
430
453
431 @staticmethod
454 @staticmethod
432 def check_url(url, config):
455 def check_url(url, config):
433 """
456 """
434 Function will check given url and try to verify if it's a valid
457 Function will check given url and try to verify if it's a valid
435 link.
458 link.
436 """
459 """
437 raise NotImplementedError
460 raise NotImplementedError
438
461
439 @staticmethod
462 @staticmethod
440 def is_valid_repository(path):
463 def is_valid_repository(path):
441 """
464 """
442 Check if given `path` contains a valid repository of this backend
465 Check if given `path` contains a valid repository of this backend
443 """
466 """
444 raise NotImplementedError
467 raise NotImplementedError
445
468
446 # ==========================================================================
469 # ==========================================================================
447 # COMMITS
470 # COMMITS
448 # ==========================================================================
471 # ==========================================================================
449
472
450 @CachedProperty
473 @CachedProperty
451 def commit_ids(self):
474 def commit_ids(self):
452 raise NotImplementedError
475 raise NotImplementedError
453
476
454 def append_commit_id(self, commit_id):
477 def append_commit_id(self, commit_id):
455 if commit_id not in self.commit_ids:
478 if commit_id not in self.commit_ids:
456 self._rebuild_cache(self.commit_ids + [commit_id])
479 self._rebuild_cache(self.commit_ids + [commit_id])
457
480
458 # clear cache
481 # clear cache
459 self._invalidate_prop_cache('commit_ids')
482 self._invalidate_prop_cache('commit_ids')
460 self._is_empty = False
483 self._is_empty = False
461
484
462 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
485 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
463 translate_tag=None, maybe_unreachable=False):
486 translate_tag=None, maybe_unreachable=False):
464 """
487 """
465 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
488 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
466 are both None, most recent commit is returned.
489 are both None, most recent commit is returned.
467
490
468 :param pre_load: Optional. List of commit attributes to load.
491 :param pre_load: Optional. List of commit attributes to load.
469
492
470 :raises ``EmptyRepositoryError``: if there are no commits
493 :raises ``EmptyRepositoryError``: if there are no commits
471 """
494 """
472 raise NotImplementedError
495 raise NotImplementedError
473
496
474 def __iter__(self):
497 def __iter__(self):
475 for commit_id in self.commit_ids:
498 for commit_id in self.commit_ids:
476 yield self.get_commit(commit_id=commit_id)
499 yield self.get_commit(commit_id=commit_id)
477
500
478 def get_commits(
501 def get_commits(
479 self, start_id=None, end_id=None, start_date=None, end_date=None,
502 self, start_id=None, end_id=None, start_date=None, end_date=None,
480 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
503 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
481 """
504 """
482 Returns iterator of `BaseCommit` objects from start to end
505 Returns iterator of `BaseCommit` objects from start to end
483 not inclusive. This should behave just like a list, ie. end is not
506 not inclusive. This should behave just like a list, ie. end is not
484 inclusive.
507 inclusive.
485
508
486 :param start_id: None or str, must be a valid commit id
509 :param start_id: None or str, must be a valid commit id
487 :param end_id: None or str, must be a valid commit id
510 :param end_id: None or str, must be a valid commit id
488 :param start_date:
511 :param start_date:
489 :param end_date:
512 :param end_date:
490 :param branch_name:
513 :param branch_name:
491 :param show_hidden:
514 :param show_hidden:
492 :param pre_load:
515 :param pre_load:
493 :param translate_tags:
516 :param translate_tags:
494 """
517 """
495 raise NotImplementedError
518 raise NotImplementedError
496
519
497 def __getitem__(self, key):
520 def __getitem__(self, key):
498 """
521 """
499 Allows index based access to the commit objects of this repository.
522 Allows index based access to the commit objects of this repository.
500 """
523 """
501 pre_load = ["author", "branch", "date", "message", "parents"]
524 pre_load = ["author", "branch", "date", "message", "parents"]
502 if isinstance(key, slice):
525 if isinstance(key, slice):
503 return self._get_range(key, pre_load)
526 return self._get_range(key, pre_load)
504 return self.get_commit(commit_idx=key, pre_load=pre_load)
527 return self.get_commit(commit_idx=key, pre_load=pre_load)
505
528
506 def _get_range(self, slice_obj, pre_load):
529 def _get_range(self, slice_obj, pre_load):
507 for commit_id in self.commit_ids.__getitem__(slice_obj):
530 for commit_id in self.commit_ids.__getitem__(slice_obj):
508 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
531 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
509
532
510 def count(self):
533 def count(self):
511 return len(self.commit_ids)
534 return len(self.commit_ids)
512
535
513 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
536 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
514 """
537 """
515 Creates and returns a tag for the given ``commit_id``.
538 Creates and returns a tag for the given ``commit_id``.
516
539
517 :param name: name for new tag
540 :param name: name for new tag
518 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
541 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
519 :param commit_id: commit id for which new tag would be created
542 :param commit_id: commit id for which new tag would be created
520 :param message: message of the tag's commit
543 :param message: message of the tag's commit
521 :param date: date of tag's commit
544 :param date: date of tag's commit
522
545
523 :raises TagAlreadyExistError: if tag with same name already exists
546 :raises TagAlreadyExistError: if tag with same name already exists
524 """
547 """
525 raise NotImplementedError
548 raise NotImplementedError
526
549
527 def remove_tag(self, name, user, message=None, date=None):
550 def remove_tag(self, name, user, message=None, date=None):
528 """
551 """
529 Removes tag with the given ``name``.
552 Removes tag with the given ``name``.
530
553
531 :param name: name of the tag to be removed
554 :param name: name of the tag to be removed
532 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
555 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
533 :param message: message of the tag's removal commit
556 :param message: message of the tag's removal commit
534 :param date: date of tag's removal commit
557 :param date: date of tag's removal commit
535
558
536 :raises TagDoesNotExistError: if tag with given name does not exists
559 :raises TagDoesNotExistError: if tag with given name does not exists
537 """
560 """
538 raise NotImplementedError
561 raise NotImplementedError
539
562
540 def get_diff(
563 def get_diff(
541 self, commit1, commit2, path=None, ignore_whitespace=False,
564 self, commit1, commit2, path=None, ignore_whitespace=False,
542 context=3, path1=None):
565 context=3, path1=None):
543 """
566 """
544 Returns (git like) *diff*, as plain text. Shows changes introduced by
567 Returns (git like) *diff*, as plain text. Shows changes introduced by
545 `commit2` since `commit1`.
568 `commit2` since `commit1`.
546
569
547 :param commit1: Entry point from which diff is shown. Can be
570 :param commit1: Entry point from which diff is shown. Can be
548 ``self.EMPTY_COMMIT`` - in this case, patch showing all
571 ``self.EMPTY_COMMIT`` - in this case, patch showing all
549 the changes since empty state of the repository until `commit2`
572 the changes since empty state of the repository until `commit2`
550 :param commit2: Until which commit changes should be shown.
573 :param commit2: Until which commit changes should be shown.
551 :param path: Can be set to a path of a file to create a diff of that
574 :param path: Can be set to a path of a file to create a diff of that
552 file. If `path1` is also set, this value is only associated to
575 file. If `path1` is also set, this value is only associated to
553 `commit2`.
576 `commit2`.
554 :param ignore_whitespace: If set to ``True``, would not show whitespace
577 :param ignore_whitespace: If set to ``True``, would not show whitespace
555 changes. Defaults to ``False``.
578 changes. Defaults to ``False``.
556 :param context: How many lines before/after changed lines should be
579 :param context: How many lines before/after changed lines should be
557 shown. Defaults to ``3``.
580 shown. Defaults to ``3``.
558 :param path1: Can be set to a path to associate with `commit1`. This
581 :param path1: Can be set to a path to associate with `commit1`. This
559 parameter works only for backends which support diff generation for
582 parameter works only for backends which support diff generation for
560 different paths. Other backends will raise a `ValueError` if `path1`
583 different paths. Other backends will raise a `ValueError` if `path1`
561 is set and has a different value than `path`.
584 is set and has a different value than `path`.
562 :param file_path: filter this diff by given path pattern
585 :param file_path: filter this diff by given path pattern
563 """
586 """
564 raise NotImplementedError
587 raise NotImplementedError
565
588
566 def strip(self, commit_id, branch=None):
589 def strip(self, commit_id, branch=None):
567 """
590 """
568 Strip given commit_id from the repository
591 Strip given commit_id from the repository
569 """
592 """
570 raise NotImplementedError
593 raise NotImplementedError
571
594
572 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
595 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
573 """
596 """
574 Return a latest common ancestor commit if one exists for this repo
597 Return a latest common ancestor commit if one exists for this repo
575 `commit_id1` vs `commit_id2` from `repo2`.
598 `commit_id1` vs `commit_id2` from `repo2`.
576
599
577 :param commit_id1: Commit it from this repository to use as a
600 :param commit_id1: Commit it from this repository to use as a
578 target for the comparison.
601 target for the comparison.
579 :param commit_id2: Source commit id to use for comparison.
602 :param commit_id2: Source commit id to use for comparison.
580 :param repo2: Source repository to use for comparison.
603 :param repo2: Source repository to use for comparison.
581 """
604 """
582 raise NotImplementedError
605 raise NotImplementedError
583
606
584 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
607 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
585 """
608 """
586 Compare this repository's revision `commit_id1` with `commit_id2`.
609 Compare this repository's revision `commit_id1` with `commit_id2`.
587
610
588 Returns a tuple(commits, ancestor) that would be merged from
611 Returns a tuple(commits, ancestor) that would be merged from
589 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
612 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
590 will be returned as ancestor.
613 will be returned as ancestor.
591
614
592 :param commit_id1: Commit it from this repository to use as a
615 :param commit_id1: Commit it from this repository to use as a
593 target for the comparison.
616 target for the comparison.
594 :param commit_id2: Source commit id to use for comparison.
617 :param commit_id2: Source commit id to use for comparison.
595 :param repo2: Source repository to use for comparison.
618 :param repo2: Source repository to use for comparison.
596 :param merge: If set to ``True`` will do a merge compare which also
619 :param merge: If set to ``True`` will do a merge compare which also
597 returns the common ancestor.
620 returns the common ancestor.
598 :param pre_load: Optional. List of commit attributes to load.
621 :param pre_load: Optional. List of commit attributes to load.
599 """
622 """
600 raise NotImplementedError
623 raise NotImplementedError
601
624
602 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
625 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
603 user_name='', user_email='', message='', dry_run=False,
626 user_name='', user_email='', message='', dry_run=False,
604 use_rebase=False, close_branch=False):
627 use_rebase=False, close_branch=False):
605 """
628 """
606 Merge the revisions specified in `source_ref` from `source_repo`
629 Merge the revisions specified in `source_ref` from `source_repo`
607 onto the `target_ref` of this repository.
630 onto the `target_ref` of this repository.
608
631
609 `source_ref` and `target_ref` are named tupls with the following
632 `source_ref` and `target_ref` are named tupls with the following
610 fields `type`, `name` and `commit_id`.
633 fields `type`, `name` and `commit_id`.
611
634
612 Returns a MergeResponse named tuple with the following fields
635 Returns a MergeResponse named tuple with the following fields
613 'possible', 'executed', 'source_commit', 'target_commit',
636 'possible', 'executed', 'source_commit', 'target_commit',
614 'merge_commit'.
637 'merge_commit'.
615
638
616 :param repo_id: `repo_id` target repo id.
639 :param repo_id: `repo_id` target repo id.
617 :param workspace_id: `workspace_id` unique identifier.
640 :param workspace_id: `workspace_id` unique identifier.
618 :param target_ref: `target_ref` points to the commit on top of which
641 :param target_ref: `target_ref` points to the commit on top of which
619 the `source_ref` should be merged.
642 the `source_ref` should be merged.
620 :param source_repo: The repository that contains the commits to be
643 :param source_repo: The repository that contains the commits to be
621 merged.
644 merged.
622 :param source_ref: `source_ref` points to the topmost commit from
645 :param source_ref: `source_ref` points to the topmost commit from
623 the `source_repo` which should be merged.
646 the `source_repo` which should be merged.
624 :param user_name: Merge commit `user_name`.
647 :param user_name: Merge commit `user_name`.
625 :param user_email: Merge commit `user_email`.
648 :param user_email: Merge commit `user_email`.
626 :param message: Merge commit `message`.
649 :param message: Merge commit `message`.
627 :param dry_run: If `True` the merge will not take place.
650 :param dry_run: If `True` the merge will not take place.
628 :param use_rebase: If `True` commits from the source will be rebased
651 :param use_rebase: If `True` commits from the source will be rebased
629 on top of the target instead of being merged.
652 on top of the target instead of being merged.
630 :param close_branch: If `True` branch will be close before merging it
653 :param close_branch: If `True` branch will be close before merging it
631 """
654 """
632 if dry_run:
655 if dry_run:
633 message = message or settings.MERGE_DRY_RUN_MESSAGE
656 message = message or settings.MERGE_DRY_RUN_MESSAGE
634 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
657 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
635 user_name = user_name or settings.MERGE_DRY_RUN_USER
658 user_name = user_name or settings.MERGE_DRY_RUN_USER
636 else:
659 else:
637 if not user_name:
660 if not user_name:
638 raise ValueError('user_name cannot be empty')
661 raise ValueError('user_name cannot be empty')
639 if not user_email:
662 if not user_email:
640 raise ValueError('user_email cannot be empty')
663 raise ValueError('user_email cannot be empty')
641 if not message:
664 if not message:
642 raise ValueError('message cannot be empty')
665 raise ValueError('message cannot be empty')
643
666
644 try:
667 try:
645 return self._merge_repo(
668 return self._merge_repo(
646 repo_id, workspace_id, target_ref, source_repo,
669 repo_id, workspace_id, target_ref, source_repo,
647 source_ref, message, user_name, user_email, dry_run=dry_run,
670 source_ref, message, user_name, user_email, dry_run=dry_run,
648 use_rebase=use_rebase, close_branch=close_branch)
671 use_rebase=use_rebase, close_branch=close_branch)
649 except RepositoryError as exc:
672 except RepositoryError as exc:
650 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
673 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
651 return MergeResponse(
674 return MergeResponse(
652 False, False, None, MergeFailureReason.UNKNOWN,
675 False, False, None, MergeFailureReason.UNKNOWN,
653 metadata={'exception': str(exc)})
676 metadata={'exception': str(exc)})
654
677
655 def _merge_repo(self, repo_id, workspace_id, target_ref,
678 def _merge_repo(self, repo_id, workspace_id, target_ref,
656 source_repo, source_ref, merge_message,
679 source_repo, source_ref, merge_message,
657 merger_name, merger_email, dry_run=False,
680 merger_name, merger_email, dry_run=False,
658 use_rebase=False, close_branch=False):
681 use_rebase=False, close_branch=False):
659 """Internal implementation of merge."""
682 """Internal implementation of merge."""
660 raise NotImplementedError
683 raise NotImplementedError
661
684
662 def _maybe_prepare_merge_workspace(
685 def _maybe_prepare_merge_workspace(
663 self, repo_id, workspace_id, target_ref, source_ref):
686 self, repo_id, workspace_id, target_ref, source_ref):
664 """
687 """
665 Create the merge workspace.
688 Create the merge workspace.
666
689
667 :param workspace_id: `workspace_id` unique identifier.
690 :param workspace_id: `workspace_id` unique identifier.
668 """
691 """
669 raise NotImplementedError
692 raise NotImplementedError
670
693
671 @classmethod
694 @classmethod
672 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
695 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
673 """
696 """
674 Legacy version that was used before. We still need it for
697 Legacy version that was used before. We still need it for
675 backward compat
698 backward compat
676 """
699 """
677 return os.path.join(
700 return os.path.join(
678 os.path.dirname(repo_path),
701 os.path.dirname(repo_path),
679 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
702 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
680
703
681 @classmethod
704 @classmethod
682 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
705 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
683 # The name of the shadow repository must start with '.', so it is
706 # The name of the shadow repository must start with '.', so it is
684 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
707 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
685 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
708 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
686 if os.path.exists(legacy_repository_path):
709 if os.path.exists(legacy_repository_path):
687 return legacy_repository_path
710 return legacy_repository_path
688 else:
711 else:
689 return os.path.join(
712 return os.path.join(
690 os.path.dirname(repo_path),
713 os.path.dirname(repo_path),
691 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
714 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
692
715
693 def cleanup_merge_workspace(self, repo_id, workspace_id):
716 def cleanup_merge_workspace(self, repo_id, workspace_id):
694 """
717 """
695 Remove merge workspace.
718 Remove merge workspace.
696
719
697 This function MUST not fail in case there is no workspace associated to
720 This function MUST not fail in case there is no workspace associated to
698 the given `workspace_id`.
721 the given `workspace_id`.
699
722
700 :param workspace_id: `workspace_id` unique identifier.
723 :param workspace_id: `workspace_id` unique identifier.
701 """
724 """
702 shadow_repository_path = self._get_shadow_repository_path(
725 shadow_repository_path = self._get_shadow_repository_path(
703 self.path, repo_id, workspace_id)
726 self.path, repo_id, workspace_id)
704 shadow_repository_path_del = '{}.{}.delete'.format(
727 shadow_repository_path_del = '{}.{}.delete'.format(
705 shadow_repository_path, time.time())
728 shadow_repository_path, time.time())
706
729
707 # move the shadow repo, so it never conflicts with the one used.
730 # move the shadow repo, so it never conflicts with the one used.
708 # we use this method because shutil.rmtree had some edge case problems
731 # we use this method because shutil.rmtree had some edge case problems
709 # removing symlinked repositories
732 # removing symlinked repositories
710 if not os.path.isdir(shadow_repository_path):
733 if not os.path.isdir(shadow_repository_path):
711 return
734 return
712
735
713 shutil.move(shadow_repository_path, shadow_repository_path_del)
736 shutil.move(shadow_repository_path, shadow_repository_path_del)
714 try:
737 try:
715 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
738 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
716 except Exception:
739 except Exception:
717 log.exception('Failed to gracefully remove shadow repo under %s',
740 log.exception('Failed to gracefully remove shadow repo under %s',
718 shadow_repository_path_del)
741 shadow_repository_path_del)
719 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
742 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
720
743
721 # ========== #
744 # ========== #
722 # COMMIT API #
745 # COMMIT API #
723 # ========== #
746 # ========== #
724
747
725 @LazyProperty
748 @LazyProperty
726 def in_memory_commit(self):
749 def in_memory_commit(self):
727 """
750 """
728 Returns :class:`InMemoryCommit` object for this repository.
751 Returns :class:`InMemoryCommit` object for this repository.
729 """
752 """
730 raise NotImplementedError
753 raise NotImplementedError
731
754
732 # ======================== #
755 # ======================== #
733 # UTILITIES FOR SUBCLASSES #
756 # UTILITIES FOR SUBCLASSES #
734 # ======================== #
757 # ======================== #
735
758
736 def _validate_diff_commits(self, commit1, commit2):
759 def _validate_diff_commits(self, commit1, commit2):
737 """
760 """
738 Validates that the given commits are related to this repository.
761 Validates that the given commits are related to this repository.
739
762
740 Intended as a utility for sub classes to have a consistent validation
763 Intended as a utility for sub classes to have a consistent validation
741 of input parameters in methods like :meth:`get_diff`.
764 of input parameters in methods like :meth:`get_diff`.
742 """
765 """
743 self._validate_commit(commit1)
766 self._validate_commit(commit1)
744 self._validate_commit(commit2)
767 self._validate_commit(commit2)
745 if (isinstance(commit1, EmptyCommit) and
768 if (isinstance(commit1, EmptyCommit) and
746 isinstance(commit2, EmptyCommit)):
769 isinstance(commit2, EmptyCommit)):
747 raise ValueError("Cannot compare two empty commits")
770 raise ValueError("Cannot compare two empty commits")
748
771
749 def _validate_commit(self, commit):
772 def _validate_commit(self, commit):
750 if not isinstance(commit, BaseCommit):
773 if not isinstance(commit, BaseCommit):
751 raise TypeError(
774 raise TypeError(
752 "%s is not of type BaseCommit" % repr(commit))
775 "%s is not of type BaseCommit" % repr(commit))
753 if commit.repository != self and not isinstance(commit, EmptyCommit):
776 if commit.repository != self and not isinstance(commit, EmptyCommit):
754 raise ValueError(
777 raise ValueError(
755 "Commit %s must be a valid commit from this repository %s, "
778 "Commit %s must be a valid commit from this repository %s, "
756 "related to this repository instead %s." %
779 "related to this repository instead %s." %
757 (commit, self, commit.repository))
780 (commit, self, commit.repository))
758
781
759 def _validate_commit_id(self, commit_id):
782 def _validate_commit_id(self, commit_id):
760 if not isinstance(commit_id, compat.string_types):
783 if not isinstance(commit_id, compat.string_types):
761 raise TypeError("commit_id must be a string value got {} instead".format(type(commit_id)))
784 raise TypeError("commit_id must be a string value got {} instead".format(type(commit_id)))
762
785
763 def _validate_commit_idx(self, commit_idx):
786 def _validate_commit_idx(self, commit_idx):
764 if not isinstance(commit_idx, (int, long)):
787 if not isinstance(commit_idx, (int, long)):
765 raise TypeError("commit_idx must be a numeric value")
788 raise TypeError("commit_idx must be a numeric value")
766
789
767 def _validate_branch_name(self, branch_name):
790 def _validate_branch_name(self, branch_name):
768 if branch_name and branch_name not in self.branches_all:
791 if branch_name and branch_name not in self.branches_all:
769 msg = ("Branch %s not found in %s" % (branch_name, self))
792 msg = ("Branch %s not found in %s" % (branch_name, self))
770 raise BranchDoesNotExistError(msg)
793 raise BranchDoesNotExistError(msg)
771
794
772 #
795 #
773 # Supporting deprecated API parts
796 # Supporting deprecated API parts
774 # TODO: johbo: consider to move this into a mixin
797 # TODO: johbo: consider to move this into a mixin
775 #
798 #
776
799
777 @property
800 @property
778 def EMPTY_CHANGESET(self):
801 def EMPTY_CHANGESET(self):
779 warnings.warn(
802 warnings.warn(
780 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
803 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
781 return self.EMPTY_COMMIT_ID
804 return self.EMPTY_COMMIT_ID
782
805
783 @property
806 @property
784 def revisions(self):
807 def revisions(self):
785 warnings.warn("Use commits attribute instead", DeprecationWarning)
808 warnings.warn("Use commits attribute instead", DeprecationWarning)
786 return self.commit_ids
809 return self.commit_ids
787
810
788 @revisions.setter
811 @revisions.setter
789 def revisions(self, value):
812 def revisions(self, value):
790 warnings.warn("Use commits attribute instead", DeprecationWarning)
813 warnings.warn("Use commits attribute instead", DeprecationWarning)
791 self.commit_ids = value
814 self.commit_ids = value
792
815
793 def get_changeset(self, revision=None, pre_load=None):
816 def get_changeset(self, revision=None, pre_load=None):
794 warnings.warn("Use get_commit instead", DeprecationWarning)
817 warnings.warn("Use get_commit instead", DeprecationWarning)
795 commit_id = None
818 commit_id = None
796 commit_idx = None
819 commit_idx = None
797 if isinstance(revision, compat.string_types):
820 if isinstance(revision, compat.string_types):
798 commit_id = revision
821 commit_id = revision
799 else:
822 else:
800 commit_idx = revision
823 commit_idx = revision
801 return self.get_commit(
824 return self.get_commit(
802 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
825 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
803
826
804 def get_changesets(
827 def get_changesets(
805 self, start=None, end=None, start_date=None, end_date=None,
828 self, start=None, end=None, start_date=None, end_date=None,
806 branch_name=None, pre_load=None):
829 branch_name=None, pre_load=None):
807 warnings.warn("Use get_commits instead", DeprecationWarning)
830 warnings.warn("Use get_commits instead", DeprecationWarning)
808 start_id = self._revision_to_commit(start)
831 start_id = self._revision_to_commit(start)
809 end_id = self._revision_to_commit(end)
832 end_id = self._revision_to_commit(end)
810 return self.get_commits(
833 return self.get_commits(
811 start_id=start_id, end_id=end_id, start_date=start_date,
834 start_id=start_id, end_id=end_id, start_date=start_date,
812 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
835 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
813
836
814 def _revision_to_commit(self, revision):
837 def _revision_to_commit(self, revision):
815 """
838 """
816 Translates a revision to a commit_id
839 Translates a revision to a commit_id
817
840
818 Helps to support the old changeset based API which allows to use
841 Helps to support the old changeset based API which allows to use
819 commit ids and commit indices interchangeable.
842 commit ids and commit indices interchangeable.
820 """
843 """
821 if revision is None:
844 if revision is None:
822 return revision
845 return revision
823
846
824 if isinstance(revision, compat.string_types):
847 if isinstance(revision, compat.string_types):
825 commit_id = revision
848 commit_id = revision
826 else:
849 else:
827 commit_id = self.commit_ids[revision]
850 commit_id = self.commit_ids[revision]
828 return commit_id
851 return commit_id
829
852
830 @property
853 @property
831 def in_memory_changeset(self):
854 def in_memory_changeset(self):
832 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
855 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
833 return self.in_memory_commit
856 return self.in_memory_commit
834
857
835 def get_path_permissions(self, username):
858 def get_path_permissions(self, username):
836 """
859 """
837 Returns a path permission checker or None if not supported
860 Returns a path permission checker or None if not supported
838
861
839 :param username: session user name
862 :param username: session user name
840 :return: an instance of BasePathPermissionChecker or None
863 :return: an instance of BasePathPermissionChecker or None
841 """
864 """
842 return None
865 return None
843
866
844 def install_hooks(self, force=False):
867 def install_hooks(self, force=False):
845 return self._remote.install_hooks(force)
868 return self._remote.install_hooks(force)
846
869
847 def get_hooks_info(self):
870 def get_hooks_info(self):
848 return self._remote.get_hooks_info()
871 return self._remote.get_hooks_info()
849
872
850
873
851 class BaseCommit(object):
874 class BaseCommit(object):
852 """
875 """
853 Each backend should implement it's commit representation.
876 Each backend should implement it's commit representation.
854
877
855 **Attributes**
878 **Attributes**
856
879
857 ``repository``
880 ``repository``
858 repository object within which commit exists
881 repository object within which commit exists
859
882
860 ``id``
883 ``id``
861 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
884 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
862 just ``tip``.
885 just ``tip``.
863
886
864 ``raw_id``
887 ``raw_id``
865 raw commit representation (i.e. full 40 length sha for git
888 raw commit representation (i.e. full 40 length sha for git
866 backend)
889 backend)
867
890
868 ``short_id``
891 ``short_id``
869 shortened (if apply) version of ``raw_id``; it would be simple
892 shortened (if apply) version of ``raw_id``; it would be simple
870 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
893 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
871 as ``raw_id`` for subversion
894 as ``raw_id`` for subversion
872
895
873 ``idx``
896 ``idx``
874 commit index
897 commit index
875
898
876 ``files``
899 ``files``
877 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
900 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
878
901
879 ``dirs``
902 ``dirs``
880 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
903 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
881
904
882 ``nodes``
905 ``nodes``
883 combined list of ``Node`` objects
906 combined list of ``Node`` objects
884
907
885 ``author``
908 ``author``
886 author of the commit, as unicode
909 author of the commit, as unicode
887
910
888 ``message``
911 ``message``
889 message of the commit, as unicode
912 message of the commit, as unicode
890
913
891 ``parents``
914 ``parents``
892 list of parent commits
915 list of parent commits
893
916
894 """
917 """
895
918
896 branch = None
919 branch = None
897 """
920 """
898 Depending on the backend this should be set to the branch name of the
921 Depending on the backend this should be set to the branch name of the
899 commit. Backends not supporting branches on commits should leave this
922 commit. Backends not supporting branches on commits should leave this
900 value as ``None``.
923 value as ``None``.
901 """
924 """
902
925
903 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
926 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
904 """
927 """
905 This template is used to generate a default prefix for repository archives
928 This template is used to generate a default prefix for repository archives
906 if no prefix has been specified.
929 if no prefix has been specified.
907 """
930 """
908
931
909 def __str__(self):
932 def __str__(self):
910 return '<%s at %s:%s>' % (
933 return '<%s at %s:%s>' % (
911 self.__class__.__name__, self.idx, self.short_id)
934 self.__class__.__name__, self.idx, self.short_id)
912
935
913 def __repr__(self):
936 def __repr__(self):
914 return self.__str__()
937 return self.__str__()
915
938
916 def __unicode__(self):
939 def __unicode__(self):
917 return u'%s:%s' % (self.idx, self.short_id)
940 return u'%s:%s' % (self.idx, self.short_id)
918
941
919 def __eq__(self, other):
942 def __eq__(self, other):
920 same_instance = isinstance(other, self.__class__)
943 same_instance = isinstance(other, self.__class__)
921 return same_instance and self.raw_id == other.raw_id
944 return same_instance and self.raw_id == other.raw_id
922
945
923 def __json__(self):
946 def __json__(self):
924 parents = []
947 parents = []
925 try:
948 try:
926 for parent in self.parents:
949 for parent in self.parents:
927 parents.append({'raw_id': parent.raw_id})
950 parents.append({'raw_id': parent.raw_id})
928 except NotImplementedError:
951 except NotImplementedError:
929 # empty commit doesn't have parents implemented
952 # empty commit doesn't have parents implemented
930 pass
953 pass
931
954
932 return {
955 return {
933 'short_id': self.short_id,
956 'short_id': self.short_id,
934 'raw_id': self.raw_id,
957 'raw_id': self.raw_id,
935 'revision': self.idx,
958 'revision': self.idx,
936 'message': self.message,
959 'message': self.message,
937 'date': self.date,
960 'date': self.date,
938 'author': self.author,
961 'author': self.author,
939 'parents': parents,
962 'parents': parents,
940 'branch': self.branch
963 'branch': self.branch
941 }
964 }
942
965
943 def __getstate__(self):
966 def __getstate__(self):
944 d = self.__dict__.copy()
967 d = self.__dict__.copy()
945 d.pop('_remote', None)
968 d.pop('_remote', None)
946 d.pop('repository', None)
969 d.pop('repository', None)
947 return d
970 return d
948
971
949 def serialize(self):
972 def serialize(self):
950 return self.__json__()
973 return self.__json__()
951
974
952 def _get_refs(self):
975 def _get_refs(self):
953 return {
976 return {
954 'branches': [self.branch] if self.branch else [],
977 'branches': [self.branch] if self.branch else [],
955 'bookmarks': getattr(self, 'bookmarks', []),
978 'bookmarks': getattr(self, 'bookmarks', []),
956 'tags': self.tags
979 'tags': self.tags
957 }
980 }
958
981
959 @LazyProperty
982 @LazyProperty
960 def last(self):
983 def last(self):
961 """
984 """
962 ``True`` if this is last commit in repository, ``False``
985 ``True`` if this is last commit in repository, ``False``
963 otherwise; trying to access this attribute while there is no
986 otherwise; trying to access this attribute while there is no
964 commits would raise `EmptyRepositoryError`
987 commits would raise `EmptyRepositoryError`
965 """
988 """
966 if self.repository is None:
989 if self.repository is None:
967 raise CommitError("Cannot check if it's most recent commit")
990 raise CommitError("Cannot check if it's most recent commit")
968 return self.raw_id == self.repository.commit_ids[-1]
991 return self.raw_id == self.repository.commit_ids[-1]
969
992
970 @LazyProperty
993 @LazyProperty
971 def parents(self):
994 def parents(self):
972 """
995 """
973 Returns list of parent commits.
996 Returns list of parent commits.
974 """
997 """
975 raise NotImplementedError
998 raise NotImplementedError
976
999
977 @LazyProperty
1000 @LazyProperty
978 def first_parent(self):
1001 def first_parent(self):
979 """
1002 """
980 Returns list of parent commits.
1003 Returns list of parent commits.
981 """
1004 """
982 return self.parents[0] if self.parents else EmptyCommit()
1005 return self.parents[0] if self.parents else EmptyCommit()
983
1006
984 @property
1007 @property
985 def merge(self):
1008 def merge(self):
986 """
1009 """
987 Returns boolean if commit is a merge.
1010 Returns boolean if commit is a merge.
988 """
1011 """
989 return len(self.parents) > 1
1012 return len(self.parents) > 1
990
1013
991 @LazyProperty
1014 @LazyProperty
992 def children(self):
1015 def children(self):
993 """
1016 """
994 Returns list of child commits.
1017 Returns list of child commits.
995 """
1018 """
996 raise NotImplementedError
1019 raise NotImplementedError
997
1020
998 @LazyProperty
1021 @LazyProperty
999 def id(self):
1022 def id(self):
1000 """
1023 """
1001 Returns string identifying this commit.
1024 Returns string identifying this commit.
1002 """
1025 """
1003 raise NotImplementedError
1026 raise NotImplementedError
1004
1027
1005 @LazyProperty
1028 @LazyProperty
1006 def raw_id(self):
1029 def raw_id(self):
1007 """
1030 """
1008 Returns raw string identifying this commit.
1031 Returns raw string identifying this commit.
1009 """
1032 """
1010 raise NotImplementedError
1033 raise NotImplementedError
1011
1034
1012 @LazyProperty
1035 @LazyProperty
1013 def short_id(self):
1036 def short_id(self):
1014 """
1037 """
1015 Returns shortened version of ``raw_id`` attribute, as string,
1038 Returns shortened version of ``raw_id`` attribute, as string,
1016 identifying this commit, useful for presentation to users.
1039 identifying this commit, useful for presentation to users.
1017 """
1040 """
1018 raise NotImplementedError
1041 raise NotImplementedError
1019
1042
1020 @LazyProperty
1043 @LazyProperty
1021 def idx(self):
1044 def idx(self):
1022 """
1045 """
1023 Returns integer identifying this commit.
1046 Returns integer identifying this commit.
1024 """
1047 """
1025 raise NotImplementedError
1048 raise NotImplementedError
1026
1049
1027 @LazyProperty
1050 @LazyProperty
1028 def committer(self):
1051 def committer(self):
1029 """
1052 """
1030 Returns committer for this commit
1053 Returns committer for this commit
1031 """
1054 """
1032 raise NotImplementedError
1055 raise NotImplementedError
1033
1056
1034 @LazyProperty
1057 @LazyProperty
1035 def committer_name(self):
1058 def committer_name(self):
1036 """
1059 """
1037 Returns committer name for this commit
1060 Returns committer name for this commit
1038 """
1061 """
1039
1062
1040 return author_name(self.committer)
1063 return author_name(self.committer)
1041
1064
1042 @LazyProperty
1065 @LazyProperty
1043 def committer_email(self):
1066 def committer_email(self):
1044 """
1067 """
1045 Returns committer email address for this commit
1068 Returns committer email address for this commit
1046 """
1069 """
1047
1070
1048 return author_email(self.committer)
1071 return author_email(self.committer)
1049
1072
1050 @LazyProperty
1073 @LazyProperty
1051 def author(self):
1074 def author(self):
1052 """
1075 """
1053 Returns author for this commit
1076 Returns author for this commit
1054 """
1077 """
1055
1078
1056 raise NotImplementedError
1079 raise NotImplementedError
1057
1080
1058 @LazyProperty
1081 @LazyProperty
1059 def author_name(self):
1082 def author_name(self):
1060 """
1083 """
1061 Returns author name for this commit
1084 Returns author name for this commit
1062 """
1085 """
1063
1086
1064 return author_name(self.author)
1087 return author_name(self.author)
1065
1088
1066 @LazyProperty
1089 @LazyProperty
1067 def author_email(self):
1090 def author_email(self):
1068 """
1091 """
1069 Returns author email address for this commit
1092 Returns author email address for this commit
1070 """
1093 """
1071
1094
1072 return author_email(self.author)
1095 return author_email(self.author)
1073
1096
1074 def get_file_mode(self, path):
1097 def get_file_mode(self, path):
1075 """
1098 """
1076 Returns stat mode of the file at `path`.
1099 Returns stat mode of the file at `path`.
1077 """
1100 """
1078 raise NotImplementedError
1101 raise NotImplementedError
1079
1102
1080 def is_link(self, path):
1103 def is_link(self, path):
1081 """
1104 """
1082 Returns ``True`` if given `path` is a symlink
1105 Returns ``True`` if given `path` is a symlink
1083 """
1106 """
1084 raise NotImplementedError
1107 raise NotImplementedError
1085
1108
1086 def is_node_binary(self, path):
1109 def is_node_binary(self, path):
1087 """
1110 """
1088 Returns ``True`` is given path is a binary file
1111 Returns ``True`` is given path is a binary file
1089 """
1112 """
1090 raise NotImplementedError
1113 raise NotImplementedError
1091
1114
1092 def get_file_content(self, path):
1115 def get_file_content(self, path):
1093 """
1116 """
1094 Returns content of the file at the given `path`.
1117 Returns content of the file at the given `path`.
1095 """
1118 """
1096 raise NotImplementedError
1119 raise NotImplementedError
1097
1120
1098 def get_file_content_streamed(self, path):
1121 def get_file_content_streamed(self, path):
1099 """
1122 """
1100 returns a streaming response from vcsserver with file content
1123 returns a streaming response from vcsserver with file content
1101 """
1124 """
1102 raise NotImplementedError
1125 raise NotImplementedError
1103
1126
1104 def get_file_size(self, path):
1127 def get_file_size(self, path):
1105 """
1128 """
1106 Returns size of the file at the given `path`.
1129 Returns size of the file at the given `path`.
1107 """
1130 """
1108 raise NotImplementedError
1131 raise NotImplementedError
1109
1132
1110 def get_path_commit(self, path, pre_load=None):
1133 def get_path_commit(self, path, pre_load=None):
1111 """
1134 """
1112 Returns last commit of the file at the given `path`.
1135 Returns last commit of the file at the given `path`.
1113
1136
1114 :param pre_load: Optional. List of commit attributes to load.
1137 :param pre_load: Optional. List of commit attributes to load.
1115 """
1138 """
1116 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1139 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1117 if not commits:
1140 if not commits:
1118 raise RepositoryError(
1141 raise RepositoryError(
1119 'Failed to fetch history for path {}. '
1142 'Failed to fetch history for path {}. '
1120 'Please check if such path exists in your repository'.format(
1143 'Please check if such path exists in your repository'.format(
1121 path))
1144 path))
1122 return commits[0]
1145 return commits[0]
1123
1146
1124 def get_path_history(self, path, limit=None, pre_load=None):
1147 def get_path_history(self, path, limit=None, pre_load=None):
1125 """
1148 """
1126 Returns history of file as reversed list of :class:`BaseCommit`
1149 Returns history of file as reversed list of :class:`BaseCommit`
1127 objects for which file at given `path` has been modified.
1150 objects for which file at given `path` has been modified.
1128
1151
1129 :param limit: Optional. Allows to limit the size of the returned
1152 :param limit: Optional. Allows to limit the size of the returned
1130 history. This is intended as a hint to the underlying backend, so
1153 history. This is intended as a hint to the underlying backend, so
1131 that it can apply optimizations depending on the limit.
1154 that it can apply optimizations depending on the limit.
1132 :param pre_load: Optional. List of commit attributes to load.
1155 :param pre_load: Optional. List of commit attributes to load.
1133 """
1156 """
1134 raise NotImplementedError
1157 raise NotImplementedError
1135
1158
1136 def get_file_annotate(self, path, pre_load=None):
1159 def get_file_annotate(self, path, pre_load=None):
1137 """
1160 """
1138 Returns a generator of four element tuples with
1161 Returns a generator of four element tuples with
1139 lineno, sha, commit lazy loader and line
1162 lineno, sha, commit lazy loader and line
1140
1163
1141 :param pre_load: Optional. List of commit attributes to load.
1164 :param pre_load: Optional. List of commit attributes to load.
1142 """
1165 """
1143 raise NotImplementedError
1166 raise NotImplementedError
1144
1167
1145 def get_nodes(self, path):
1168 def get_nodes(self, path):
1146 """
1169 """
1147 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1170 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1148 state of commit at the given ``path``.
1171 state of commit at the given ``path``.
1149
1172
1150 :raises ``CommitError``: if node at the given ``path`` is not
1173 :raises ``CommitError``: if node at the given ``path`` is not
1151 instance of ``DirNode``
1174 instance of ``DirNode``
1152 """
1175 """
1153 raise NotImplementedError
1176 raise NotImplementedError
1154
1177
1155 def get_node(self, path):
1178 def get_node(self, path):
1156 """
1179 """
1157 Returns ``Node`` object from the given ``path``.
1180 Returns ``Node`` object from the given ``path``.
1158
1181
1159 :raises ``NodeDoesNotExistError``: if there is no node at the given
1182 :raises ``NodeDoesNotExistError``: if there is no node at the given
1160 ``path``
1183 ``path``
1161 """
1184 """
1162 raise NotImplementedError
1185 raise NotImplementedError
1163
1186
1164 def get_largefile_node(self, path):
1187 def get_largefile_node(self, path):
1165 """
1188 """
1166 Returns the path to largefile from Mercurial/Git-lfs storage.
1189 Returns the path to largefile from Mercurial/Git-lfs storage.
1167 or None if it's not a largefile node
1190 or None if it's not a largefile node
1168 """
1191 """
1169 return None
1192 return None
1170
1193
1171 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1194 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1172 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1195 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1173 """
1196 """
1174 Creates an archive containing the contents of the repository.
1197 Creates an archive containing the contents of the repository.
1175
1198
1176 :param archive_dest_path: path to the file which to create the archive.
1199 :param archive_dest_path: path to the file which to create the archive.
1177 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1200 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1178 :param prefix: name of root directory in archive.
1201 :param prefix: name of root directory in archive.
1179 Default is repository name and commit's short_id joined with dash:
1202 Default is repository name and commit's short_id joined with dash:
1180 ``"{repo_name}-{short_id}"``.
1203 ``"{repo_name}-{short_id}"``.
1181 :param write_metadata: write a metadata file into archive.
1204 :param write_metadata: write a metadata file into archive.
1182 :param mtime: custom modification time for archive creation, defaults
1205 :param mtime: custom modification time for archive creation, defaults
1183 to time.time() if not given.
1206 to time.time() if not given.
1184 :param archive_at_path: pack files at this path (default '/')
1207 :param archive_at_path: pack files at this path (default '/')
1185
1208
1186 :raise VCSError: If prefix has a problem.
1209 :raise VCSError: If prefix has a problem.
1187 """
1210 """
1188 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1211 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1189 if kind not in allowed_kinds:
1212 if kind not in allowed_kinds:
1190 raise ImproperArchiveTypeError(
1213 raise ImproperArchiveTypeError(
1191 'Archive kind (%s) not supported use one of %s' %
1214 'Archive kind (%s) not supported use one of %s' %
1192 (kind, allowed_kinds))
1215 (kind, allowed_kinds))
1193
1216
1194 prefix = self._validate_archive_prefix(prefix)
1217 prefix = self._validate_archive_prefix(prefix)
1195
1218
1196 mtime = mtime is not None or time.mktime(self.date.timetuple())
1219 mtime = mtime is not None or time.mktime(self.date.timetuple())
1197
1220
1198 file_info = []
1221 file_info = []
1199 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1222 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1200 for _r, _d, files in cur_rev.walk(archive_at_path):
1223 for _r, _d, files in cur_rev.walk(archive_at_path):
1201 for f in files:
1224 for f in files:
1202 f_path = os.path.join(prefix, f.path)
1225 f_path = os.path.join(prefix, f.path)
1203 file_info.append(
1226 file_info.append(
1204 (f_path, f.mode, f.is_link(), f.raw_bytes))
1227 (f_path, f.mode, f.is_link(), f.raw_bytes))
1205
1228
1206 if write_metadata:
1229 if write_metadata:
1207 metadata = [
1230 metadata = [
1208 ('repo_name', self.repository.name),
1231 ('repo_name', self.repository.name),
1209 ('commit_id', self.raw_id),
1232 ('commit_id', self.raw_id),
1210 ('mtime', mtime),
1233 ('mtime', mtime),
1211 ('branch', self.branch),
1234 ('branch', self.branch),
1212 ('tags', ','.join(self.tags)),
1235 ('tags', ','.join(self.tags)),
1213 ]
1236 ]
1214 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1237 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1215 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1238 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1216
1239
1217 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1240 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1218
1241
1219 def _validate_archive_prefix(self, prefix):
1242 def _validate_archive_prefix(self, prefix):
1220 if prefix is None:
1243 if prefix is None:
1221 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1244 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1222 repo_name=safe_str(self.repository.name),
1245 repo_name=safe_str(self.repository.name),
1223 short_id=self.short_id)
1246 short_id=self.short_id)
1224 elif not isinstance(prefix, str):
1247 elif not isinstance(prefix, str):
1225 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1248 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1226 elif prefix.startswith('/'):
1249 elif prefix.startswith('/'):
1227 raise VCSError("Prefix cannot start with leading slash")
1250 raise VCSError("Prefix cannot start with leading slash")
1228 elif prefix.strip() == '':
1251 elif prefix.strip() == '':
1229 raise VCSError("Prefix cannot be empty")
1252 raise VCSError("Prefix cannot be empty")
1230 return prefix
1253 return prefix
1231
1254
1232 @LazyProperty
1255 @LazyProperty
1233 def root(self):
1256 def root(self):
1234 """
1257 """
1235 Returns ``RootNode`` object for this commit.
1258 Returns ``RootNode`` object for this commit.
1236 """
1259 """
1237 return self.get_node('')
1260 return self.get_node('')
1238
1261
1239 def next(self, branch=None):
1262 def next(self, branch=None):
1240 """
1263 """
1241 Returns next commit from current, if branch is gives it will return
1264 Returns next commit from current, if branch is gives it will return
1242 next commit belonging to this branch
1265 next commit belonging to this branch
1243
1266
1244 :param branch: show commits within the given named branch
1267 :param branch: show commits within the given named branch
1245 """
1268 """
1246 indexes = xrange(self.idx + 1, self.repository.count())
1269 indexes = xrange(self.idx + 1, self.repository.count())
1247 return self._find_next(indexes, branch)
1270 return self._find_next(indexes, branch)
1248
1271
1249 def prev(self, branch=None):
1272 def prev(self, branch=None):
1250 """
1273 """
1251 Returns previous commit from current, if branch is gives it will
1274 Returns previous commit from current, if branch is gives it will
1252 return previous commit belonging to this branch
1275 return previous commit belonging to this branch
1253
1276
1254 :param branch: show commit within the given named branch
1277 :param branch: show commit within the given named branch
1255 """
1278 """
1256 indexes = xrange(self.idx - 1, -1, -1)
1279 indexes = xrange(self.idx - 1, -1, -1)
1257 return self._find_next(indexes, branch)
1280 return self._find_next(indexes, branch)
1258
1281
1259 def _find_next(self, indexes, branch=None):
1282 def _find_next(self, indexes, branch=None):
1260 if branch and self.branch != branch:
1283 if branch and self.branch != branch:
1261 raise VCSError('Branch option used on commit not belonging '
1284 raise VCSError('Branch option used on commit not belonging '
1262 'to that branch')
1285 'to that branch')
1263
1286
1264 for next_idx in indexes:
1287 for next_idx in indexes:
1265 commit = self.repository.get_commit(commit_idx=next_idx)
1288 commit = self.repository.get_commit(commit_idx=next_idx)
1266 if branch and branch != commit.branch:
1289 if branch and branch != commit.branch:
1267 continue
1290 continue
1268 return commit
1291 return commit
1269 raise CommitDoesNotExistError
1292 raise CommitDoesNotExistError
1270
1293
1271 def diff(self, ignore_whitespace=True, context=3):
1294 def diff(self, ignore_whitespace=True, context=3):
1272 """
1295 """
1273 Returns a `Diff` object representing the change made by this commit.
1296 Returns a `Diff` object representing the change made by this commit.
1274 """
1297 """
1275 parent = self.first_parent
1298 parent = self.first_parent
1276 diff = self.repository.get_diff(
1299 diff = self.repository.get_diff(
1277 parent, self,
1300 parent, self,
1278 ignore_whitespace=ignore_whitespace,
1301 ignore_whitespace=ignore_whitespace,
1279 context=context)
1302 context=context)
1280 return diff
1303 return diff
1281
1304
1282 @LazyProperty
1305 @LazyProperty
1283 def added(self):
1306 def added(self):
1284 """
1307 """
1285 Returns list of added ``FileNode`` objects.
1308 Returns list of added ``FileNode`` objects.
1286 """
1309 """
1287 raise NotImplementedError
1310 raise NotImplementedError
1288
1311
1289 @LazyProperty
1312 @LazyProperty
1290 def changed(self):
1313 def changed(self):
1291 """
1314 """
1292 Returns list of modified ``FileNode`` objects.
1315 Returns list of modified ``FileNode`` objects.
1293 """
1316 """
1294 raise NotImplementedError
1317 raise NotImplementedError
1295
1318
1296 @LazyProperty
1319 @LazyProperty
1297 def removed(self):
1320 def removed(self):
1298 """
1321 """
1299 Returns list of removed ``FileNode`` objects.
1322 Returns list of removed ``FileNode`` objects.
1300 """
1323 """
1301 raise NotImplementedError
1324 raise NotImplementedError
1302
1325
1303 @LazyProperty
1326 @LazyProperty
1304 def size(self):
1327 def size(self):
1305 """
1328 """
1306 Returns total number of bytes from contents of all filenodes.
1329 Returns total number of bytes from contents of all filenodes.
1307 """
1330 """
1308 return sum((node.size for node in self.get_filenodes_generator()))
1331 return sum((node.size for node in self.get_filenodes_generator()))
1309
1332
1310 def walk(self, topurl=''):
1333 def walk(self, topurl=''):
1311 """
1334 """
1312 Similar to os.walk method. Insted of filesystem it walks through
1335 Similar to os.walk method. Insted of filesystem it walks through
1313 commit starting at given ``topurl``. Returns generator of tuples
1336 commit starting at given ``topurl``. Returns generator of tuples
1314 (topnode, dirnodes, filenodes).
1337 (topnode, dirnodes, filenodes).
1315 """
1338 """
1316 topnode = self.get_node(topurl)
1339 topnode = self.get_node(topurl)
1317 if not topnode.is_dir():
1340 if not topnode.is_dir():
1318 return
1341 return
1319 yield (topnode, topnode.dirs, topnode.files)
1342 yield (topnode, topnode.dirs, topnode.files)
1320 for dirnode in topnode.dirs:
1343 for dirnode in topnode.dirs:
1321 for tup in self.walk(dirnode.path):
1344 for tup in self.walk(dirnode.path):
1322 yield tup
1345 yield tup
1323
1346
1324 def get_filenodes_generator(self):
1347 def get_filenodes_generator(self):
1325 """
1348 """
1326 Returns generator that yields *all* file nodes.
1349 Returns generator that yields *all* file nodes.
1327 """
1350 """
1328 for topnode, dirs, files in self.walk():
1351 for topnode, dirs, files in self.walk():
1329 for node in files:
1352 for node in files:
1330 yield node
1353 yield node
1331
1354
1332 #
1355 #
1333 # Utilities for sub classes to support consistent behavior
1356 # Utilities for sub classes to support consistent behavior
1334 #
1357 #
1335
1358
1336 def no_node_at_path(self, path):
1359 def no_node_at_path(self, path):
1337 return NodeDoesNotExistError(
1360 return NodeDoesNotExistError(
1338 u"There is no file nor directory at the given path: "
1361 u"There is no file nor directory at the given path: "
1339 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1362 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1340
1363
1341 def _fix_path(self, path):
1364 def _fix_path(self, path):
1342 """
1365 """
1343 Paths are stored without trailing slash so we need to get rid off it if
1366 Paths are stored without trailing slash so we need to get rid off it if
1344 needed.
1367 needed.
1345 """
1368 """
1346 return path.rstrip('/')
1369 return path.rstrip('/')
1347
1370
1348 #
1371 #
1349 # Deprecated API based on changesets
1372 # Deprecated API based on changesets
1350 #
1373 #
1351
1374
1352 @property
1375 @property
1353 def revision(self):
1376 def revision(self):
1354 warnings.warn("Use idx instead", DeprecationWarning)
1377 warnings.warn("Use idx instead", DeprecationWarning)
1355 return self.idx
1378 return self.idx
1356
1379
1357 @revision.setter
1380 @revision.setter
1358 def revision(self, value):
1381 def revision(self, value):
1359 warnings.warn("Use idx instead", DeprecationWarning)
1382 warnings.warn("Use idx instead", DeprecationWarning)
1360 self.idx = value
1383 self.idx = value
1361
1384
1362 def get_file_changeset(self, path):
1385 def get_file_changeset(self, path):
1363 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1386 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1364 return self.get_path_commit(path)
1387 return self.get_path_commit(path)
1365
1388
1366
1389
1367 class BaseChangesetClass(type):
1390 class BaseChangesetClass(type):
1368
1391
1369 def __instancecheck__(self, instance):
1392 def __instancecheck__(self, instance):
1370 return isinstance(instance, BaseCommit)
1393 return isinstance(instance, BaseCommit)
1371
1394
1372
1395
1373 class BaseChangeset(BaseCommit):
1396 class BaseChangeset(BaseCommit):
1374
1397
1375 __metaclass__ = BaseChangesetClass
1398 __metaclass__ = BaseChangesetClass
1376
1399
1377 def __new__(cls, *args, **kwargs):
1400 def __new__(cls, *args, **kwargs):
1378 warnings.warn(
1401 warnings.warn(
1379 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1402 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1380 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1403 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1381
1404
1382
1405
1383 class BaseInMemoryCommit(object):
1406 class BaseInMemoryCommit(object):
1384 """
1407 """
1385 Represents differences between repository's state (most recent head) and
1408 Represents differences between repository's state (most recent head) and
1386 changes made *in place*.
1409 changes made *in place*.
1387
1410
1388 **Attributes**
1411 **Attributes**
1389
1412
1390 ``repository``
1413 ``repository``
1391 repository object for this in-memory-commit
1414 repository object for this in-memory-commit
1392
1415
1393 ``added``
1416 ``added``
1394 list of ``FileNode`` objects marked as *added*
1417 list of ``FileNode`` objects marked as *added*
1395
1418
1396 ``changed``
1419 ``changed``
1397 list of ``FileNode`` objects marked as *changed*
1420 list of ``FileNode`` objects marked as *changed*
1398
1421
1399 ``removed``
1422 ``removed``
1400 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1423 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1401 *removed*
1424 *removed*
1402
1425
1403 ``parents``
1426 ``parents``
1404 list of :class:`BaseCommit` instances representing parents of
1427 list of :class:`BaseCommit` instances representing parents of
1405 in-memory commit. Should always be 2-element sequence.
1428 in-memory commit. Should always be 2-element sequence.
1406
1429
1407 """
1430 """
1408
1431
1409 def __init__(self, repository):
1432 def __init__(self, repository):
1410 self.repository = repository
1433 self.repository = repository
1411 self.added = []
1434 self.added = []
1412 self.changed = []
1435 self.changed = []
1413 self.removed = []
1436 self.removed = []
1414 self.parents = []
1437 self.parents = []
1415
1438
1416 def add(self, *filenodes):
1439 def add(self, *filenodes):
1417 """
1440 """
1418 Marks given ``FileNode`` objects as *to be committed*.
1441 Marks given ``FileNode`` objects as *to be committed*.
1419
1442
1420 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1443 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1421 latest commit
1444 latest commit
1422 :raises ``NodeAlreadyAddedError``: if node with same path is already
1445 :raises ``NodeAlreadyAddedError``: if node with same path is already
1423 marked as *added*
1446 marked as *added*
1424 """
1447 """
1425 # Check if not already marked as *added* first
1448 # Check if not already marked as *added* first
1426 for node in filenodes:
1449 for node in filenodes:
1427 if node.path in (n.path for n in self.added):
1450 if node.path in (n.path for n in self.added):
1428 raise NodeAlreadyAddedError(
1451 raise NodeAlreadyAddedError(
1429 "Such FileNode %s is already marked for addition"
1452 "Such FileNode %s is already marked for addition"
1430 % node.path)
1453 % node.path)
1431 for node in filenodes:
1454 for node in filenodes:
1432 self.added.append(node)
1455 self.added.append(node)
1433
1456
1434 def change(self, *filenodes):
1457 def change(self, *filenodes):
1435 """
1458 """
1436 Marks given ``FileNode`` objects to be *changed* in next commit.
1459 Marks given ``FileNode`` objects to be *changed* in next commit.
1437
1460
1438 :raises ``EmptyRepositoryError``: if there are no commits yet
1461 :raises ``EmptyRepositoryError``: if there are no commits yet
1439 :raises ``NodeAlreadyExistsError``: if node with same path is already
1462 :raises ``NodeAlreadyExistsError``: if node with same path is already
1440 marked to be *changed*
1463 marked to be *changed*
1441 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1464 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1442 marked to be *removed*
1465 marked to be *removed*
1443 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1466 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1444 commit
1467 commit
1445 :raises ``NodeNotChangedError``: if node hasn't really be changed
1468 :raises ``NodeNotChangedError``: if node hasn't really be changed
1446 """
1469 """
1447 for node in filenodes:
1470 for node in filenodes:
1448 if node.path in (n.path for n in self.removed):
1471 if node.path in (n.path for n in self.removed):
1449 raise NodeAlreadyRemovedError(
1472 raise NodeAlreadyRemovedError(
1450 "Node at %s is already marked as removed" % node.path)
1473 "Node at %s is already marked as removed" % node.path)
1451 try:
1474 try:
1452 self.repository.get_commit()
1475 self.repository.get_commit()
1453 except EmptyRepositoryError:
1476 except EmptyRepositoryError:
1454 raise EmptyRepositoryError(
1477 raise EmptyRepositoryError(
1455 "Nothing to change - try to *add* new nodes rather than "
1478 "Nothing to change - try to *add* new nodes rather than "
1456 "changing them")
1479 "changing them")
1457 for node in filenodes:
1480 for node in filenodes:
1458 if node.path in (n.path for n in self.changed):
1481 if node.path in (n.path for n in self.changed):
1459 raise NodeAlreadyChangedError(
1482 raise NodeAlreadyChangedError(
1460 "Node at '%s' is already marked as changed" % node.path)
1483 "Node at '%s' is already marked as changed" % node.path)
1461 self.changed.append(node)
1484 self.changed.append(node)
1462
1485
1463 def remove(self, *filenodes):
1486 def remove(self, *filenodes):
1464 """
1487 """
1465 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1488 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1466 *removed* in next commit.
1489 *removed* in next commit.
1467
1490
1468 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1491 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1469 be *removed*
1492 be *removed*
1470 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1493 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1471 be *changed*
1494 be *changed*
1472 """
1495 """
1473 for node in filenodes:
1496 for node in filenodes:
1474 if node.path in (n.path for n in self.removed):
1497 if node.path in (n.path for n in self.removed):
1475 raise NodeAlreadyRemovedError(
1498 raise NodeAlreadyRemovedError(
1476 "Node is already marked to for removal at %s" % node.path)
1499 "Node is already marked to for removal at %s" % node.path)
1477 if node.path in (n.path for n in self.changed):
1500 if node.path in (n.path for n in self.changed):
1478 raise NodeAlreadyChangedError(
1501 raise NodeAlreadyChangedError(
1479 "Node is already marked to be changed at %s" % node.path)
1502 "Node is already marked to be changed at %s" % node.path)
1480 # We only mark node as *removed* - real removal is done by
1503 # We only mark node as *removed* - real removal is done by
1481 # commit method
1504 # commit method
1482 self.removed.append(node)
1505 self.removed.append(node)
1483
1506
1484 def reset(self):
1507 def reset(self):
1485 """
1508 """
1486 Resets this instance to initial state (cleans ``added``, ``changed``
1509 Resets this instance to initial state (cleans ``added``, ``changed``
1487 and ``removed`` lists).
1510 and ``removed`` lists).
1488 """
1511 """
1489 self.added = []
1512 self.added = []
1490 self.changed = []
1513 self.changed = []
1491 self.removed = []
1514 self.removed = []
1492 self.parents = []
1515 self.parents = []
1493
1516
1494 def get_ipaths(self):
1517 def get_ipaths(self):
1495 """
1518 """
1496 Returns generator of paths from nodes marked as added, changed or
1519 Returns generator of paths from nodes marked as added, changed or
1497 removed.
1520 removed.
1498 """
1521 """
1499 for node in itertools.chain(self.added, self.changed, self.removed):
1522 for node in itertools.chain(self.added, self.changed, self.removed):
1500 yield node.path
1523 yield node.path
1501
1524
1502 def get_paths(self):
1525 def get_paths(self):
1503 """
1526 """
1504 Returns list of paths from nodes marked as added, changed or removed.
1527 Returns list of paths from nodes marked as added, changed or removed.
1505 """
1528 """
1506 return list(self.get_ipaths())
1529 return list(self.get_ipaths())
1507
1530
1508 def check_integrity(self, parents=None):
1531 def check_integrity(self, parents=None):
1509 """
1532 """
1510 Checks in-memory commit's integrity. Also, sets parents if not
1533 Checks in-memory commit's integrity. Also, sets parents if not
1511 already set.
1534 already set.
1512
1535
1513 :raises CommitError: if any error occurs (i.e.
1536 :raises CommitError: if any error occurs (i.e.
1514 ``NodeDoesNotExistError``).
1537 ``NodeDoesNotExistError``).
1515 """
1538 """
1516 if not self.parents:
1539 if not self.parents:
1517 parents = parents or []
1540 parents = parents or []
1518 if len(parents) == 0:
1541 if len(parents) == 0:
1519 try:
1542 try:
1520 parents = [self.repository.get_commit(), None]
1543 parents = [self.repository.get_commit(), None]
1521 except EmptyRepositoryError:
1544 except EmptyRepositoryError:
1522 parents = [None, None]
1545 parents = [None, None]
1523 elif len(parents) == 1:
1546 elif len(parents) == 1:
1524 parents += [None]
1547 parents += [None]
1525 self.parents = parents
1548 self.parents = parents
1526
1549
1527 # Local parents, only if not None
1550 # Local parents, only if not None
1528 parents = [p for p in self.parents if p]
1551 parents = [p for p in self.parents if p]
1529
1552
1530 # Check nodes marked as added
1553 # Check nodes marked as added
1531 for p in parents:
1554 for p in parents:
1532 for node in self.added:
1555 for node in self.added:
1533 try:
1556 try:
1534 p.get_node(node.path)
1557 p.get_node(node.path)
1535 except NodeDoesNotExistError:
1558 except NodeDoesNotExistError:
1536 pass
1559 pass
1537 else:
1560 else:
1538 raise NodeAlreadyExistsError(
1561 raise NodeAlreadyExistsError(
1539 "Node `%s` already exists at %s" % (node.path, p))
1562 "Node `%s` already exists at %s" % (node.path, p))
1540
1563
1541 # Check nodes marked as changed
1564 # Check nodes marked as changed
1542 missing = set(self.changed)
1565 missing = set(self.changed)
1543 not_changed = set(self.changed)
1566 not_changed = set(self.changed)
1544 if self.changed and not parents:
1567 if self.changed and not parents:
1545 raise NodeDoesNotExistError(str(self.changed[0].path))
1568 raise NodeDoesNotExistError(str(self.changed[0].path))
1546 for p in parents:
1569 for p in parents:
1547 for node in self.changed:
1570 for node in self.changed:
1548 try:
1571 try:
1549 old = p.get_node(node.path)
1572 old = p.get_node(node.path)
1550 missing.remove(node)
1573 missing.remove(node)
1551 # if content actually changed, remove node from not_changed
1574 # if content actually changed, remove node from not_changed
1552 if old.content != node.content:
1575 if old.content != node.content:
1553 not_changed.remove(node)
1576 not_changed.remove(node)
1554 except NodeDoesNotExistError:
1577 except NodeDoesNotExistError:
1555 pass
1578 pass
1556 if self.changed and missing:
1579 if self.changed and missing:
1557 raise NodeDoesNotExistError(
1580 raise NodeDoesNotExistError(
1558 "Node `%s` marked as modified but missing in parents: %s"
1581 "Node `%s` marked as modified but missing in parents: %s"
1559 % (node.path, parents))
1582 % (node.path, parents))
1560
1583
1561 if self.changed and not_changed:
1584 if self.changed and not_changed:
1562 raise NodeNotChangedError(
1585 raise NodeNotChangedError(
1563 "Node `%s` wasn't actually changed (parents: %s)"
1586 "Node `%s` wasn't actually changed (parents: %s)"
1564 % (not_changed.pop().path, parents))
1587 % (not_changed.pop().path, parents))
1565
1588
1566 # Check nodes marked as removed
1589 # Check nodes marked as removed
1567 if self.removed and not parents:
1590 if self.removed and not parents:
1568 raise NodeDoesNotExistError(
1591 raise NodeDoesNotExistError(
1569 "Cannot remove node at %s as there "
1592 "Cannot remove node at %s as there "
1570 "were no parents specified" % self.removed[0].path)
1593 "were no parents specified" % self.removed[0].path)
1571 really_removed = set()
1594 really_removed = set()
1572 for p in parents:
1595 for p in parents:
1573 for node in self.removed:
1596 for node in self.removed:
1574 try:
1597 try:
1575 p.get_node(node.path)
1598 p.get_node(node.path)
1576 really_removed.add(node)
1599 really_removed.add(node)
1577 except CommitError:
1600 except CommitError:
1578 pass
1601 pass
1579 not_removed = set(self.removed) - really_removed
1602 not_removed = set(self.removed) - really_removed
1580 if not_removed:
1603 if not_removed:
1581 # TODO: johbo: This code branch does not seem to be covered
1604 # TODO: johbo: This code branch does not seem to be covered
1582 raise NodeDoesNotExistError(
1605 raise NodeDoesNotExistError(
1583 "Cannot remove node at %s from "
1606 "Cannot remove node at %s from "
1584 "following parents: %s" % (not_removed, parents))
1607 "following parents: %s" % (not_removed, parents))
1585
1608
1586 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1609 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1587 """
1610 """
1588 Performs in-memory commit (doesn't check workdir in any way) and
1611 Performs in-memory commit (doesn't check workdir in any way) and
1589 returns newly created :class:`BaseCommit`. Updates repository's
1612 returns newly created :class:`BaseCommit`. Updates repository's
1590 attribute `commits`.
1613 attribute `commits`.
1591
1614
1592 .. note::
1615 .. note::
1593
1616
1594 While overriding this method each backend's should call
1617 While overriding this method each backend's should call
1595 ``self.check_integrity(parents)`` in the first place.
1618 ``self.check_integrity(parents)`` in the first place.
1596
1619
1597 :param message: message of the commit
1620 :param message: message of the commit
1598 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1621 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1599 :param parents: single parent or sequence of parents from which commit
1622 :param parents: single parent or sequence of parents from which commit
1600 would be derived
1623 would be derived
1601 :param date: ``datetime.datetime`` instance. Defaults to
1624 :param date: ``datetime.datetime`` instance. Defaults to
1602 ``datetime.datetime.now()``.
1625 ``datetime.datetime.now()``.
1603 :param branch: branch name, as string. If none given, default backend's
1626 :param branch: branch name, as string. If none given, default backend's
1604 branch would be used.
1627 branch would be used.
1605
1628
1606 :raises ``CommitError``: if any error occurs while committing
1629 :raises ``CommitError``: if any error occurs while committing
1607 """
1630 """
1608 raise NotImplementedError
1631 raise NotImplementedError
1609
1632
1610
1633
1611 class BaseInMemoryChangesetClass(type):
1634 class BaseInMemoryChangesetClass(type):
1612
1635
1613 def __instancecheck__(self, instance):
1636 def __instancecheck__(self, instance):
1614 return isinstance(instance, BaseInMemoryCommit)
1637 return isinstance(instance, BaseInMemoryCommit)
1615
1638
1616
1639
1617 class BaseInMemoryChangeset(BaseInMemoryCommit):
1640 class BaseInMemoryChangeset(BaseInMemoryCommit):
1618
1641
1619 __metaclass__ = BaseInMemoryChangesetClass
1642 __metaclass__ = BaseInMemoryChangesetClass
1620
1643
1621 def __new__(cls, *args, **kwargs):
1644 def __new__(cls, *args, **kwargs):
1622 warnings.warn(
1645 warnings.warn(
1623 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1646 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1624 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1647 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1625
1648
1626
1649
1627 class EmptyCommit(BaseCommit):
1650 class EmptyCommit(BaseCommit):
1628 """
1651 """
1629 An dummy empty commit. It's possible to pass hash when creating
1652 An dummy empty commit. It's possible to pass hash when creating
1630 an EmptyCommit
1653 an EmptyCommit
1631 """
1654 """
1632
1655
1633 def __init__(
1656 def __init__(
1634 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1657 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1635 message='', author='', date=None):
1658 message='', author='', date=None):
1636 self._empty_commit_id = commit_id
1659 self._empty_commit_id = commit_id
1637 # TODO: johbo: Solve idx parameter, default value does not make
1660 # TODO: johbo: Solve idx parameter, default value does not make
1638 # too much sense
1661 # too much sense
1639 self.idx = idx
1662 self.idx = idx
1640 self.message = message
1663 self.message = message
1641 self.author = author
1664 self.author = author
1642 self.date = date or datetime.datetime.fromtimestamp(0)
1665 self.date = date or datetime.datetime.fromtimestamp(0)
1643 self.repository = repo
1666 self.repository = repo
1644 self.alias = alias
1667 self.alias = alias
1645
1668
1646 @LazyProperty
1669 @LazyProperty
1647 def raw_id(self):
1670 def raw_id(self):
1648 """
1671 """
1649 Returns raw string identifying this commit, useful for web
1672 Returns raw string identifying this commit, useful for web
1650 representation.
1673 representation.
1651 """
1674 """
1652
1675
1653 return self._empty_commit_id
1676 return self._empty_commit_id
1654
1677
1655 @LazyProperty
1678 @LazyProperty
1656 def branch(self):
1679 def branch(self):
1657 if self.alias:
1680 if self.alias:
1658 from rhodecode.lib.vcs.backends import get_backend
1681 from rhodecode.lib.vcs.backends import get_backend
1659 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1682 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1660
1683
1661 @LazyProperty
1684 @LazyProperty
1662 def short_id(self):
1685 def short_id(self):
1663 return self.raw_id[:12]
1686 return self.raw_id[:12]
1664
1687
1665 @LazyProperty
1688 @LazyProperty
1666 def id(self):
1689 def id(self):
1667 return self.raw_id
1690 return self.raw_id
1668
1691
1669 def get_path_commit(self, path):
1692 def get_path_commit(self, path):
1670 return self
1693 return self
1671
1694
1672 def get_file_content(self, path):
1695 def get_file_content(self, path):
1673 return u''
1696 return u''
1674
1697
1675 def get_file_content_streamed(self, path):
1698 def get_file_content_streamed(self, path):
1676 yield self.get_file_content()
1699 yield self.get_file_content()
1677
1700
1678 def get_file_size(self, path):
1701 def get_file_size(self, path):
1679 return 0
1702 return 0
1680
1703
1681
1704
1682 class EmptyChangesetClass(type):
1705 class EmptyChangesetClass(type):
1683
1706
1684 def __instancecheck__(self, instance):
1707 def __instancecheck__(self, instance):
1685 return isinstance(instance, EmptyCommit)
1708 return isinstance(instance, EmptyCommit)
1686
1709
1687
1710
1688 class EmptyChangeset(EmptyCommit):
1711 class EmptyChangeset(EmptyCommit):
1689
1712
1690 __metaclass__ = EmptyChangesetClass
1713 __metaclass__ = EmptyChangesetClass
1691
1714
1692 def __new__(cls, *args, **kwargs):
1715 def __new__(cls, *args, **kwargs):
1693 warnings.warn(
1716 warnings.warn(
1694 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1717 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1695 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1718 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1696
1719
1697 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1720 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1698 alias=None, revision=-1, message='', author='', date=None):
1721 alias=None, revision=-1, message='', author='', date=None):
1699 if requested_revision is not None:
1722 if requested_revision is not None:
1700 warnings.warn(
1723 warnings.warn(
1701 "Parameter requested_revision not supported anymore",
1724 "Parameter requested_revision not supported anymore",
1702 DeprecationWarning)
1725 DeprecationWarning)
1703 super(EmptyChangeset, self).__init__(
1726 super(EmptyChangeset, self).__init__(
1704 commit_id=cs, repo=repo, alias=alias, idx=revision,
1727 commit_id=cs, repo=repo, alias=alias, idx=revision,
1705 message=message, author=author, date=date)
1728 message=message, author=author, date=date)
1706
1729
1707 @property
1730 @property
1708 def revision(self):
1731 def revision(self):
1709 warnings.warn("Use idx instead", DeprecationWarning)
1732 warnings.warn("Use idx instead", DeprecationWarning)
1710 return self.idx
1733 return self.idx
1711
1734
1712 @revision.setter
1735 @revision.setter
1713 def revision(self, value):
1736 def revision(self, value):
1714 warnings.warn("Use idx instead", DeprecationWarning)
1737 warnings.warn("Use idx instead", DeprecationWarning)
1715 self.idx = value
1738 self.idx = value
1716
1739
1717
1740
1718 class EmptyRepository(BaseRepository):
1741 class EmptyRepository(BaseRepository):
1719 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1742 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1720 pass
1743 pass
1721
1744
1722 def get_diff(self, *args, **kwargs):
1745 def get_diff(self, *args, **kwargs):
1723 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1746 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1724 return GitDiff('')
1747 return GitDiff('')
1725
1748
1726
1749
1727 class CollectionGenerator(object):
1750 class CollectionGenerator(object):
1728
1751
1729 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1752 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1730 self.repo = repo
1753 self.repo = repo
1731 self.commit_ids = commit_ids
1754 self.commit_ids = commit_ids
1732 # TODO: (oliver) this isn't currently hooked up
1755 # TODO: (oliver) this isn't currently hooked up
1733 self.collection_size = None
1756 self.collection_size = None
1734 self.pre_load = pre_load
1757 self.pre_load = pre_load
1735 self.translate_tag = translate_tag
1758 self.translate_tag = translate_tag
1736
1759
1737 def __len__(self):
1760 def __len__(self):
1738 if self.collection_size is not None:
1761 if self.collection_size is not None:
1739 return self.collection_size
1762 return self.collection_size
1740 return self.commit_ids.__len__()
1763 return self.commit_ids.__len__()
1741
1764
1742 def __iter__(self):
1765 def __iter__(self):
1743 for commit_id in self.commit_ids:
1766 for commit_id in self.commit_ids:
1744 # TODO: johbo: Mercurial passes in commit indices or commit ids
1767 # TODO: johbo: Mercurial passes in commit indices or commit ids
1745 yield self._commit_factory(commit_id)
1768 yield self._commit_factory(commit_id)
1746
1769
1747 def _commit_factory(self, commit_id):
1770 def _commit_factory(self, commit_id):
1748 """
1771 """
1749 Allows backends to override the way commits are generated.
1772 Allows backends to override the way commits are generated.
1750 """
1773 """
1751 return self.repo.get_commit(
1774 return self.repo.get_commit(
1752 commit_id=commit_id, pre_load=self.pre_load,
1775 commit_id=commit_id, pre_load=self.pre_load,
1753 translate_tag=self.translate_tag)
1776 translate_tag=self.translate_tag)
1754
1777
1755 def __getslice__(self, i, j):
1778 def __getslice__(self, i, j):
1756 """
1779 """
1757 Returns an iterator of sliced repository
1780 Returns an iterator of sliced repository
1758 """
1781 """
1759 commit_ids = self.commit_ids[i:j]
1782 commit_ids = self.commit_ids[i:j]
1760 return self.__class__(
1783 return self.__class__(
1761 self.repo, commit_ids, pre_load=self.pre_load,
1784 self.repo, commit_ids, pre_load=self.pre_load,
1762 translate_tag=self.translate_tag)
1785 translate_tag=self.translate_tag)
1763
1786
1764 def __repr__(self):
1787 def __repr__(self):
1765 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1788 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1766
1789
1767
1790
1768 class Config(object):
1791 class Config(object):
1769 """
1792 """
1770 Represents the configuration for a repository.
1793 Represents the configuration for a repository.
1771
1794
1772 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1795 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1773 standard library. It implements only the needed subset.
1796 standard library. It implements only the needed subset.
1774 """
1797 """
1775
1798
1776 def __init__(self):
1799 def __init__(self):
1777 self._values = {}
1800 self._values = {}
1778
1801
1779 def copy(self):
1802 def copy(self):
1780 clone = Config()
1803 clone = Config()
1781 for section, values in self._values.items():
1804 for section, values in self._values.items():
1782 clone._values[section] = values.copy()
1805 clone._values[section] = values.copy()
1783 return clone
1806 return clone
1784
1807
1785 def __repr__(self):
1808 def __repr__(self):
1786 return '<Config(%s sections) at %s>' % (
1809 return '<Config(%s sections) at %s>' % (
1787 len(self._values), hex(id(self)))
1810 len(self._values), hex(id(self)))
1788
1811
1789 def items(self, section):
1812 def items(self, section):
1790 return self._values.get(section, {}).iteritems()
1813 return self._values.get(section, {}).iteritems()
1791
1814
1792 def get(self, section, option):
1815 def get(self, section, option):
1793 return self._values.get(section, {}).get(option)
1816 return self._values.get(section, {}).get(option)
1794
1817
1795 def set(self, section, option, value):
1818 def set(self, section, option, value):
1796 section_values = self._values.setdefault(section, {})
1819 section_values = self._values.setdefault(section, {})
1797 section_values[option] = value
1820 section_values[option] = value
1798
1821
1799 def clear_section(self, section):
1822 def clear_section(self, section):
1800 self._values[section] = {}
1823 self._values[section] = {}
1801
1824
1802 def serialize(self):
1825 def serialize(self):
1803 """
1826 """
1804 Creates a list of three tuples (section, key, value) representing
1827 Creates a list of three tuples (section, key, value) representing
1805 this config object.
1828 this config object.
1806 """
1829 """
1807 items = []
1830 items = []
1808 for section in self._values:
1831 for section in self._values:
1809 for option, value in self._values[section].items():
1832 for option, value in self._values[section].items():
1810 items.append(
1833 items.append(
1811 (safe_str(section), safe_str(option), safe_str(value)))
1834 (safe_str(section), safe_str(option), safe_str(value)))
1812 return items
1835 return items
1813
1836
1814
1837
1815 class Diff(object):
1838 class Diff(object):
1816 """
1839 """
1817 Represents a diff result from a repository backend.
1840 Represents a diff result from a repository backend.
1818
1841
1819 Subclasses have to provide a backend specific value for
1842 Subclasses have to provide a backend specific value for
1820 :attr:`_header_re` and :attr:`_meta_re`.
1843 :attr:`_header_re` and :attr:`_meta_re`.
1821 """
1844 """
1822 _meta_re = None
1845 _meta_re = None
1823 _header_re = None
1846 _header_re = None
1824
1847
1825 def __init__(self, raw_diff):
1848 def __init__(self, raw_diff):
1826 self.raw = raw_diff
1849 self.raw = raw_diff
1827
1850
1828 def chunks(self):
1851 def chunks(self):
1829 """
1852 """
1830 split the diff in chunks of separate --git a/file b/file chunks
1853 split the diff in chunks of separate --git a/file b/file chunks
1831 to make diffs consistent we must prepend with \n, and make sure
1854 to make diffs consistent we must prepend with \n, and make sure
1832 we can detect last chunk as this was also has special rule
1855 we can detect last chunk as this was also has special rule
1833 """
1856 """
1834
1857
1835 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1858 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1836 header = diff_parts[0]
1859 header = diff_parts[0]
1837
1860
1838 if self._meta_re:
1861 if self._meta_re:
1839 match = self._meta_re.match(header)
1862 match = self._meta_re.match(header)
1840
1863
1841 chunks = diff_parts[1:]
1864 chunks = diff_parts[1:]
1842 total_chunks = len(chunks)
1865 total_chunks = len(chunks)
1843
1866
1844 return (
1867 return (
1845 DiffChunk(chunk, self, cur_chunk == total_chunks)
1868 DiffChunk(chunk, self, cur_chunk == total_chunks)
1846 for cur_chunk, chunk in enumerate(chunks, start=1))
1869 for cur_chunk, chunk in enumerate(chunks, start=1))
1847
1870
1848
1871
1849 class DiffChunk(object):
1872 class DiffChunk(object):
1850
1873
1851 def __init__(self, chunk, diff, last_chunk):
1874 def __init__(self, chunk, diff, last_chunk):
1852 self._diff = diff
1875 self._diff = diff
1853
1876
1854 # since we split by \ndiff --git that part is lost from original diff
1877 # since we split by \ndiff --git that part is lost from original diff
1855 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1878 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1856 if not last_chunk:
1879 if not last_chunk:
1857 chunk += '\n'
1880 chunk += '\n'
1858
1881
1859 match = self._diff._header_re.match(chunk)
1882 match = self._diff._header_re.match(chunk)
1860 self.header = match.groupdict()
1883 self.header = match.groupdict()
1861 self.diff = chunk[match.end():]
1884 self.diff = chunk[match.end():]
1862 self.raw = chunk
1885 self.raw = chunk
1863
1886
1864
1887
1865 class BasePathPermissionChecker(object):
1888 class BasePathPermissionChecker(object):
1866
1889
1867 @staticmethod
1890 @staticmethod
1868 def create_from_patterns(includes, excludes):
1891 def create_from_patterns(includes, excludes):
1869 if includes and '*' in includes and not excludes:
1892 if includes and '*' in includes and not excludes:
1870 return AllPathPermissionChecker()
1893 return AllPathPermissionChecker()
1871 elif excludes and '*' in excludes:
1894 elif excludes and '*' in excludes:
1872 return NonePathPermissionChecker()
1895 return NonePathPermissionChecker()
1873 else:
1896 else:
1874 return PatternPathPermissionChecker(includes, excludes)
1897 return PatternPathPermissionChecker(includes, excludes)
1875
1898
1876 @property
1899 @property
1877 def has_full_access(self):
1900 def has_full_access(self):
1878 raise NotImplemented()
1901 raise NotImplemented()
1879
1902
1880 def has_access(self, path):
1903 def has_access(self, path):
1881 raise NotImplemented()
1904 raise NotImplemented()
1882
1905
1883
1906
1884 class AllPathPermissionChecker(BasePathPermissionChecker):
1907 class AllPathPermissionChecker(BasePathPermissionChecker):
1885
1908
1886 @property
1909 @property
1887 def has_full_access(self):
1910 def has_full_access(self):
1888 return True
1911 return True
1889
1912
1890 def has_access(self, path):
1913 def has_access(self, path):
1891 return True
1914 return True
1892
1915
1893
1916
1894 class NonePathPermissionChecker(BasePathPermissionChecker):
1917 class NonePathPermissionChecker(BasePathPermissionChecker):
1895
1918
1896 @property
1919 @property
1897 def has_full_access(self):
1920 def has_full_access(self):
1898 return False
1921 return False
1899
1922
1900 def has_access(self, path):
1923 def has_access(self, path):
1901 return False
1924 return False
1902
1925
1903
1926
1904 class PatternPathPermissionChecker(BasePathPermissionChecker):
1927 class PatternPathPermissionChecker(BasePathPermissionChecker):
1905
1928
1906 def __init__(self, includes, excludes):
1929 def __init__(self, includes, excludes):
1907 self.includes = includes
1930 self.includes = includes
1908 self.excludes = excludes
1931 self.excludes = excludes
1909 self.includes_re = [] if not includes else [
1932 self.includes_re = [] if not includes else [
1910 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1933 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1911 self.excludes_re = [] if not excludes else [
1934 self.excludes_re = [] if not excludes else [
1912 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1935 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1913
1936
1914 @property
1937 @property
1915 def has_full_access(self):
1938 def has_full_access(self):
1916 return '*' in self.includes and not self.excludes
1939 return '*' in self.includes and not self.excludes
1917
1940
1918 def has_access(self, path):
1941 def has_access(self, path):
1919 for regex in self.excludes_re:
1942 for regex in self.excludes_re:
1920 if regex.match(path):
1943 if regex.match(path):
1921 return False
1944 return False
1922 for regex in self.includes_re:
1945 for regex in self.includes_re:
1923 if regex.match(path):
1946 if regex.match(path):
1924 return True
1947 return True
1925 return False
1948 return False
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now