##// END OF EJS Templates
audit-logs: store properly IP and user for certain comments types....
marcink -
r2728:9f3cefa1 default
parent child Browse files
Show More
@@ -1,903 +1,904 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124
124
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if Optional.extract(repoid):
126 if Optional.extract(repoid):
127 repo = get_repo_or_error(repoid)
127 repo = get_repo_or_error(repoid)
128 else:
128 else:
129 repo = pull_request.target_repo
129 repo = pull_request.target_repo
130
130
131 if not PullRequestModel().check_user_read(
131 if not PullRequestModel().check_user_read(
132 pull_request, apiuser, api=True):
132 pull_request, apiuser, api=True):
133 raise JSONRPCError('repository `%s` or pull request `%s` '
133 raise JSONRPCError('repository `%s` or pull request `%s` '
134 'does not exist' % (repoid, pullrequestid))
134 'does not exist' % (repoid, pullrequestid))
135 data = pull_request.get_api_data()
135 data = pull_request.get_api_data()
136 return data
136 return data
137
137
138
138
139 @jsonrpc_method()
139 @jsonrpc_method()
140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
141 """
141 """
142 Get all pull requests from the repository specified in `repoid`.
142 Get all pull requests from the repository specified in `repoid`.
143
143
144 :param apiuser: This is filled automatically from the |authtoken|.
144 :param apiuser: This is filled automatically from the |authtoken|.
145 :type apiuser: AuthUser
145 :type apiuser: AuthUser
146 :param repoid: Optional repository name or repository ID.
146 :param repoid: Optional repository name or repository ID.
147 :type repoid: str or int
147 :type repoid: str or int
148 :param status: Only return pull requests with the specified status.
148 :param status: Only return pull requests with the specified status.
149 Valid options are.
149 Valid options are.
150 * ``new`` (default)
150 * ``new`` (default)
151 * ``open``
151 * ``open``
152 * ``closed``
152 * ``closed``
153 :type status: str
153 :type status: str
154
154
155 Example output:
155 Example output:
156
156
157 .. code-block:: bash
157 .. code-block:: bash
158
158
159 "id": <id_given_in_input>,
159 "id": <id_given_in_input>,
160 "result":
160 "result":
161 [
161 [
162 ...
162 ...
163 {
163 {
164 "pull_request_id": "<pull_request_id>",
164 "pull_request_id": "<pull_request_id>",
165 "url": "<url>",
165 "url": "<url>",
166 "title" : "<title>",
166 "title" : "<title>",
167 "description": "<description>",
167 "description": "<description>",
168 "status": "<status>",
168 "status": "<status>",
169 "created_on": "<date_time_created>",
169 "created_on": "<date_time_created>",
170 "updated_on": "<date_time_updated>",
170 "updated_on": "<date_time_updated>",
171 "commit_ids": [
171 "commit_ids": [
172 ...
172 ...
173 "<commit_id>",
173 "<commit_id>",
174 "<commit_id>",
174 "<commit_id>",
175 ...
175 ...
176 ],
176 ],
177 "review_status": "<review_status>",
177 "review_status": "<review_status>",
178 "mergeable": {
178 "mergeable": {
179 "status": "<bool>",
179 "status": "<bool>",
180 "message: "<message>",
180 "message: "<message>",
181 },
181 },
182 "source": {
182 "source": {
183 "clone_url": "<clone_url>",
183 "clone_url": "<clone_url>",
184 "reference":
184 "reference":
185 {
185 {
186 "name": "<name>",
186 "name": "<name>",
187 "type": "<type>",
187 "type": "<type>",
188 "commit_id": "<commit_id>",
188 "commit_id": "<commit_id>",
189 }
189 }
190 },
190 },
191 "target": {
191 "target": {
192 "clone_url": "<clone_url>",
192 "clone_url": "<clone_url>",
193 "reference":
193 "reference":
194 {
194 {
195 "name": "<name>",
195 "name": "<name>",
196 "type": "<type>",
196 "type": "<type>",
197 "commit_id": "<commit_id>",
197 "commit_id": "<commit_id>",
198 }
198 }
199 },
199 },
200 "merge": {
200 "merge": {
201 "clone_url": "<clone_url>",
201 "clone_url": "<clone_url>",
202 "reference":
202 "reference":
203 {
203 {
204 "name": "<name>",
204 "name": "<name>",
205 "type": "<type>",
205 "type": "<type>",
206 "commit_id": "<commit_id>",
206 "commit_id": "<commit_id>",
207 }
207 }
208 },
208 },
209 "author": <user_obj>,
209 "author": <user_obj>,
210 "reviewers": [
210 "reviewers": [
211 ...
211 ...
212 {
212 {
213 "user": "<user_obj>",
213 "user": "<user_obj>",
214 "review_status": "<review_status>",
214 "review_status": "<review_status>",
215 }
215 }
216 ...
216 ...
217 ]
217 ]
218 }
218 }
219 ...
219 ...
220 ],
220 ],
221 "error": null
221 "error": null
222
222
223 """
223 """
224 repo = get_repo_or_error(repoid)
224 repo = get_repo_or_error(repoid)
225 if not has_superadmin_permission(apiuser):
225 if not has_superadmin_permission(apiuser):
226 _perms = (
226 _perms = (
227 'repository.admin', 'repository.write', 'repository.read',)
227 'repository.admin', 'repository.write', 'repository.read',)
228 validate_repo_permissions(apiuser, repoid, repo, _perms)
228 validate_repo_permissions(apiuser, repoid, repo, _perms)
229
229
230 status = Optional.extract(status)
230 status = Optional.extract(status)
231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
232 data = [pr.get_api_data() for pr in pull_requests]
232 data = [pr.get_api_data() for pr in pull_requests]
233 return data
233 return data
234
234
235
235
236 @jsonrpc_method()
236 @jsonrpc_method()
237 def merge_pull_request(
237 def merge_pull_request(
238 request, apiuser, pullrequestid, repoid=Optional(None),
238 request, apiuser, pullrequestid, repoid=Optional(None),
239 userid=Optional(OAttr('apiuser'))):
239 userid=Optional(OAttr('apiuser'))):
240 """
240 """
241 Merge the pull request specified by `pullrequestid` into its target
241 Merge the pull request specified by `pullrequestid` into its target
242 repository.
242 repository.
243
243
244 :param apiuser: This is filled automatically from the |authtoken|.
244 :param apiuser: This is filled automatically from the |authtoken|.
245 :type apiuser: AuthUser
245 :type apiuser: AuthUser
246 :param repoid: Optional, repository name or repository ID of the
246 :param repoid: Optional, repository name or repository ID of the
247 target repository to which the |pr| is to be merged.
247 target repository to which the |pr| is to be merged.
248 :type repoid: str or int
248 :type repoid: str or int
249 :param pullrequestid: ID of the pull request which shall be merged.
249 :param pullrequestid: ID of the pull request which shall be merged.
250 :type pullrequestid: int
250 :type pullrequestid: int
251 :param userid: Merge the pull request as this user.
251 :param userid: Merge the pull request as this user.
252 :type userid: Optional(str or int)
252 :type userid: Optional(str or int)
253
253
254 Example output:
254 Example output:
255
255
256 .. code-block:: bash
256 .. code-block:: bash
257
257
258 "id": <id_given_in_input>,
258 "id": <id_given_in_input>,
259 "result": {
259 "result": {
260 "executed": "<bool>",
260 "executed": "<bool>",
261 "failure_reason": "<int>",
261 "failure_reason": "<int>",
262 "merge_commit_id": "<merge_commit_id>",
262 "merge_commit_id": "<merge_commit_id>",
263 "possible": "<bool>",
263 "possible": "<bool>",
264 "merge_ref": {
264 "merge_ref": {
265 "commit_id": "<commit_id>",
265 "commit_id": "<commit_id>",
266 "type": "<type>",
266 "type": "<type>",
267 "name": "<name>"
267 "name": "<name>"
268 }
268 }
269 },
269 },
270 "error": null
270 "error": null
271 """
271 """
272 pull_request = get_pull_request_or_error(pullrequestid)
272 pull_request = get_pull_request_or_error(pullrequestid)
273 if Optional.extract(repoid):
273 if Optional.extract(repoid):
274 repo = get_repo_or_error(repoid)
274 repo = get_repo_or_error(repoid)
275 else:
275 else:
276 repo = pull_request.target_repo
276 repo = pull_request.target_repo
277
277
278 if not isinstance(userid, Optional):
278 if not isinstance(userid, Optional):
279 if (has_superadmin_permission(apiuser) or
279 if (has_superadmin_permission(apiuser) or
280 HasRepoPermissionAnyApi('repository.admin')(
280 HasRepoPermissionAnyApi('repository.admin')(
281 user=apiuser, repo_name=repo.repo_name)):
281 user=apiuser, repo_name=repo.repo_name)):
282 apiuser = get_user_or_error(userid)
282 apiuser = get_user_or_error(userid)
283 else:
283 else:
284 raise JSONRPCError('userid is not the same as your user')
284 raise JSONRPCError('userid is not the same as your user')
285
285
286 check = MergeCheck.validate(
286 check = MergeCheck.validate(
287 pull_request, user=apiuser, translator=request.translate)
287 pull_request, user=apiuser, translator=request.translate)
288 merge_possible = not check.failed
288 merge_possible = not check.failed
289
289
290 if not merge_possible:
290 if not merge_possible:
291 error_messages = []
291 error_messages = []
292 for err_type, error_msg in check.errors:
292 for err_type, error_msg in check.errors:
293 error_msg = request.translate(error_msg)
293 error_msg = request.translate(error_msg)
294 error_messages.append(error_msg)
294 error_messages.append(error_msg)
295
295
296 reasons = ','.join(error_messages)
296 reasons = ','.join(error_messages)
297 raise JSONRPCError(
297 raise JSONRPCError(
298 'merge not possible for following reasons: {}'.format(reasons))
298 'merge not possible for following reasons: {}'.format(reasons))
299
299
300 target_repo = pull_request.target_repo
300 target_repo = pull_request.target_repo
301 extras = vcs_operation_context(
301 extras = vcs_operation_context(
302 request.environ, repo_name=target_repo.repo_name,
302 request.environ, repo_name=target_repo.repo_name,
303 username=apiuser.username, action='push',
303 username=apiuser.username, action='push',
304 scm=target_repo.repo_type)
304 scm=target_repo.repo_type)
305 merge_response = PullRequestModel().merge(
305 merge_response = PullRequestModel().merge(
306 pull_request, apiuser, extras=extras)
306 pull_request, apiuser, extras=extras)
307 if merge_response.executed:
307 if merge_response.executed:
308 PullRequestModel().close_pull_request(
308 PullRequestModel().close_pull_request(
309 pull_request.pull_request_id, apiuser)
309 pull_request.pull_request_id, apiuser)
310
310
311 Session().commit()
311 Session().commit()
312
312
313 # In previous versions the merge response directly contained the merge
313 # In previous versions the merge response directly contained the merge
314 # commit id. It is now contained in the merge reference object. To be
314 # commit id. It is now contained in the merge reference object. To be
315 # backwards compatible we have to extract it again.
315 # backwards compatible we have to extract it again.
316 merge_response = merge_response._asdict()
316 merge_response = merge_response._asdict()
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318
318
319 return merge_response
319 return merge_response
320
320
321
321
322 @jsonrpc_method()
322 @jsonrpc_method()
323 def get_pull_request_comments(
323 def get_pull_request_comments(
324 request, apiuser, pullrequestid, repoid=Optional(None)):
324 request, apiuser, pullrequestid, repoid=Optional(None)):
325 """
325 """
326 Get all comments of pull request specified with the `pullrequestid`
326 Get all comments of pull request specified with the `pullrequestid`
327
327
328 :param apiuser: This is filled automatically from the |authtoken|.
328 :param apiuser: This is filled automatically from the |authtoken|.
329 :type apiuser: AuthUser
329 :type apiuser: AuthUser
330 :param repoid: Optional repository name or repository ID.
330 :param repoid: Optional repository name or repository ID.
331 :type repoid: str or int
331 :type repoid: str or int
332 :param pullrequestid: The pull request ID.
332 :param pullrequestid: The pull request ID.
333 :type pullrequestid: int
333 :type pullrequestid: int
334
334
335 Example output:
335 Example output:
336
336
337 .. code-block:: bash
337 .. code-block:: bash
338
338
339 id : <id_given_in_input>
339 id : <id_given_in_input>
340 result : [
340 result : [
341 {
341 {
342 "comment_author": {
342 "comment_author": {
343 "active": true,
343 "active": true,
344 "full_name_or_username": "Tom Gore",
344 "full_name_or_username": "Tom Gore",
345 "username": "admin"
345 "username": "admin"
346 },
346 },
347 "comment_created_on": "2017-01-02T18:43:45.533",
347 "comment_created_on": "2017-01-02T18:43:45.533",
348 "comment_f_path": null,
348 "comment_f_path": null,
349 "comment_id": 25,
349 "comment_id": 25,
350 "comment_lineno": null,
350 "comment_lineno": null,
351 "comment_status": {
351 "comment_status": {
352 "status": "under_review",
352 "status": "under_review",
353 "status_lbl": "Under Review"
353 "status_lbl": "Under Review"
354 },
354 },
355 "comment_text": "Example text",
355 "comment_text": "Example text",
356 "comment_type": null,
356 "comment_type": null,
357 "pull_request_version": null
357 "pull_request_version": null
358 }
358 }
359 ],
359 ],
360 error : null
360 error : null
361 """
361 """
362
362
363 pull_request = get_pull_request_or_error(pullrequestid)
363 pull_request = get_pull_request_or_error(pullrequestid)
364 if Optional.extract(repoid):
364 if Optional.extract(repoid):
365 repo = get_repo_or_error(repoid)
365 repo = get_repo_or_error(repoid)
366 else:
366 else:
367 repo = pull_request.target_repo
367 repo = pull_request.target_repo
368
368
369 if not PullRequestModel().check_user_read(
369 if not PullRequestModel().check_user_read(
370 pull_request, apiuser, api=True):
370 pull_request, apiuser, api=True):
371 raise JSONRPCError('repository `%s` or pull request `%s` '
371 raise JSONRPCError('repository `%s` or pull request `%s` '
372 'does not exist' % (repoid, pullrequestid))
372 'does not exist' % (repoid, pullrequestid))
373
373
374 (pull_request_latest,
374 (pull_request_latest,
375 pull_request_at_ver,
375 pull_request_at_ver,
376 pull_request_display_obj,
376 pull_request_display_obj,
377 at_version) = PullRequestModel().get_pr_version(
377 at_version) = PullRequestModel().get_pr_version(
378 pull_request.pull_request_id, version=None)
378 pull_request.pull_request_id, version=None)
379
379
380 versions = pull_request_display_obj.versions()
380 versions = pull_request_display_obj.versions()
381 ver_map = {
381 ver_map = {
382 ver.pull_request_version_id: cnt
382 ver.pull_request_version_id: cnt
383 for cnt, ver in enumerate(versions, 1)
383 for cnt, ver in enumerate(versions, 1)
384 }
384 }
385
385
386 # GENERAL COMMENTS with versions #
386 # GENERAL COMMENTS with versions #
387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
388 q = q.order_by(ChangesetComment.comment_id.asc())
388 q = q.order_by(ChangesetComment.comment_id.asc())
389 general_comments = q.all()
389 general_comments = q.all()
390
390
391 # INLINE COMMENTS with versions #
391 # INLINE COMMENTS with versions #
392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
393 q = q.order_by(ChangesetComment.comment_id.asc())
393 q = q.order_by(ChangesetComment.comment_id.asc())
394 inline_comments = q.all()
394 inline_comments = q.all()
395
395
396 data = []
396 data = []
397 for comment in inline_comments + general_comments:
397 for comment in inline_comments + general_comments:
398 full_data = comment.get_api_data()
398 full_data = comment.get_api_data()
399 pr_version_id = None
399 pr_version_id = None
400 if comment.pull_request_version_id:
400 if comment.pull_request_version_id:
401 pr_version_id = 'v{}'.format(
401 pr_version_id = 'v{}'.format(
402 ver_map[comment.pull_request_version_id])
402 ver_map[comment.pull_request_version_id])
403
403
404 # sanitize some entries
404 # sanitize some entries
405
405
406 full_data['pull_request_version'] = pr_version_id
406 full_data['pull_request_version'] = pr_version_id
407 full_data['comment_author'] = {
407 full_data['comment_author'] = {
408 'username': full_data['comment_author'].username,
408 'username': full_data['comment_author'].username,
409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
410 'active': full_data['comment_author'].active,
410 'active': full_data['comment_author'].active,
411 }
411 }
412
412
413 if full_data['comment_status']:
413 if full_data['comment_status']:
414 full_data['comment_status'] = {
414 full_data['comment_status'] = {
415 'status': full_data['comment_status'][0].status,
415 'status': full_data['comment_status'][0].status,
416 'status_lbl': full_data['comment_status'][0].status_lbl,
416 'status_lbl': full_data['comment_status'][0].status_lbl,
417 }
417 }
418 else:
418 else:
419 full_data['comment_status'] = {}
419 full_data['comment_status'] = {}
420
420
421 data.append(full_data)
421 data.append(full_data)
422 return data
422 return data
423
423
424
424
425 @jsonrpc_method()
425 @jsonrpc_method()
426 def comment_pull_request(
426 def comment_pull_request(
427 request, apiuser, pullrequestid, repoid=Optional(None),
427 request, apiuser, pullrequestid, repoid=Optional(None),
428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
430 resolves_comment_id=Optional(None),
430 resolves_comment_id=Optional(None),
431 userid=Optional(OAttr('apiuser'))):
431 userid=Optional(OAttr('apiuser'))):
432 """
432 """
433 Comment on the pull request specified with the `pullrequestid`,
433 Comment on the pull request specified with the `pullrequestid`,
434 in the |repo| specified by the `repoid`, and optionally change the
434 in the |repo| specified by the `repoid`, and optionally change the
435 review status.
435 review status.
436
436
437 :param apiuser: This is filled automatically from the |authtoken|.
437 :param apiuser: This is filled automatically from the |authtoken|.
438 :type apiuser: AuthUser
438 :type apiuser: AuthUser
439 :param repoid: Optional repository name or repository ID.
439 :param repoid: Optional repository name or repository ID.
440 :type repoid: str or int
440 :type repoid: str or int
441 :param pullrequestid: The pull request ID.
441 :param pullrequestid: The pull request ID.
442 :type pullrequestid: int
442 :type pullrequestid: int
443 :param commit_id: Specify the commit_id for which to set a comment. If
443 :param commit_id: Specify the commit_id for which to set a comment. If
444 given commit_id is different than latest in the PR status
444 given commit_id is different than latest in the PR status
445 change won't be performed.
445 change won't be performed.
446 :type commit_id: str
446 :type commit_id: str
447 :param message: The text content of the comment.
447 :param message: The text content of the comment.
448 :type message: str
448 :type message: str
449 :param status: (**Optional**) Set the approval status of the pull
449 :param status: (**Optional**) Set the approval status of the pull
450 request. One of: 'not_reviewed', 'approved', 'rejected',
450 request. One of: 'not_reviewed', 'approved', 'rejected',
451 'under_review'
451 'under_review'
452 :type status: str
452 :type status: str
453 :param comment_type: Comment type, one of: 'note', 'todo'
453 :param comment_type: Comment type, one of: 'note', 'todo'
454 :type comment_type: Optional(str), default: 'note'
454 :type comment_type: Optional(str), default: 'note'
455 :param userid: Comment on the pull request as this user
455 :param userid: Comment on the pull request as this user
456 :type userid: Optional(str or int)
456 :type userid: Optional(str or int)
457
457
458 Example output:
458 Example output:
459
459
460 .. code-block:: bash
460 .. code-block:: bash
461
461
462 id : <id_given_in_input>
462 id : <id_given_in_input>
463 result : {
463 result : {
464 "pull_request_id": "<Integer>",
464 "pull_request_id": "<Integer>",
465 "comment_id": "<Integer>",
465 "comment_id": "<Integer>",
466 "status": {"given": <given_status>,
466 "status": {"given": <given_status>,
467 "was_changed": <bool status_was_actually_changed> },
467 "was_changed": <bool status_was_actually_changed> },
468 },
468 },
469 error : null
469 error : null
470 """
470 """
471 pull_request = get_pull_request_or_error(pullrequestid)
471 pull_request = get_pull_request_or_error(pullrequestid)
472 if Optional.extract(repoid):
472 if Optional.extract(repoid):
473 repo = get_repo_or_error(repoid)
473 repo = get_repo_or_error(repoid)
474 else:
474 else:
475 repo = pull_request.target_repo
475 repo = pull_request.target_repo
476
476
477 if not isinstance(userid, Optional):
477 if not isinstance(userid, Optional):
478 if (has_superadmin_permission(apiuser) or
478 if (has_superadmin_permission(apiuser) or
479 HasRepoPermissionAnyApi('repository.admin')(
479 HasRepoPermissionAnyApi('repository.admin')(
480 user=apiuser, repo_name=repo.repo_name)):
480 user=apiuser, repo_name=repo.repo_name)):
481 apiuser = get_user_or_error(userid)
481 apiuser = get_user_or_error(userid)
482 else:
482 else:
483 raise JSONRPCError('userid is not the same as your user')
483 raise JSONRPCError('userid is not the same as your user')
484
484
485 if not PullRequestModel().check_user_read(
485 if not PullRequestModel().check_user_read(
486 pull_request, apiuser, api=True):
486 pull_request, apiuser, api=True):
487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
488 message = Optional.extract(message)
488 message = Optional.extract(message)
489 status = Optional.extract(status)
489 status = Optional.extract(status)
490 commit_id = Optional.extract(commit_id)
490 commit_id = Optional.extract(commit_id)
491 comment_type = Optional.extract(comment_type)
491 comment_type = Optional.extract(comment_type)
492 resolves_comment_id = Optional.extract(resolves_comment_id)
492 resolves_comment_id = Optional.extract(resolves_comment_id)
493
493
494 if not message and not status:
494 if not message and not status:
495 raise JSONRPCError(
495 raise JSONRPCError(
496 'Both message and status parameters are missing. '
496 'Both message and status parameters are missing. '
497 'At least one is required.')
497 'At least one is required.')
498
498
499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
500 status is not None):
500 status is not None):
501 raise JSONRPCError('Unknown comment status: `%s`' % status)
501 raise JSONRPCError('Unknown comment status: `%s`' % status)
502
502
503 if commit_id and commit_id not in pull_request.revisions:
503 if commit_id and commit_id not in pull_request.revisions:
504 raise JSONRPCError(
504 raise JSONRPCError(
505 'Invalid commit_id `%s` for this pull request.' % commit_id)
505 'Invalid commit_id `%s` for this pull request.' % commit_id)
506
506
507 allowed_to_change_status = PullRequestModel().check_user_change_status(
507 allowed_to_change_status = PullRequestModel().check_user_change_status(
508 pull_request, apiuser)
508 pull_request, apiuser)
509
509
510 # if commit_id is passed re-validated if user is allowed to change status
510 # if commit_id is passed re-validated if user is allowed to change status
511 # based on latest commit_id from the PR
511 # based on latest commit_id from the PR
512 if commit_id:
512 if commit_id:
513 commit_idx = pull_request.revisions.index(commit_id)
513 commit_idx = pull_request.revisions.index(commit_id)
514 if commit_idx != 0:
514 if commit_idx != 0:
515 allowed_to_change_status = False
515 allowed_to_change_status = False
516
516
517 if resolves_comment_id:
517 if resolves_comment_id:
518 comment = ChangesetComment.get(resolves_comment_id)
518 comment = ChangesetComment.get(resolves_comment_id)
519 if not comment:
519 if not comment:
520 raise JSONRPCError(
520 raise JSONRPCError(
521 'Invalid resolves_comment_id `%s` for this pull request.'
521 'Invalid resolves_comment_id `%s` for this pull request.'
522 % resolves_comment_id)
522 % resolves_comment_id)
523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
524 raise JSONRPCError(
524 raise JSONRPCError(
525 'Comment `%s` is wrong type for setting status to resolved.'
525 'Comment `%s` is wrong type for setting status to resolved.'
526 % resolves_comment_id)
526 % resolves_comment_id)
527
527
528 text = message
528 text = message
529 status_label = ChangesetStatus.get_status_lbl(status)
529 status_label = ChangesetStatus.get_status_lbl(status)
530 if status and allowed_to_change_status:
530 if status and allowed_to_change_status:
531 st_message = ('Status change %(transition_icon)s %(status)s'
531 st_message = ('Status change %(transition_icon)s %(status)s'
532 % {'transition_icon': '>', 'status': status_label})
532 % {'transition_icon': '>', 'status': status_label})
533 text = message or st_message
533 text = message or st_message
534
534
535 rc_config = SettingsModel().get_all_settings()
535 rc_config = SettingsModel().get_all_settings()
536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
537
537
538 status_change = status and allowed_to_change_status
538 status_change = status and allowed_to_change_status
539 comment = CommentsModel().create(
539 comment = CommentsModel().create(
540 text=text,
540 text=text,
541 repo=pull_request.target_repo.repo_id,
541 repo=pull_request.target_repo.repo_id,
542 user=apiuser.user_id,
542 user=apiuser.user_id,
543 pull_request=pull_request.pull_request_id,
543 pull_request=pull_request.pull_request_id,
544 f_path=None,
544 f_path=None,
545 line_no=None,
545 line_no=None,
546 status_change=(status_label if status_change else None),
546 status_change=(status_label if status_change else None),
547 status_change_type=(status if status_change else None),
547 status_change_type=(status if status_change else None),
548 closing_pr=False,
548 closing_pr=False,
549 renderer=renderer,
549 renderer=renderer,
550 comment_type=comment_type,
550 comment_type=comment_type,
551 resolves_comment_id=resolves_comment_id
551 resolves_comment_id=resolves_comment_id,
552 auth_user=apiuser
552 )
553 )
553
554
554 if allowed_to_change_status and status:
555 if allowed_to_change_status and status:
555 ChangesetStatusModel().set_status(
556 ChangesetStatusModel().set_status(
556 pull_request.target_repo.repo_id,
557 pull_request.target_repo.repo_id,
557 status,
558 status,
558 apiuser.user_id,
559 apiuser.user_id,
559 comment,
560 comment,
560 pull_request=pull_request.pull_request_id
561 pull_request=pull_request.pull_request_id
561 )
562 )
562 Session().flush()
563 Session().flush()
563
564
564 Session().commit()
565 Session().commit()
565 data = {
566 data = {
566 'pull_request_id': pull_request.pull_request_id,
567 'pull_request_id': pull_request.pull_request_id,
567 'comment_id': comment.comment_id if comment else None,
568 'comment_id': comment.comment_id if comment else None,
568 'status': {'given': status, 'was_changed': status_change},
569 'status': {'given': status, 'was_changed': status_change},
569 }
570 }
570 return data
571 return data
571
572
572
573
573 @jsonrpc_method()
574 @jsonrpc_method()
574 def create_pull_request(
575 def create_pull_request(
575 request, apiuser, source_repo, target_repo, source_ref, target_ref,
576 request, apiuser, source_repo, target_repo, source_ref, target_ref,
576 title, description=Optional(''), reviewers=Optional(None)):
577 title, description=Optional(''), reviewers=Optional(None)):
577 """
578 """
578 Creates a new pull request.
579 Creates a new pull request.
579
580
580 Accepts refs in the following formats:
581 Accepts refs in the following formats:
581
582
582 * branch:<branch_name>:<sha>
583 * branch:<branch_name>:<sha>
583 * branch:<branch_name>
584 * branch:<branch_name>
584 * bookmark:<bookmark_name>:<sha> (Mercurial only)
585 * bookmark:<bookmark_name>:<sha> (Mercurial only)
585 * bookmark:<bookmark_name> (Mercurial only)
586 * bookmark:<bookmark_name> (Mercurial only)
586
587
587 :param apiuser: This is filled automatically from the |authtoken|.
588 :param apiuser: This is filled automatically from the |authtoken|.
588 :type apiuser: AuthUser
589 :type apiuser: AuthUser
589 :param source_repo: Set the source repository name.
590 :param source_repo: Set the source repository name.
590 :type source_repo: str
591 :type source_repo: str
591 :param target_repo: Set the target repository name.
592 :param target_repo: Set the target repository name.
592 :type target_repo: str
593 :type target_repo: str
593 :param source_ref: Set the source ref name.
594 :param source_ref: Set the source ref name.
594 :type source_ref: str
595 :type source_ref: str
595 :param target_ref: Set the target ref name.
596 :param target_ref: Set the target ref name.
596 :type target_ref: str
597 :type target_ref: str
597 :param title: Set the pull request title.
598 :param title: Set the pull request title.
598 :type title: str
599 :type title: str
599 :param description: Set the pull request description.
600 :param description: Set the pull request description.
600 :type description: Optional(str)
601 :type description: Optional(str)
601 :param reviewers: Set the new pull request reviewers list.
602 :param reviewers: Set the new pull request reviewers list.
602 Reviewer defined by review rules will be added automatically to the
603 Reviewer defined by review rules will be added automatically to the
603 defined list.
604 defined list.
604 :type reviewers: Optional(list)
605 :type reviewers: Optional(list)
605 Accepts username strings or objects of the format:
606 Accepts username strings or objects of the format:
606
607
607 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
608 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
608 """
609 """
609
610
610 source_db_repo = get_repo_or_error(source_repo)
611 source_db_repo = get_repo_or_error(source_repo)
611 target_db_repo = get_repo_or_error(target_repo)
612 target_db_repo = get_repo_or_error(target_repo)
612 if not has_superadmin_permission(apiuser):
613 if not has_superadmin_permission(apiuser):
613 _perms = ('repository.admin', 'repository.write', 'repository.read',)
614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
614 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
615 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
615
616
616 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
617 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
617 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
618 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
618 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
619 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
619 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
620 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
620 source_scm = source_db_repo.scm_instance()
621 source_scm = source_db_repo.scm_instance()
621 target_scm = target_db_repo.scm_instance()
622 target_scm = target_db_repo.scm_instance()
622
623
623 commit_ranges = target_scm.compare(
624 commit_ranges = target_scm.compare(
624 target_commit.raw_id, source_commit.raw_id, source_scm,
625 target_commit.raw_id, source_commit.raw_id, source_scm,
625 merge=True, pre_load=[])
626 merge=True, pre_load=[])
626
627
627 ancestor = target_scm.get_common_ancestor(
628 ancestor = target_scm.get_common_ancestor(
628 target_commit.raw_id, source_commit.raw_id, source_scm)
629 target_commit.raw_id, source_commit.raw_id, source_scm)
629
630
630 if not commit_ranges:
631 if not commit_ranges:
631 raise JSONRPCError('no commits found')
632 raise JSONRPCError('no commits found')
632
633
633 if not ancestor:
634 if not ancestor:
634 raise JSONRPCError('no common ancestor found')
635 raise JSONRPCError('no common ancestor found')
635
636
636 reviewer_objects = Optional.extract(reviewers) or []
637 reviewer_objects = Optional.extract(reviewers) or []
637
638
638 if reviewer_objects:
639 if reviewer_objects:
639 schema = ReviewerListSchema()
640 schema = ReviewerListSchema()
640 try:
641 try:
641 reviewer_objects = schema.deserialize(reviewer_objects)
642 reviewer_objects = schema.deserialize(reviewer_objects)
642 except Invalid as err:
643 except Invalid as err:
643 raise JSONRPCValidationError(colander_exc=err)
644 raise JSONRPCValidationError(colander_exc=err)
644
645
645 # validate users
646 # validate users
646 for reviewer_object in reviewer_objects:
647 for reviewer_object in reviewer_objects:
647 user = get_user_or_error(reviewer_object['username'])
648 user = get_user_or_error(reviewer_object['username'])
648 reviewer_object['user_id'] = user.user_id
649 reviewer_object['user_id'] = user.user_id
649
650
650 get_default_reviewers_data, get_validated_reviewers = \
651 get_default_reviewers_data, get_validated_reviewers = \
651 PullRequestModel().get_reviewer_functions()
652 PullRequestModel().get_reviewer_functions()
652
653
653 reviewer_rules = get_default_reviewers_data(
654 reviewer_rules = get_default_reviewers_data(
654 apiuser.get_instance(), source_db_repo,
655 apiuser.get_instance(), source_db_repo,
655 source_commit, target_db_repo, target_commit)
656 source_commit, target_db_repo, target_commit)
656
657
657 # specified rules are later re-validated, thus we can assume users will
658 # specified rules are later re-validated, thus we can assume users will
658 # eventually provide those that meet the reviewer criteria.
659 # eventually provide those that meet the reviewer criteria.
659 if not reviewer_objects:
660 if not reviewer_objects:
660 reviewer_objects = reviewer_rules['reviewers']
661 reviewer_objects = reviewer_rules['reviewers']
661
662
662 try:
663 try:
663 reviewers = get_validated_reviewers(
664 reviewers = get_validated_reviewers(
664 reviewer_objects, reviewer_rules)
665 reviewer_objects, reviewer_rules)
665 except ValueError as e:
666 except ValueError as e:
666 raise JSONRPCError('Reviewers Validation: {}'.format(e))
667 raise JSONRPCError('Reviewers Validation: {}'.format(e))
667
668
668 pull_request_model = PullRequestModel()
669 pull_request_model = PullRequestModel()
669 pull_request = pull_request_model.create(
670 pull_request = pull_request_model.create(
670 created_by=apiuser.user_id,
671 created_by=apiuser.user_id,
671 source_repo=source_repo,
672 source_repo=source_repo,
672 source_ref=full_source_ref,
673 source_ref=full_source_ref,
673 target_repo=target_repo,
674 target_repo=target_repo,
674 target_ref=full_target_ref,
675 target_ref=full_target_ref,
675 revisions=reversed(
676 revisions=reversed(
676 [commit.raw_id for commit in reversed(commit_ranges)]),
677 [commit.raw_id for commit in reversed(commit_ranges)]),
677 reviewers=reviewers,
678 reviewers=reviewers,
678 title=title,
679 title=title,
679 description=Optional.extract(description)
680 description=Optional.extract(description)
680 )
681 )
681
682
682 Session().commit()
683 Session().commit()
683 data = {
684 data = {
684 'msg': 'Created new pull request `{}`'.format(title),
685 'msg': 'Created new pull request `{}`'.format(title),
685 'pull_request_id': pull_request.pull_request_id,
686 'pull_request_id': pull_request.pull_request_id,
686 }
687 }
687 return data
688 return data
688
689
689
690
690 @jsonrpc_method()
691 @jsonrpc_method()
691 def update_pull_request(
692 def update_pull_request(
692 request, apiuser, pullrequestid, repoid=Optional(None),
693 request, apiuser, pullrequestid, repoid=Optional(None),
693 title=Optional(''), description=Optional(''), reviewers=Optional(None),
694 title=Optional(''), description=Optional(''), reviewers=Optional(None),
694 update_commits=Optional(None)):
695 update_commits=Optional(None)):
695 """
696 """
696 Updates a pull request.
697 Updates a pull request.
697
698
698 :param apiuser: This is filled automatically from the |authtoken|.
699 :param apiuser: This is filled automatically from the |authtoken|.
699 :type apiuser: AuthUser
700 :type apiuser: AuthUser
700 :param repoid: Optional repository name or repository ID.
701 :param repoid: Optional repository name or repository ID.
701 :type repoid: str or int
702 :type repoid: str or int
702 :param pullrequestid: The pull request ID.
703 :param pullrequestid: The pull request ID.
703 :type pullrequestid: int
704 :type pullrequestid: int
704 :param title: Set the pull request title.
705 :param title: Set the pull request title.
705 :type title: str
706 :type title: str
706 :param description: Update pull request description.
707 :param description: Update pull request description.
707 :type description: Optional(str)
708 :type description: Optional(str)
708 :param reviewers: Update pull request reviewers list with new value.
709 :param reviewers: Update pull request reviewers list with new value.
709 :type reviewers: Optional(list)
710 :type reviewers: Optional(list)
710 Accepts username strings or objects of the format:
711 Accepts username strings or objects of the format:
711
712
712 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
713 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
713
714
714 :param update_commits: Trigger update of commits for this pull request
715 :param update_commits: Trigger update of commits for this pull request
715 :type: update_commits: Optional(bool)
716 :type: update_commits: Optional(bool)
716
717
717 Example output:
718 Example output:
718
719
719 .. code-block:: bash
720 .. code-block:: bash
720
721
721 id : <id_given_in_input>
722 id : <id_given_in_input>
722 result : {
723 result : {
723 "msg": "Updated pull request `63`",
724 "msg": "Updated pull request `63`",
724 "pull_request": <pull_request_object>,
725 "pull_request": <pull_request_object>,
725 "updated_reviewers": {
726 "updated_reviewers": {
726 "added": [
727 "added": [
727 "username"
728 "username"
728 ],
729 ],
729 "removed": []
730 "removed": []
730 },
731 },
731 "updated_commits": {
732 "updated_commits": {
732 "added": [
733 "added": [
733 "<sha1_hash>"
734 "<sha1_hash>"
734 ],
735 ],
735 "common": [
736 "common": [
736 "<sha1_hash>",
737 "<sha1_hash>",
737 "<sha1_hash>",
738 "<sha1_hash>",
738 ],
739 ],
739 "removed": []
740 "removed": []
740 }
741 }
741 }
742 }
742 error : null
743 error : null
743 """
744 """
744
745
745 pull_request = get_pull_request_or_error(pullrequestid)
746 pull_request = get_pull_request_or_error(pullrequestid)
746 if Optional.extract(repoid):
747 if Optional.extract(repoid):
747 repo = get_repo_or_error(repoid)
748 repo = get_repo_or_error(repoid)
748 else:
749 else:
749 repo = pull_request.target_repo
750 repo = pull_request.target_repo
750
751
751 if not PullRequestModel().check_user_update(
752 if not PullRequestModel().check_user_update(
752 pull_request, apiuser, api=True):
753 pull_request, apiuser, api=True):
753 raise JSONRPCError(
754 raise JSONRPCError(
754 'pull request `%s` update failed, no permission to update.' % (
755 'pull request `%s` update failed, no permission to update.' % (
755 pullrequestid,))
756 pullrequestid,))
756 if pull_request.is_closed():
757 if pull_request.is_closed():
757 raise JSONRPCError(
758 raise JSONRPCError(
758 'pull request `%s` update failed, pull request is closed' % (
759 'pull request `%s` update failed, pull request is closed' % (
759 pullrequestid,))
760 pullrequestid,))
760
761
761 reviewer_objects = Optional.extract(reviewers) or []
762 reviewer_objects = Optional.extract(reviewers) or []
762
763
763 if reviewer_objects:
764 if reviewer_objects:
764 schema = ReviewerListSchema()
765 schema = ReviewerListSchema()
765 try:
766 try:
766 reviewer_objects = schema.deserialize(reviewer_objects)
767 reviewer_objects = schema.deserialize(reviewer_objects)
767 except Invalid as err:
768 except Invalid as err:
768 raise JSONRPCValidationError(colander_exc=err)
769 raise JSONRPCValidationError(colander_exc=err)
769
770
770 # validate users
771 # validate users
771 for reviewer_object in reviewer_objects:
772 for reviewer_object in reviewer_objects:
772 user = get_user_or_error(reviewer_object['username'])
773 user = get_user_or_error(reviewer_object['username'])
773 reviewer_object['user_id'] = user.user_id
774 reviewer_object['user_id'] = user.user_id
774
775
775 get_default_reviewers_data, get_validated_reviewers = \
776 get_default_reviewers_data, get_validated_reviewers = \
776 PullRequestModel().get_reviewer_functions()
777 PullRequestModel().get_reviewer_functions()
777
778
778 # re-use stored rules
779 # re-use stored rules
779 reviewer_rules = pull_request.reviewer_data
780 reviewer_rules = pull_request.reviewer_data
780 try:
781 try:
781 reviewers = get_validated_reviewers(
782 reviewers = get_validated_reviewers(
782 reviewer_objects, reviewer_rules)
783 reviewer_objects, reviewer_rules)
783 except ValueError as e:
784 except ValueError as e:
784 raise JSONRPCError('Reviewers Validation: {}'.format(e))
785 raise JSONRPCError('Reviewers Validation: {}'.format(e))
785 else:
786 else:
786 reviewers = []
787 reviewers = []
787
788
788 title = Optional.extract(title)
789 title = Optional.extract(title)
789 description = Optional.extract(description)
790 description = Optional.extract(description)
790 if title or description:
791 if title or description:
791 PullRequestModel().edit(
792 PullRequestModel().edit(
792 pull_request, title or pull_request.title,
793 pull_request, title or pull_request.title,
793 description or pull_request.description, apiuser)
794 description or pull_request.description, apiuser)
794 Session().commit()
795 Session().commit()
795
796
796 commit_changes = {"added": [], "common": [], "removed": []}
797 commit_changes = {"added": [], "common": [], "removed": []}
797 if str2bool(Optional.extract(update_commits)):
798 if str2bool(Optional.extract(update_commits)):
798 if PullRequestModel().has_valid_update_type(pull_request):
799 if PullRequestModel().has_valid_update_type(pull_request):
799 update_response = PullRequestModel().update_commits(
800 update_response = PullRequestModel().update_commits(
800 pull_request)
801 pull_request)
801 commit_changes = update_response.changes or commit_changes
802 commit_changes = update_response.changes or commit_changes
802 Session().commit()
803 Session().commit()
803
804
804 reviewers_changes = {"added": [], "removed": []}
805 reviewers_changes = {"added": [], "removed": []}
805 if reviewers:
806 if reviewers:
806 added_reviewers, removed_reviewers = \
807 added_reviewers, removed_reviewers = \
807 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
808 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
808
809
809 reviewers_changes['added'] = sorted(
810 reviewers_changes['added'] = sorted(
810 [get_user_or_error(n).username for n in added_reviewers])
811 [get_user_or_error(n).username for n in added_reviewers])
811 reviewers_changes['removed'] = sorted(
812 reviewers_changes['removed'] = sorted(
812 [get_user_or_error(n).username for n in removed_reviewers])
813 [get_user_or_error(n).username for n in removed_reviewers])
813 Session().commit()
814 Session().commit()
814
815
815 data = {
816 data = {
816 'msg': 'Updated pull request `{}`'.format(
817 'msg': 'Updated pull request `{}`'.format(
817 pull_request.pull_request_id),
818 pull_request.pull_request_id),
818 'pull_request': pull_request.get_api_data(),
819 'pull_request': pull_request.get_api_data(),
819 'updated_commits': commit_changes,
820 'updated_commits': commit_changes,
820 'updated_reviewers': reviewers_changes
821 'updated_reviewers': reviewers_changes
821 }
822 }
822
823
823 return data
824 return data
824
825
825
826
826 @jsonrpc_method()
827 @jsonrpc_method()
827 def close_pull_request(
828 def close_pull_request(
828 request, apiuser, pullrequestid, repoid=Optional(None),
829 request, apiuser, pullrequestid, repoid=Optional(None),
829 userid=Optional(OAttr('apiuser')), message=Optional('')):
830 userid=Optional(OAttr('apiuser')), message=Optional('')):
830 """
831 """
831 Close the pull request specified by `pullrequestid`.
832 Close the pull request specified by `pullrequestid`.
832
833
833 :param apiuser: This is filled automatically from the |authtoken|.
834 :param apiuser: This is filled automatically from the |authtoken|.
834 :type apiuser: AuthUser
835 :type apiuser: AuthUser
835 :param repoid: Repository name or repository ID to which the pull
836 :param repoid: Repository name or repository ID to which the pull
836 request belongs.
837 request belongs.
837 :type repoid: str or int
838 :type repoid: str or int
838 :param pullrequestid: ID of the pull request to be closed.
839 :param pullrequestid: ID of the pull request to be closed.
839 :type pullrequestid: int
840 :type pullrequestid: int
840 :param userid: Close the pull request as this user.
841 :param userid: Close the pull request as this user.
841 :type userid: Optional(str or int)
842 :type userid: Optional(str or int)
842 :param message: Optional message to close the Pull Request with. If not
843 :param message: Optional message to close the Pull Request with. If not
843 specified it will be generated automatically.
844 specified it will be generated automatically.
844 :type message: Optional(str)
845 :type message: Optional(str)
845
846
846 Example output:
847 Example output:
847
848
848 .. code-block:: bash
849 .. code-block:: bash
849
850
850 "id": <id_given_in_input>,
851 "id": <id_given_in_input>,
851 "result": {
852 "result": {
852 "pull_request_id": "<int>",
853 "pull_request_id": "<int>",
853 "close_status": "<str:status_lbl>,
854 "close_status": "<str:status_lbl>,
854 "closed": "<bool>"
855 "closed": "<bool>"
855 },
856 },
856 "error": null
857 "error": null
857
858
858 """
859 """
859 _ = request.translate
860 _ = request.translate
860
861
861 pull_request = get_pull_request_or_error(pullrequestid)
862 pull_request = get_pull_request_or_error(pullrequestid)
862 if Optional.extract(repoid):
863 if Optional.extract(repoid):
863 repo = get_repo_or_error(repoid)
864 repo = get_repo_or_error(repoid)
864 else:
865 else:
865 repo = pull_request.target_repo
866 repo = pull_request.target_repo
866
867
867 if not isinstance(userid, Optional):
868 if not isinstance(userid, Optional):
868 if (has_superadmin_permission(apiuser) or
869 if (has_superadmin_permission(apiuser) or
869 HasRepoPermissionAnyApi('repository.admin')(
870 HasRepoPermissionAnyApi('repository.admin')(
870 user=apiuser, repo_name=repo.repo_name)):
871 user=apiuser, repo_name=repo.repo_name)):
871 apiuser = get_user_or_error(userid)
872 apiuser = get_user_or_error(userid)
872 else:
873 else:
873 raise JSONRPCError('userid is not the same as your user')
874 raise JSONRPCError('userid is not the same as your user')
874
875
875 if pull_request.is_closed():
876 if pull_request.is_closed():
876 raise JSONRPCError(
877 raise JSONRPCError(
877 'pull request `%s` is already closed' % (pullrequestid,))
878 'pull request `%s` is already closed' % (pullrequestid,))
878
879
879 # only owner or admin or person with write permissions
880 # only owner or admin or person with write permissions
880 allowed_to_close = PullRequestModel().check_user_update(
881 allowed_to_close = PullRequestModel().check_user_update(
881 pull_request, apiuser, api=True)
882 pull_request, apiuser, api=True)
882
883
883 if not allowed_to_close:
884 if not allowed_to_close:
884 raise JSONRPCError(
885 raise JSONRPCError(
885 'pull request `%s` close failed, no permission to close.' % (
886 'pull request `%s` close failed, no permission to close.' % (
886 pullrequestid,))
887 pullrequestid,))
887
888
888 # message we're using to close the PR, else it's automatically generated
889 # message we're using to close the PR, else it's automatically generated
889 message = Optional.extract(message)
890 message = Optional.extract(message)
890
891
891 # finally close the PR, with proper message comment
892 # finally close the PR, with proper message comment
892 comment, status = PullRequestModel().close_pull_request_with_comment(
893 comment, status = PullRequestModel().close_pull_request_with_comment(
893 pull_request, apiuser, repo, message=message)
894 pull_request, apiuser, repo, message=message)
894 status_lbl = ChangesetStatus.get_status_lbl(status)
895 status_lbl = ChangesetStatus.get_status_lbl(status)
895
896
896 Session().commit()
897 Session().commit()
897
898
898 data = {
899 data = {
899 'pull_request_id': pull_request.pull_request_id,
900 'pull_request_id': pull_request.pull_request_id,
900 'close_status': status_lbl,
901 'close_status': status_lbl,
901 'closed': True,
902 'closed': True,
902 }
903 }
903 return data
904 return data
@@ -1,2064 +1,2065 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import 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
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 from rhodecode.lib.celerylib.utils import get_task_id
35 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 from rhodecode.model.changeset_status import ChangesetStatusModel
39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 from rhodecode.model.comment import CommentsModel
40 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
42 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
43 ChangesetComment)
43 ChangesetComment)
44 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.repo import RepoModel
45 from rhodecode.model.scm import ScmModel, RepoList
45 from rhodecode.model.scm import ScmModel, RepoList
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 from rhodecode.model import validation_schema
47 from rhodecode.model import validation_schema
48 from rhodecode.model.validation_schema.schemas import repo_schema
48 from rhodecode.model.validation_schema.schemas import repo_schema
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 @jsonrpc_method()
53 @jsonrpc_method()
54 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 def get_repo(request, apiuser, repoid, cache=Optional(True)):
55 """
55 """
56 Gets an existing repository by its name or repository_id.
56 Gets an existing repository by its name or repository_id.
57
57
58 The members section so the output returns users groups or users
58 The members section so the output returns users groups or users
59 associated with that repository.
59 associated with that repository.
60
60
61 This command can only be run using an |authtoken| with admin rights,
61 This command can only be run using an |authtoken| with admin rights,
62 or users with at least read rights to the |repo|.
62 or users with at least read rights to the |repo|.
63
63
64 :param apiuser: This is filled automatically from the |authtoken|.
64 :param apiuser: This is filled automatically from the |authtoken|.
65 :type apiuser: AuthUser
65 :type apiuser: AuthUser
66 :param repoid: The repository name or repository id.
66 :param repoid: The repository name or repository id.
67 :type repoid: str or int
67 :type repoid: str or int
68 :param cache: use the cached value for last changeset
68 :param cache: use the cached value for last changeset
69 :type: cache: Optional(bool)
69 :type: cache: Optional(bool)
70
70
71 Example output:
71 Example output:
72
72
73 .. code-block:: bash
73 .. code-block:: bash
74
74
75 {
75 {
76 "error": null,
76 "error": null,
77 "id": <repo_id>,
77 "id": <repo_id>,
78 "result": {
78 "result": {
79 "clone_uri": null,
79 "clone_uri": null,
80 "created_on": "timestamp",
80 "created_on": "timestamp",
81 "description": "repo description",
81 "description": "repo description",
82 "enable_downloads": false,
82 "enable_downloads": false,
83 "enable_locking": false,
83 "enable_locking": false,
84 "enable_statistics": false,
84 "enable_statistics": false,
85 "followers": [
85 "followers": [
86 {
86 {
87 "active": true,
87 "active": true,
88 "admin": false,
88 "admin": false,
89 "api_key": "****************************************",
89 "api_key": "****************************************",
90 "api_keys": [
90 "api_keys": [
91 "****************************************"
91 "****************************************"
92 ],
92 ],
93 "email": "user@example.com",
93 "email": "user@example.com",
94 "emails": [
94 "emails": [
95 "user@example.com"
95 "user@example.com"
96 ],
96 ],
97 "extern_name": "rhodecode",
97 "extern_name": "rhodecode",
98 "extern_type": "rhodecode",
98 "extern_type": "rhodecode",
99 "firstname": "username",
99 "firstname": "username",
100 "ip_addresses": [],
100 "ip_addresses": [],
101 "language": null,
101 "language": null,
102 "last_login": "2015-09-16T17:16:35.854",
102 "last_login": "2015-09-16T17:16:35.854",
103 "lastname": "surname",
103 "lastname": "surname",
104 "user_id": <user_id>,
104 "user_id": <user_id>,
105 "username": "name"
105 "username": "name"
106 }
106 }
107 ],
107 ],
108 "fork_of": "parent-repo",
108 "fork_of": "parent-repo",
109 "landing_rev": [
109 "landing_rev": [
110 "rev",
110 "rev",
111 "tip"
111 "tip"
112 ],
112 ],
113 "last_changeset": {
113 "last_changeset": {
114 "author": "User <user@example.com>",
114 "author": "User <user@example.com>",
115 "branch": "default",
115 "branch": "default",
116 "date": "timestamp",
116 "date": "timestamp",
117 "message": "last commit message",
117 "message": "last commit message",
118 "parents": [
118 "parents": [
119 {
119 {
120 "raw_id": "commit-id"
120 "raw_id": "commit-id"
121 }
121 }
122 ],
122 ],
123 "raw_id": "commit-id",
123 "raw_id": "commit-id",
124 "revision": <revision number>,
124 "revision": <revision number>,
125 "short_id": "short id"
125 "short_id": "short id"
126 },
126 },
127 "lock_reason": null,
127 "lock_reason": null,
128 "locked_by": null,
128 "locked_by": null,
129 "locked_date": null,
129 "locked_date": null,
130 "owner": "owner-name",
130 "owner": "owner-name",
131 "permissions": [
131 "permissions": [
132 {
132 {
133 "name": "super-admin-name",
133 "name": "super-admin-name",
134 "origin": "super-admin",
134 "origin": "super-admin",
135 "permission": "repository.admin",
135 "permission": "repository.admin",
136 "type": "user"
136 "type": "user"
137 },
137 },
138 {
138 {
139 "name": "owner-name",
139 "name": "owner-name",
140 "origin": "owner",
140 "origin": "owner",
141 "permission": "repository.admin",
141 "permission": "repository.admin",
142 "type": "user"
142 "type": "user"
143 },
143 },
144 {
144 {
145 "name": "user-group-name",
145 "name": "user-group-name",
146 "origin": "permission",
146 "origin": "permission",
147 "permission": "repository.write",
147 "permission": "repository.write",
148 "type": "user_group"
148 "type": "user_group"
149 }
149 }
150 ],
150 ],
151 "private": true,
151 "private": true,
152 "repo_id": 676,
152 "repo_id": 676,
153 "repo_name": "user-group/repo-name",
153 "repo_name": "user-group/repo-name",
154 "repo_type": "hg"
154 "repo_type": "hg"
155 }
155 }
156 }
156 }
157 """
157 """
158
158
159 repo = get_repo_or_error(repoid)
159 repo = get_repo_or_error(repoid)
160 cache = Optional.extract(cache)
160 cache = Optional.extract(cache)
161
161
162 include_secrets = False
162 include_secrets = False
163 if has_superadmin_permission(apiuser):
163 if has_superadmin_permission(apiuser):
164 include_secrets = True
164 include_secrets = True
165 else:
165 else:
166 # check if we have at least read permission for this repo !
166 # check if we have at least read permission for this repo !
167 _perms = (
167 _perms = (
168 'repository.admin', 'repository.write', 'repository.read',)
168 'repository.admin', 'repository.write', 'repository.read',)
169 validate_repo_permissions(apiuser, repoid, repo, _perms)
169 validate_repo_permissions(apiuser, repoid, repo, _perms)
170
170
171 permissions = []
171 permissions = []
172 for _user in repo.permissions():
172 for _user in repo.permissions():
173 user_data = {
173 user_data = {
174 'name': _user.username,
174 'name': _user.username,
175 'permission': _user.permission,
175 'permission': _user.permission,
176 'origin': get_origin(_user),
176 'origin': get_origin(_user),
177 'type': "user",
177 'type': "user",
178 }
178 }
179 permissions.append(user_data)
179 permissions.append(user_data)
180
180
181 for _user_group in repo.permission_user_groups():
181 for _user_group in repo.permission_user_groups():
182 user_group_data = {
182 user_group_data = {
183 'name': _user_group.users_group_name,
183 'name': _user_group.users_group_name,
184 'permission': _user_group.permission,
184 'permission': _user_group.permission,
185 'origin': get_origin(_user_group),
185 'origin': get_origin(_user_group),
186 'type': "user_group",
186 'type': "user_group",
187 }
187 }
188 permissions.append(user_group_data)
188 permissions.append(user_group_data)
189
189
190 following_users = [
190 following_users = [
191 user.user.get_api_data(include_secrets=include_secrets)
191 user.user.get_api_data(include_secrets=include_secrets)
192 for user in repo.followers]
192 for user in repo.followers]
193
193
194 if not cache:
194 if not cache:
195 repo.update_commit_cache()
195 repo.update_commit_cache()
196 data = repo.get_api_data(include_secrets=include_secrets)
196 data = repo.get_api_data(include_secrets=include_secrets)
197 data['permissions'] = permissions
197 data['permissions'] = permissions
198 data['followers'] = following_users
198 data['followers'] = following_users
199 return data
199 return data
200
200
201
201
202 @jsonrpc_method()
202 @jsonrpc_method()
203 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
203 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
204 """
204 """
205 Lists all existing repositories.
205 Lists all existing repositories.
206
206
207 This command can only be run using an |authtoken| with admin rights,
207 This command can only be run using an |authtoken| with admin rights,
208 or users with at least read rights to |repos|.
208 or users with at least read rights to |repos|.
209
209
210 :param apiuser: This is filled automatically from the |authtoken|.
210 :param apiuser: This is filled automatically from the |authtoken|.
211 :type apiuser: AuthUser
211 :type apiuser: AuthUser
212 :param root: specify root repository group to fetch repositories.
212 :param root: specify root repository group to fetch repositories.
213 filters the returned repositories to be members of given root group.
213 filters the returned repositories to be members of given root group.
214 :type root: Optional(None)
214 :type root: Optional(None)
215 :param traverse: traverse given root into subrepositories. With this flag
215 :param traverse: traverse given root into subrepositories. With this flag
216 set to False, it will only return top-level repositories from `root`.
216 set to False, it will only return top-level repositories from `root`.
217 if root is empty it will return just top-level repositories.
217 if root is empty it will return just top-level repositories.
218 :type traverse: Optional(True)
218 :type traverse: Optional(True)
219
219
220
220
221 Example output:
221 Example output:
222
222
223 .. code-block:: bash
223 .. code-block:: bash
224
224
225 id : <id_given_in_input>
225 id : <id_given_in_input>
226 result: [
226 result: [
227 {
227 {
228 "repo_id" : "<repo_id>",
228 "repo_id" : "<repo_id>",
229 "repo_name" : "<reponame>"
229 "repo_name" : "<reponame>"
230 "repo_type" : "<repo_type>",
230 "repo_type" : "<repo_type>",
231 "clone_uri" : "<clone_uri>",
231 "clone_uri" : "<clone_uri>",
232 "private": : "<bool>",
232 "private": : "<bool>",
233 "created_on" : "<datetimecreated>",
233 "created_on" : "<datetimecreated>",
234 "description" : "<description>",
234 "description" : "<description>",
235 "landing_rev": "<landing_rev>",
235 "landing_rev": "<landing_rev>",
236 "owner": "<repo_owner>",
236 "owner": "<repo_owner>",
237 "fork_of": "<name_of_fork_parent>",
237 "fork_of": "<name_of_fork_parent>",
238 "enable_downloads": "<bool>",
238 "enable_downloads": "<bool>",
239 "enable_locking": "<bool>",
239 "enable_locking": "<bool>",
240 "enable_statistics": "<bool>",
240 "enable_statistics": "<bool>",
241 },
241 },
242 ...
242 ...
243 ]
243 ]
244 error: null
244 error: null
245 """
245 """
246
246
247 include_secrets = has_superadmin_permission(apiuser)
247 include_secrets = has_superadmin_permission(apiuser)
248 _perms = ('repository.read', 'repository.write', 'repository.admin',)
248 _perms = ('repository.read', 'repository.write', 'repository.admin',)
249 extras = {'user': apiuser}
249 extras = {'user': apiuser}
250
250
251 root = Optional.extract(root)
251 root = Optional.extract(root)
252 traverse = Optional.extract(traverse, binary=True)
252 traverse = Optional.extract(traverse, binary=True)
253
253
254 if root:
254 if root:
255 # verify parent existance, if it's empty return an error
255 # verify parent existance, if it's empty return an error
256 parent = RepoGroup.get_by_group_name(root)
256 parent = RepoGroup.get_by_group_name(root)
257 if not parent:
257 if not parent:
258 raise JSONRPCError(
258 raise JSONRPCError(
259 'Root repository group `{}` does not exist'.format(root))
259 'Root repository group `{}` does not exist'.format(root))
260
260
261 if traverse:
261 if traverse:
262 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
262 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
263 else:
263 else:
264 repos = RepoModel().get_repos_for_root(root=parent)
264 repos = RepoModel().get_repos_for_root(root=parent)
265 else:
265 else:
266 if traverse:
266 if traverse:
267 repos = RepoModel().get_all()
267 repos = RepoModel().get_all()
268 else:
268 else:
269 # return just top-level
269 # return just top-level
270 repos = RepoModel().get_repos_for_root(root=None)
270 repos = RepoModel().get_repos_for_root(root=None)
271
271
272 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
272 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
273 return [repo.get_api_data(include_secrets=include_secrets)
273 return [repo.get_api_data(include_secrets=include_secrets)
274 for repo in repo_list]
274 for repo in repo_list]
275
275
276
276
277 @jsonrpc_method()
277 @jsonrpc_method()
278 def get_repo_changeset(request, apiuser, repoid, revision,
278 def get_repo_changeset(request, apiuser, repoid, revision,
279 details=Optional('basic')):
279 details=Optional('basic')):
280 """
280 """
281 Returns information about a changeset.
281 Returns information about a changeset.
282
282
283 Additionally parameters define the amount of details returned by
283 Additionally parameters define the amount of details returned by
284 this function.
284 this function.
285
285
286 This command can only be run using an |authtoken| with admin rights,
286 This command can only be run using an |authtoken| with admin rights,
287 or users with at least read rights to the |repo|.
287 or users with at least read rights to the |repo|.
288
288
289 :param apiuser: This is filled automatically from the |authtoken|.
289 :param apiuser: This is filled automatically from the |authtoken|.
290 :type apiuser: AuthUser
290 :type apiuser: AuthUser
291 :param repoid: The repository name or repository id
291 :param repoid: The repository name or repository id
292 :type repoid: str or int
292 :type repoid: str or int
293 :param revision: revision for which listing should be done
293 :param revision: revision for which listing should be done
294 :type revision: str
294 :type revision: str
295 :param details: details can be 'basic|extended|full' full gives diff
295 :param details: details can be 'basic|extended|full' full gives diff
296 info details like the diff itself, and number of changed files etc.
296 info details like the diff itself, and number of changed files etc.
297 :type details: Optional(str)
297 :type details: Optional(str)
298
298
299 """
299 """
300 repo = get_repo_or_error(repoid)
300 repo = get_repo_or_error(repoid)
301 if not has_superadmin_permission(apiuser):
301 if not has_superadmin_permission(apiuser):
302 _perms = (
302 _perms = (
303 'repository.admin', 'repository.write', 'repository.read',)
303 'repository.admin', 'repository.write', 'repository.read',)
304 validate_repo_permissions(apiuser, repoid, repo, _perms)
304 validate_repo_permissions(apiuser, repoid, repo, _perms)
305
305
306 changes_details = Optional.extract(details)
306 changes_details = Optional.extract(details)
307 _changes_details_types = ['basic', 'extended', 'full']
307 _changes_details_types = ['basic', 'extended', 'full']
308 if changes_details not in _changes_details_types:
308 if changes_details not in _changes_details_types:
309 raise JSONRPCError(
309 raise JSONRPCError(
310 'ret_type must be one of %s' % (
310 'ret_type must be one of %s' % (
311 ','.join(_changes_details_types)))
311 ','.join(_changes_details_types)))
312
312
313 pre_load = ['author', 'branch', 'date', 'message', 'parents',
313 pre_load = ['author', 'branch', 'date', 'message', 'parents',
314 'status', '_commit', '_file_paths']
314 'status', '_commit', '_file_paths']
315
315
316 try:
316 try:
317 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
317 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
318 except TypeError as e:
318 except TypeError as e:
319 raise JSONRPCError(e.message)
319 raise JSONRPCError(e.message)
320 _cs_json = cs.__json__()
320 _cs_json = cs.__json__()
321 _cs_json['diff'] = build_commit_data(cs, changes_details)
321 _cs_json['diff'] = build_commit_data(cs, changes_details)
322 if changes_details == 'full':
322 if changes_details == 'full':
323 _cs_json['refs'] = cs._get_refs()
323 _cs_json['refs'] = cs._get_refs()
324 return _cs_json
324 return _cs_json
325
325
326
326
327 @jsonrpc_method()
327 @jsonrpc_method()
328 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
328 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
329 details=Optional('basic')):
329 details=Optional('basic')):
330 """
330 """
331 Returns a set of commits limited by the number starting
331 Returns a set of commits limited by the number starting
332 from the `start_rev` option.
332 from the `start_rev` option.
333
333
334 Additional parameters define the amount of details returned by this
334 Additional parameters define the amount of details returned by this
335 function.
335 function.
336
336
337 This command can only be run using an |authtoken| with admin rights,
337 This command can only be run using an |authtoken| with admin rights,
338 or users with at least read rights to |repos|.
338 or users with at least read rights to |repos|.
339
339
340 :param apiuser: This is filled automatically from the |authtoken|.
340 :param apiuser: This is filled automatically from the |authtoken|.
341 :type apiuser: AuthUser
341 :type apiuser: AuthUser
342 :param repoid: The repository name or repository ID.
342 :param repoid: The repository name or repository ID.
343 :type repoid: str or int
343 :type repoid: str or int
344 :param start_rev: The starting revision from where to get changesets.
344 :param start_rev: The starting revision from where to get changesets.
345 :type start_rev: str
345 :type start_rev: str
346 :param limit: Limit the number of commits to this amount
346 :param limit: Limit the number of commits to this amount
347 :type limit: str or int
347 :type limit: str or int
348 :param details: Set the level of detail returned. Valid option are:
348 :param details: Set the level of detail returned. Valid option are:
349 ``basic``, ``extended`` and ``full``.
349 ``basic``, ``extended`` and ``full``.
350 :type details: Optional(str)
350 :type details: Optional(str)
351
351
352 .. note::
352 .. note::
353
353
354 Setting the parameter `details` to the value ``full`` is extensive
354 Setting the parameter `details` to the value ``full`` is extensive
355 and returns details like the diff itself, and the number
355 and returns details like the diff itself, and the number
356 of changed files.
356 of changed files.
357
357
358 """
358 """
359 repo = get_repo_or_error(repoid)
359 repo = get_repo_or_error(repoid)
360 if not has_superadmin_permission(apiuser):
360 if not has_superadmin_permission(apiuser):
361 _perms = (
361 _perms = (
362 'repository.admin', 'repository.write', 'repository.read',)
362 'repository.admin', 'repository.write', 'repository.read',)
363 validate_repo_permissions(apiuser, repoid, repo, _perms)
363 validate_repo_permissions(apiuser, repoid, repo, _perms)
364
364
365 changes_details = Optional.extract(details)
365 changes_details = Optional.extract(details)
366 _changes_details_types = ['basic', 'extended', 'full']
366 _changes_details_types = ['basic', 'extended', 'full']
367 if changes_details not in _changes_details_types:
367 if changes_details not in _changes_details_types:
368 raise JSONRPCError(
368 raise JSONRPCError(
369 'ret_type must be one of %s' % (
369 'ret_type must be one of %s' % (
370 ','.join(_changes_details_types)))
370 ','.join(_changes_details_types)))
371
371
372 limit = int(limit)
372 limit = int(limit)
373 pre_load = ['author', 'branch', 'date', 'message', 'parents',
373 pre_load = ['author', 'branch', 'date', 'message', 'parents',
374 'status', '_commit', '_file_paths']
374 'status', '_commit', '_file_paths']
375
375
376 vcs_repo = repo.scm_instance()
376 vcs_repo = repo.scm_instance()
377 # SVN needs a special case to distinguish its index and commit id
377 # SVN needs a special case to distinguish its index and commit id
378 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
378 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
379 start_rev = vcs_repo.commit_ids[0]
379 start_rev = vcs_repo.commit_ids[0]
380
380
381 try:
381 try:
382 commits = vcs_repo.get_commits(
382 commits = vcs_repo.get_commits(
383 start_id=start_rev, pre_load=pre_load)
383 start_id=start_rev, pre_load=pre_load)
384 except TypeError as e:
384 except TypeError as e:
385 raise JSONRPCError(e.message)
385 raise JSONRPCError(e.message)
386 except Exception:
386 except Exception:
387 log.exception('Fetching of commits failed')
387 log.exception('Fetching of commits failed')
388 raise JSONRPCError('Error occurred during commit fetching')
388 raise JSONRPCError('Error occurred during commit fetching')
389
389
390 ret = []
390 ret = []
391 for cnt, commit in enumerate(commits):
391 for cnt, commit in enumerate(commits):
392 if cnt >= limit != -1:
392 if cnt >= limit != -1:
393 break
393 break
394 _cs_json = commit.__json__()
394 _cs_json = commit.__json__()
395 _cs_json['diff'] = build_commit_data(commit, changes_details)
395 _cs_json['diff'] = build_commit_data(commit, changes_details)
396 if changes_details == 'full':
396 if changes_details == 'full':
397 _cs_json['refs'] = {
397 _cs_json['refs'] = {
398 'branches': [commit.branch],
398 'branches': [commit.branch],
399 'bookmarks': getattr(commit, 'bookmarks', []),
399 'bookmarks': getattr(commit, 'bookmarks', []),
400 'tags': commit.tags
400 'tags': commit.tags
401 }
401 }
402 ret.append(_cs_json)
402 ret.append(_cs_json)
403 return ret
403 return ret
404
404
405
405
406 @jsonrpc_method()
406 @jsonrpc_method()
407 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
407 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
408 ret_type=Optional('all'), details=Optional('basic'),
408 ret_type=Optional('all'), details=Optional('basic'),
409 max_file_bytes=Optional(None)):
409 max_file_bytes=Optional(None)):
410 """
410 """
411 Returns a list of nodes and children in a flat list for a given
411 Returns a list of nodes and children in a flat list for a given
412 path at given revision.
412 path at given revision.
413
413
414 It's possible to specify ret_type to show only `files` or `dirs`.
414 It's possible to specify ret_type to show only `files` or `dirs`.
415
415
416 This command can only be run using an |authtoken| with admin rights,
416 This command can only be run using an |authtoken| with admin rights,
417 or users with at least read rights to |repos|.
417 or users with at least read rights to |repos|.
418
418
419 :param apiuser: This is filled automatically from the |authtoken|.
419 :param apiuser: This is filled automatically from the |authtoken|.
420 :type apiuser: AuthUser
420 :type apiuser: AuthUser
421 :param repoid: The repository name or repository ID.
421 :param repoid: The repository name or repository ID.
422 :type repoid: str or int
422 :type repoid: str or int
423 :param revision: The revision for which listing should be done.
423 :param revision: The revision for which listing should be done.
424 :type revision: str
424 :type revision: str
425 :param root_path: The path from which to start displaying.
425 :param root_path: The path from which to start displaying.
426 :type root_path: str
426 :type root_path: str
427 :param ret_type: Set the return type. Valid options are
427 :param ret_type: Set the return type. Valid options are
428 ``all`` (default), ``files`` and ``dirs``.
428 ``all`` (default), ``files`` and ``dirs``.
429 :type ret_type: Optional(str)
429 :type ret_type: Optional(str)
430 :param details: Returns extended information about nodes, such as
430 :param details: Returns extended information about nodes, such as
431 md5, binary, and or content. The valid options are ``basic`` and
431 md5, binary, and or content. The valid options are ``basic`` and
432 ``full``.
432 ``full``.
433 :type details: Optional(str)
433 :type details: Optional(str)
434 :param max_file_bytes: Only return file content under this file size bytes
434 :param max_file_bytes: Only return file content under this file size bytes
435 :type details: Optional(int)
435 :type details: Optional(int)
436
436
437 Example output:
437 Example output:
438
438
439 .. code-block:: bash
439 .. code-block:: bash
440
440
441 id : <id_given_in_input>
441 id : <id_given_in_input>
442 result: [
442 result: [
443 {
443 {
444 "name" : "<name>"
444 "name" : "<name>"
445 "type" : "<type>",
445 "type" : "<type>",
446 "binary": "<true|false>" (only in extended mode)
446 "binary": "<true|false>" (only in extended mode)
447 "md5" : "<md5 of file content>" (only in extended mode)
447 "md5" : "<md5 of file content>" (only in extended mode)
448 },
448 },
449 ...
449 ...
450 ]
450 ]
451 error: null
451 error: null
452 """
452 """
453
453
454 repo = get_repo_or_error(repoid)
454 repo = get_repo_or_error(repoid)
455 if not has_superadmin_permission(apiuser):
455 if not has_superadmin_permission(apiuser):
456 _perms = (
456 _perms = (
457 'repository.admin', 'repository.write', 'repository.read',)
457 'repository.admin', 'repository.write', 'repository.read',)
458 validate_repo_permissions(apiuser, repoid, repo, _perms)
458 validate_repo_permissions(apiuser, repoid, repo, _perms)
459
459
460 ret_type = Optional.extract(ret_type)
460 ret_type = Optional.extract(ret_type)
461 details = Optional.extract(details)
461 details = Optional.extract(details)
462 _extended_types = ['basic', 'full']
462 _extended_types = ['basic', 'full']
463 if details not in _extended_types:
463 if details not in _extended_types:
464 raise JSONRPCError(
464 raise JSONRPCError(
465 'ret_type must be one of %s' % (','.join(_extended_types)))
465 'ret_type must be one of %s' % (','.join(_extended_types)))
466 extended_info = False
466 extended_info = False
467 content = False
467 content = False
468 if details == 'basic':
468 if details == 'basic':
469 extended_info = True
469 extended_info = True
470
470
471 if details == 'full':
471 if details == 'full':
472 extended_info = content = True
472 extended_info = content = True
473
473
474 _map = {}
474 _map = {}
475 try:
475 try:
476 # check if repo is not empty by any chance, skip quicker if it is.
476 # check if repo is not empty by any chance, skip quicker if it is.
477 _scm = repo.scm_instance()
477 _scm = repo.scm_instance()
478 if _scm.is_empty():
478 if _scm.is_empty():
479 return []
479 return []
480
480
481 _d, _f = ScmModel().get_nodes(
481 _d, _f = ScmModel().get_nodes(
482 repo, revision, root_path, flat=False,
482 repo, revision, root_path, flat=False,
483 extended_info=extended_info, content=content,
483 extended_info=extended_info, content=content,
484 max_file_bytes=max_file_bytes)
484 max_file_bytes=max_file_bytes)
485 _map = {
485 _map = {
486 'all': _d + _f,
486 'all': _d + _f,
487 'files': _f,
487 'files': _f,
488 'dirs': _d,
488 'dirs': _d,
489 }
489 }
490 return _map[ret_type]
490 return _map[ret_type]
491 except KeyError:
491 except KeyError:
492 raise JSONRPCError(
492 raise JSONRPCError(
493 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
493 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
494 except Exception:
494 except Exception:
495 log.exception("Exception occurred while trying to get repo nodes")
495 log.exception("Exception occurred while trying to get repo nodes")
496 raise JSONRPCError(
496 raise JSONRPCError(
497 'failed to get repo: `%s` nodes' % repo.repo_name
497 'failed to get repo: `%s` nodes' % repo.repo_name
498 )
498 )
499
499
500
500
501 @jsonrpc_method()
501 @jsonrpc_method()
502 def get_repo_refs(request, apiuser, repoid):
502 def get_repo_refs(request, apiuser, repoid):
503 """
503 """
504 Returns a dictionary of current references. It returns
504 Returns a dictionary of current references. It returns
505 bookmarks, branches, closed_branches, and tags for given repository
505 bookmarks, branches, closed_branches, and tags for given repository
506
506
507 It's possible to specify ret_type to show only `files` or `dirs`.
507 It's possible to specify ret_type to show only `files` or `dirs`.
508
508
509 This command can only be run using an |authtoken| with admin rights,
509 This command can only be run using an |authtoken| with admin rights,
510 or users with at least read rights to |repos|.
510 or users with at least read rights to |repos|.
511
511
512 :param apiuser: This is filled automatically from the |authtoken|.
512 :param apiuser: This is filled automatically from the |authtoken|.
513 :type apiuser: AuthUser
513 :type apiuser: AuthUser
514 :param repoid: The repository name or repository ID.
514 :param repoid: The repository name or repository ID.
515 :type repoid: str or int
515 :type repoid: str or int
516
516
517 Example output:
517 Example output:
518
518
519 .. code-block:: bash
519 .. code-block:: bash
520
520
521 id : <id_given_in_input>
521 id : <id_given_in_input>
522 "result": {
522 "result": {
523 "bookmarks": {
523 "bookmarks": {
524 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
524 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
525 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
525 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
526 },
526 },
527 "branches": {
527 "branches": {
528 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
528 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
529 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
529 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
530 },
530 },
531 "branches_closed": {},
531 "branches_closed": {},
532 "tags": {
532 "tags": {
533 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
533 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
534 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
534 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
535 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
535 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
536 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
536 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
537 }
537 }
538 }
538 }
539 error: null
539 error: null
540 """
540 """
541
541
542 repo = get_repo_or_error(repoid)
542 repo = get_repo_or_error(repoid)
543 if not has_superadmin_permission(apiuser):
543 if not has_superadmin_permission(apiuser):
544 _perms = ('repository.admin', 'repository.write', 'repository.read',)
544 _perms = ('repository.admin', 'repository.write', 'repository.read',)
545 validate_repo_permissions(apiuser, repoid, repo, _perms)
545 validate_repo_permissions(apiuser, repoid, repo, _perms)
546
546
547 try:
547 try:
548 # check if repo is not empty by any chance, skip quicker if it is.
548 # check if repo is not empty by any chance, skip quicker if it is.
549 vcs_instance = repo.scm_instance()
549 vcs_instance = repo.scm_instance()
550 refs = vcs_instance.refs()
550 refs = vcs_instance.refs()
551 return refs
551 return refs
552 except Exception:
552 except Exception:
553 log.exception("Exception occurred while trying to get repo refs")
553 log.exception("Exception occurred while trying to get repo refs")
554 raise JSONRPCError(
554 raise JSONRPCError(
555 'failed to get repo: `%s` references' % repo.repo_name
555 'failed to get repo: `%s` references' % repo.repo_name
556 )
556 )
557
557
558
558
559 @jsonrpc_method()
559 @jsonrpc_method()
560 def create_repo(
560 def create_repo(
561 request, apiuser, repo_name, repo_type,
561 request, apiuser, repo_name, repo_type,
562 owner=Optional(OAttr('apiuser')),
562 owner=Optional(OAttr('apiuser')),
563 description=Optional(''),
563 description=Optional(''),
564 private=Optional(False),
564 private=Optional(False),
565 clone_uri=Optional(None),
565 clone_uri=Optional(None),
566 push_uri=Optional(None),
566 push_uri=Optional(None),
567 landing_rev=Optional('rev:tip'),
567 landing_rev=Optional('rev:tip'),
568 enable_statistics=Optional(False),
568 enable_statistics=Optional(False),
569 enable_locking=Optional(False),
569 enable_locking=Optional(False),
570 enable_downloads=Optional(False),
570 enable_downloads=Optional(False),
571 copy_permissions=Optional(False)):
571 copy_permissions=Optional(False)):
572 """
572 """
573 Creates a repository.
573 Creates a repository.
574
574
575 * If the repository name contains "/", repository will be created inside
575 * If the repository name contains "/", repository will be created inside
576 a repository group or nested repository groups
576 a repository group or nested repository groups
577
577
578 For example "foo/bar/repo1" will create |repo| called "repo1" inside
578 For example "foo/bar/repo1" will create |repo| called "repo1" inside
579 group "foo/bar". You have to have permissions to access and write to
579 group "foo/bar". You have to have permissions to access and write to
580 the last repository group ("bar" in this example)
580 the last repository group ("bar" in this example)
581
581
582 This command can only be run using an |authtoken| with at least
582 This command can only be run using an |authtoken| with at least
583 permissions to create repositories, or write permissions to
583 permissions to create repositories, or write permissions to
584 parent repository groups.
584 parent repository groups.
585
585
586 :param apiuser: This is filled automatically from the |authtoken|.
586 :param apiuser: This is filled automatically from the |authtoken|.
587 :type apiuser: AuthUser
587 :type apiuser: AuthUser
588 :param repo_name: Set the repository name.
588 :param repo_name: Set the repository name.
589 :type repo_name: str
589 :type repo_name: str
590 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
590 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
591 :type repo_type: str
591 :type repo_type: str
592 :param owner: user_id or username
592 :param owner: user_id or username
593 :type owner: Optional(str)
593 :type owner: Optional(str)
594 :param description: Set the repository description.
594 :param description: Set the repository description.
595 :type description: Optional(str)
595 :type description: Optional(str)
596 :param private: set repository as private
596 :param private: set repository as private
597 :type private: bool
597 :type private: bool
598 :param clone_uri: set clone_uri
598 :param clone_uri: set clone_uri
599 :type clone_uri: str
599 :type clone_uri: str
600 :param push_uri: set push_uri
600 :param push_uri: set push_uri
601 :type push_uri: str
601 :type push_uri: str
602 :param landing_rev: <rev_type>:<rev>
602 :param landing_rev: <rev_type>:<rev>
603 :type landing_rev: str
603 :type landing_rev: str
604 :param enable_locking:
604 :param enable_locking:
605 :type enable_locking: bool
605 :type enable_locking: bool
606 :param enable_downloads:
606 :param enable_downloads:
607 :type enable_downloads: bool
607 :type enable_downloads: bool
608 :param enable_statistics:
608 :param enable_statistics:
609 :type enable_statistics: bool
609 :type enable_statistics: bool
610 :param copy_permissions: Copy permission from group in which the
610 :param copy_permissions: Copy permission from group in which the
611 repository is being created.
611 repository is being created.
612 :type copy_permissions: bool
612 :type copy_permissions: bool
613
613
614
614
615 Example output:
615 Example output:
616
616
617 .. code-block:: bash
617 .. code-block:: bash
618
618
619 id : <id_given_in_input>
619 id : <id_given_in_input>
620 result: {
620 result: {
621 "msg": "Created new repository `<reponame>`",
621 "msg": "Created new repository `<reponame>`",
622 "success": true,
622 "success": true,
623 "task": "<celery task id or None if done sync>"
623 "task": "<celery task id or None if done sync>"
624 }
624 }
625 error: null
625 error: null
626
626
627
627
628 Example error output:
628 Example error output:
629
629
630 .. code-block:: bash
630 .. code-block:: bash
631
631
632 id : <id_given_in_input>
632 id : <id_given_in_input>
633 result : null
633 result : null
634 error : {
634 error : {
635 'failed to create repository `<repo_name>`'
635 'failed to create repository `<repo_name>`'
636 }
636 }
637
637
638 """
638 """
639
639
640 owner = validate_set_owner_permissions(apiuser, owner)
640 owner = validate_set_owner_permissions(apiuser, owner)
641
641
642 description = Optional.extract(description)
642 description = Optional.extract(description)
643 copy_permissions = Optional.extract(copy_permissions)
643 copy_permissions = Optional.extract(copy_permissions)
644 clone_uri = Optional.extract(clone_uri)
644 clone_uri = Optional.extract(clone_uri)
645 push_uri = Optional.extract(push_uri)
645 push_uri = Optional.extract(push_uri)
646 landing_commit_ref = Optional.extract(landing_rev)
646 landing_commit_ref = Optional.extract(landing_rev)
647
647
648 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
648 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
649 if isinstance(private, Optional):
649 if isinstance(private, Optional):
650 private = defs.get('repo_private') or Optional.extract(private)
650 private = defs.get('repo_private') or Optional.extract(private)
651 if isinstance(repo_type, Optional):
651 if isinstance(repo_type, Optional):
652 repo_type = defs.get('repo_type')
652 repo_type = defs.get('repo_type')
653 if isinstance(enable_statistics, Optional):
653 if isinstance(enable_statistics, Optional):
654 enable_statistics = defs.get('repo_enable_statistics')
654 enable_statistics = defs.get('repo_enable_statistics')
655 if isinstance(enable_locking, Optional):
655 if isinstance(enable_locking, Optional):
656 enable_locking = defs.get('repo_enable_locking')
656 enable_locking = defs.get('repo_enable_locking')
657 if isinstance(enable_downloads, Optional):
657 if isinstance(enable_downloads, Optional):
658 enable_downloads = defs.get('repo_enable_downloads')
658 enable_downloads = defs.get('repo_enable_downloads')
659
659
660 schema = repo_schema.RepoSchema().bind(
660 schema = repo_schema.RepoSchema().bind(
661 repo_type_options=rhodecode.BACKENDS.keys(),
661 repo_type_options=rhodecode.BACKENDS.keys(),
662 repo_type=repo_type,
662 repo_type=repo_type,
663 # user caller
663 # user caller
664 user=apiuser)
664 user=apiuser)
665
665
666 try:
666 try:
667 schema_data = schema.deserialize(dict(
667 schema_data = schema.deserialize(dict(
668 repo_name=repo_name,
668 repo_name=repo_name,
669 repo_type=repo_type,
669 repo_type=repo_type,
670 repo_owner=owner.username,
670 repo_owner=owner.username,
671 repo_description=description,
671 repo_description=description,
672 repo_landing_commit_ref=landing_commit_ref,
672 repo_landing_commit_ref=landing_commit_ref,
673 repo_clone_uri=clone_uri,
673 repo_clone_uri=clone_uri,
674 repo_push_uri=push_uri,
674 repo_push_uri=push_uri,
675 repo_private=private,
675 repo_private=private,
676 repo_copy_permissions=copy_permissions,
676 repo_copy_permissions=copy_permissions,
677 repo_enable_statistics=enable_statistics,
677 repo_enable_statistics=enable_statistics,
678 repo_enable_downloads=enable_downloads,
678 repo_enable_downloads=enable_downloads,
679 repo_enable_locking=enable_locking))
679 repo_enable_locking=enable_locking))
680 except validation_schema.Invalid as err:
680 except validation_schema.Invalid as err:
681 raise JSONRPCValidationError(colander_exc=err)
681 raise JSONRPCValidationError(colander_exc=err)
682
682
683 try:
683 try:
684 data = {
684 data = {
685 'owner': owner,
685 'owner': owner,
686 'repo_name': schema_data['repo_group']['repo_name_without_group'],
686 'repo_name': schema_data['repo_group']['repo_name_without_group'],
687 'repo_name_full': schema_data['repo_name'],
687 'repo_name_full': schema_data['repo_name'],
688 'repo_group': schema_data['repo_group']['repo_group_id'],
688 'repo_group': schema_data['repo_group']['repo_group_id'],
689 'repo_type': schema_data['repo_type'],
689 'repo_type': schema_data['repo_type'],
690 'repo_description': schema_data['repo_description'],
690 'repo_description': schema_data['repo_description'],
691 'repo_private': schema_data['repo_private'],
691 'repo_private': schema_data['repo_private'],
692 'clone_uri': schema_data['repo_clone_uri'],
692 'clone_uri': schema_data['repo_clone_uri'],
693 'push_uri': schema_data['repo_push_uri'],
693 'push_uri': schema_data['repo_push_uri'],
694 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
694 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
695 'enable_statistics': schema_data['repo_enable_statistics'],
695 'enable_statistics': schema_data['repo_enable_statistics'],
696 'enable_locking': schema_data['repo_enable_locking'],
696 'enable_locking': schema_data['repo_enable_locking'],
697 'enable_downloads': schema_data['repo_enable_downloads'],
697 'enable_downloads': schema_data['repo_enable_downloads'],
698 'repo_copy_permissions': schema_data['repo_copy_permissions'],
698 'repo_copy_permissions': schema_data['repo_copy_permissions'],
699 }
699 }
700
700
701 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
701 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
702 task_id = get_task_id(task)
702 task_id = get_task_id(task)
703 # no commit, it's done in RepoModel, or async via celery
703 # no commit, it's done in RepoModel, or async via celery
704 return {
704 return {
705 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
705 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
706 'success': True, # cannot return the repo data here since fork
706 'success': True, # cannot return the repo data here since fork
707 # can be done async
707 # can be done async
708 'task': task_id
708 'task': task_id
709 }
709 }
710 except Exception:
710 except Exception:
711 log.exception(
711 log.exception(
712 u"Exception while trying to create the repository %s",
712 u"Exception while trying to create the repository %s",
713 schema_data['repo_name'])
713 schema_data['repo_name'])
714 raise JSONRPCError(
714 raise JSONRPCError(
715 'failed to create repository `%s`' % (schema_data['repo_name'],))
715 'failed to create repository `%s`' % (schema_data['repo_name'],))
716
716
717
717
718 @jsonrpc_method()
718 @jsonrpc_method()
719 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
719 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
720 description=Optional('')):
720 description=Optional('')):
721 """
721 """
722 Adds an extra field to a repository.
722 Adds an extra field to a repository.
723
723
724 This command can only be run using an |authtoken| with at least
724 This command can only be run using an |authtoken| with at least
725 write permissions to the |repo|.
725 write permissions to the |repo|.
726
726
727 :param apiuser: This is filled automatically from the |authtoken|.
727 :param apiuser: This is filled automatically from the |authtoken|.
728 :type apiuser: AuthUser
728 :type apiuser: AuthUser
729 :param repoid: Set the repository name or repository id.
729 :param repoid: Set the repository name or repository id.
730 :type repoid: str or int
730 :type repoid: str or int
731 :param key: Create a unique field key for this repository.
731 :param key: Create a unique field key for this repository.
732 :type key: str
732 :type key: str
733 :param label:
733 :param label:
734 :type label: Optional(str)
734 :type label: Optional(str)
735 :param description:
735 :param description:
736 :type description: Optional(str)
736 :type description: Optional(str)
737 """
737 """
738 repo = get_repo_or_error(repoid)
738 repo = get_repo_or_error(repoid)
739 if not has_superadmin_permission(apiuser):
739 if not has_superadmin_permission(apiuser):
740 _perms = ('repository.admin',)
740 _perms = ('repository.admin',)
741 validate_repo_permissions(apiuser, repoid, repo, _perms)
741 validate_repo_permissions(apiuser, repoid, repo, _perms)
742
742
743 label = Optional.extract(label) or key
743 label = Optional.extract(label) or key
744 description = Optional.extract(description)
744 description = Optional.extract(description)
745
745
746 field = RepositoryField.get_by_key_name(key, repo)
746 field = RepositoryField.get_by_key_name(key, repo)
747 if field:
747 if field:
748 raise JSONRPCError('Field with key '
748 raise JSONRPCError('Field with key '
749 '`%s` exists for repo `%s`' % (key, repoid))
749 '`%s` exists for repo `%s`' % (key, repoid))
750
750
751 try:
751 try:
752 RepoModel().add_repo_field(repo, key, field_label=label,
752 RepoModel().add_repo_field(repo, key, field_label=label,
753 field_desc=description)
753 field_desc=description)
754 Session().commit()
754 Session().commit()
755 return {
755 return {
756 'msg': "Added new repository field `%s`" % (key,),
756 'msg': "Added new repository field `%s`" % (key,),
757 'success': True,
757 'success': True,
758 }
758 }
759 except Exception:
759 except Exception:
760 log.exception("Exception occurred while trying to add field to repo")
760 log.exception("Exception occurred while trying to add field to repo")
761 raise JSONRPCError(
761 raise JSONRPCError(
762 'failed to create new field for repository `%s`' % (repoid,))
762 'failed to create new field for repository `%s`' % (repoid,))
763
763
764
764
765 @jsonrpc_method()
765 @jsonrpc_method()
766 def remove_field_from_repo(request, apiuser, repoid, key):
766 def remove_field_from_repo(request, apiuser, repoid, key):
767 """
767 """
768 Removes an extra field from a repository.
768 Removes an extra field from a repository.
769
769
770 This command can only be run using an |authtoken| with at least
770 This command can only be run using an |authtoken| with at least
771 write permissions to the |repo|.
771 write permissions to the |repo|.
772
772
773 :param apiuser: This is filled automatically from the |authtoken|.
773 :param apiuser: This is filled automatically from the |authtoken|.
774 :type apiuser: AuthUser
774 :type apiuser: AuthUser
775 :param repoid: Set the repository name or repository ID.
775 :param repoid: Set the repository name or repository ID.
776 :type repoid: str or int
776 :type repoid: str or int
777 :param key: Set the unique field key for this repository.
777 :param key: Set the unique field key for this repository.
778 :type key: str
778 :type key: str
779 """
779 """
780
780
781 repo = get_repo_or_error(repoid)
781 repo = get_repo_or_error(repoid)
782 if not has_superadmin_permission(apiuser):
782 if not has_superadmin_permission(apiuser):
783 _perms = ('repository.admin',)
783 _perms = ('repository.admin',)
784 validate_repo_permissions(apiuser, repoid, repo, _perms)
784 validate_repo_permissions(apiuser, repoid, repo, _perms)
785
785
786 field = RepositoryField.get_by_key_name(key, repo)
786 field = RepositoryField.get_by_key_name(key, repo)
787 if not field:
787 if not field:
788 raise JSONRPCError('Field with key `%s` does not '
788 raise JSONRPCError('Field with key `%s` does not '
789 'exists for repo `%s`' % (key, repoid))
789 'exists for repo `%s`' % (key, repoid))
790
790
791 try:
791 try:
792 RepoModel().delete_repo_field(repo, field_key=key)
792 RepoModel().delete_repo_field(repo, field_key=key)
793 Session().commit()
793 Session().commit()
794 return {
794 return {
795 'msg': "Deleted repository field `%s`" % (key,),
795 'msg': "Deleted repository field `%s`" % (key,),
796 'success': True,
796 'success': True,
797 }
797 }
798 except Exception:
798 except Exception:
799 log.exception(
799 log.exception(
800 "Exception occurred while trying to delete field from repo")
800 "Exception occurred while trying to delete field from repo")
801 raise JSONRPCError(
801 raise JSONRPCError(
802 'failed to delete field for repository `%s`' % (repoid,))
802 'failed to delete field for repository `%s`' % (repoid,))
803
803
804
804
805 @jsonrpc_method()
805 @jsonrpc_method()
806 def update_repo(
806 def update_repo(
807 request, apiuser, repoid, repo_name=Optional(None),
807 request, apiuser, repoid, repo_name=Optional(None),
808 owner=Optional(OAttr('apiuser')), description=Optional(''),
808 owner=Optional(OAttr('apiuser')), description=Optional(''),
809 private=Optional(False),
809 private=Optional(False),
810 clone_uri=Optional(None), push_uri=Optional(None),
810 clone_uri=Optional(None), push_uri=Optional(None),
811 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
811 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
812 enable_statistics=Optional(False),
812 enable_statistics=Optional(False),
813 enable_locking=Optional(False),
813 enable_locking=Optional(False),
814 enable_downloads=Optional(False), fields=Optional('')):
814 enable_downloads=Optional(False), fields=Optional('')):
815 """
815 """
816 Updates a repository with the given information.
816 Updates a repository with the given information.
817
817
818 This command can only be run using an |authtoken| with at least
818 This command can only be run using an |authtoken| with at least
819 admin permissions to the |repo|.
819 admin permissions to the |repo|.
820
820
821 * If the repository name contains "/", repository will be updated
821 * If the repository name contains "/", repository will be updated
822 accordingly with a repository group or nested repository groups
822 accordingly with a repository group or nested repository groups
823
823
824 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
824 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
825 called "repo-test" and place it inside group "foo/bar".
825 called "repo-test" and place it inside group "foo/bar".
826 You have to have permissions to access and write to the last repository
826 You have to have permissions to access and write to the last repository
827 group ("bar" in this example)
827 group ("bar" in this example)
828
828
829 :param apiuser: This is filled automatically from the |authtoken|.
829 :param apiuser: This is filled automatically from the |authtoken|.
830 :type apiuser: AuthUser
830 :type apiuser: AuthUser
831 :param repoid: repository name or repository ID.
831 :param repoid: repository name or repository ID.
832 :type repoid: str or int
832 :type repoid: str or int
833 :param repo_name: Update the |repo| name, including the
833 :param repo_name: Update the |repo| name, including the
834 repository group it's in.
834 repository group it's in.
835 :type repo_name: str
835 :type repo_name: str
836 :param owner: Set the |repo| owner.
836 :param owner: Set the |repo| owner.
837 :type owner: str
837 :type owner: str
838 :param fork_of: Set the |repo| as fork of another |repo|.
838 :param fork_of: Set the |repo| as fork of another |repo|.
839 :type fork_of: str
839 :type fork_of: str
840 :param description: Update the |repo| description.
840 :param description: Update the |repo| description.
841 :type description: str
841 :type description: str
842 :param private: Set the |repo| as private. (True | False)
842 :param private: Set the |repo| as private. (True | False)
843 :type private: bool
843 :type private: bool
844 :param clone_uri: Update the |repo| clone URI.
844 :param clone_uri: Update the |repo| clone URI.
845 :type clone_uri: str
845 :type clone_uri: str
846 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
846 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
847 :type landing_rev: str
847 :type landing_rev: str
848 :param enable_statistics: Enable statistics on the |repo|, (True | False).
848 :param enable_statistics: Enable statistics on the |repo|, (True | False).
849 :type enable_statistics: bool
849 :type enable_statistics: bool
850 :param enable_locking: Enable |repo| locking.
850 :param enable_locking: Enable |repo| locking.
851 :type enable_locking: bool
851 :type enable_locking: bool
852 :param enable_downloads: Enable downloads from the |repo|, (True | False).
852 :param enable_downloads: Enable downloads from the |repo|, (True | False).
853 :type enable_downloads: bool
853 :type enable_downloads: bool
854 :param fields: Add extra fields to the |repo|. Use the following
854 :param fields: Add extra fields to the |repo|. Use the following
855 example format: ``field_key=field_val,field_key2=fieldval2``.
855 example format: ``field_key=field_val,field_key2=fieldval2``.
856 Escape ', ' with \,
856 Escape ', ' with \,
857 :type fields: str
857 :type fields: str
858 """
858 """
859
859
860 repo = get_repo_or_error(repoid)
860 repo = get_repo_or_error(repoid)
861
861
862 include_secrets = False
862 include_secrets = False
863 if not has_superadmin_permission(apiuser):
863 if not has_superadmin_permission(apiuser):
864 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
864 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
865 else:
865 else:
866 include_secrets = True
866 include_secrets = True
867
867
868 updates = dict(
868 updates = dict(
869 repo_name=repo_name
869 repo_name=repo_name
870 if not isinstance(repo_name, Optional) else repo.repo_name,
870 if not isinstance(repo_name, Optional) else repo.repo_name,
871
871
872 fork_id=fork_of
872 fork_id=fork_of
873 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
873 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
874
874
875 user=owner
875 user=owner
876 if not isinstance(owner, Optional) else repo.user.username,
876 if not isinstance(owner, Optional) else repo.user.username,
877
877
878 repo_description=description
878 repo_description=description
879 if not isinstance(description, Optional) else repo.description,
879 if not isinstance(description, Optional) else repo.description,
880
880
881 repo_private=private
881 repo_private=private
882 if not isinstance(private, Optional) else repo.private,
882 if not isinstance(private, Optional) else repo.private,
883
883
884 clone_uri=clone_uri
884 clone_uri=clone_uri
885 if not isinstance(clone_uri, Optional) else repo.clone_uri,
885 if not isinstance(clone_uri, Optional) else repo.clone_uri,
886
886
887 push_uri=push_uri
887 push_uri=push_uri
888 if not isinstance(push_uri, Optional) else repo.push_uri,
888 if not isinstance(push_uri, Optional) else repo.push_uri,
889
889
890 repo_landing_rev=landing_rev
890 repo_landing_rev=landing_rev
891 if not isinstance(landing_rev, Optional) else repo._landing_revision,
891 if not isinstance(landing_rev, Optional) else repo._landing_revision,
892
892
893 repo_enable_statistics=enable_statistics
893 repo_enable_statistics=enable_statistics
894 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
894 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
895
895
896 repo_enable_locking=enable_locking
896 repo_enable_locking=enable_locking
897 if not isinstance(enable_locking, Optional) else repo.enable_locking,
897 if not isinstance(enable_locking, Optional) else repo.enable_locking,
898
898
899 repo_enable_downloads=enable_downloads
899 repo_enable_downloads=enable_downloads
900 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
900 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
901
901
902 ref_choices, _labels = ScmModel().get_repo_landing_revs(
902 ref_choices, _labels = ScmModel().get_repo_landing_revs(
903 request.translate, repo=repo)
903 request.translate, repo=repo)
904
904
905 old_values = repo.get_api_data()
905 old_values = repo.get_api_data()
906 repo_type = repo.repo_type
906 repo_type = repo.repo_type
907 schema = repo_schema.RepoSchema().bind(
907 schema = repo_schema.RepoSchema().bind(
908 repo_type_options=rhodecode.BACKENDS.keys(),
908 repo_type_options=rhodecode.BACKENDS.keys(),
909 repo_ref_options=ref_choices,
909 repo_ref_options=ref_choices,
910 repo_type=repo_type,
910 repo_type=repo_type,
911 # user caller
911 # user caller
912 user=apiuser,
912 user=apiuser,
913 old_values=old_values)
913 old_values=old_values)
914 try:
914 try:
915 schema_data = schema.deserialize(dict(
915 schema_data = schema.deserialize(dict(
916 # we save old value, users cannot change type
916 # we save old value, users cannot change type
917 repo_type=repo_type,
917 repo_type=repo_type,
918
918
919 repo_name=updates['repo_name'],
919 repo_name=updates['repo_name'],
920 repo_owner=updates['user'],
920 repo_owner=updates['user'],
921 repo_description=updates['repo_description'],
921 repo_description=updates['repo_description'],
922 repo_clone_uri=updates['clone_uri'],
922 repo_clone_uri=updates['clone_uri'],
923 repo_push_uri=updates['push_uri'],
923 repo_push_uri=updates['push_uri'],
924 repo_fork_of=updates['fork_id'],
924 repo_fork_of=updates['fork_id'],
925 repo_private=updates['repo_private'],
925 repo_private=updates['repo_private'],
926 repo_landing_commit_ref=updates['repo_landing_rev'],
926 repo_landing_commit_ref=updates['repo_landing_rev'],
927 repo_enable_statistics=updates['repo_enable_statistics'],
927 repo_enable_statistics=updates['repo_enable_statistics'],
928 repo_enable_downloads=updates['repo_enable_downloads'],
928 repo_enable_downloads=updates['repo_enable_downloads'],
929 repo_enable_locking=updates['repo_enable_locking']))
929 repo_enable_locking=updates['repo_enable_locking']))
930 except validation_schema.Invalid as err:
930 except validation_schema.Invalid as err:
931 raise JSONRPCValidationError(colander_exc=err)
931 raise JSONRPCValidationError(colander_exc=err)
932
932
933 # save validated data back into the updates dict
933 # save validated data back into the updates dict
934 validated_updates = dict(
934 validated_updates = dict(
935 repo_name=schema_data['repo_group']['repo_name_without_group'],
935 repo_name=schema_data['repo_group']['repo_name_without_group'],
936 repo_group=schema_data['repo_group']['repo_group_id'],
936 repo_group=schema_data['repo_group']['repo_group_id'],
937
937
938 user=schema_data['repo_owner'],
938 user=schema_data['repo_owner'],
939 repo_description=schema_data['repo_description'],
939 repo_description=schema_data['repo_description'],
940 repo_private=schema_data['repo_private'],
940 repo_private=schema_data['repo_private'],
941 clone_uri=schema_data['repo_clone_uri'],
941 clone_uri=schema_data['repo_clone_uri'],
942 push_uri=schema_data['repo_push_uri'],
942 push_uri=schema_data['repo_push_uri'],
943 repo_landing_rev=schema_data['repo_landing_commit_ref'],
943 repo_landing_rev=schema_data['repo_landing_commit_ref'],
944 repo_enable_statistics=schema_data['repo_enable_statistics'],
944 repo_enable_statistics=schema_data['repo_enable_statistics'],
945 repo_enable_locking=schema_data['repo_enable_locking'],
945 repo_enable_locking=schema_data['repo_enable_locking'],
946 repo_enable_downloads=schema_data['repo_enable_downloads'],
946 repo_enable_downloads=schema_data['repo_enable_downloads'],
947 )
947 )
948
948
949 if schema_data['repo_fork_of']:
949 if schema_data['repo_fork_of']:
950 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
950 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
951 validated_updates['fork_id'] = fork_repo.repo_id
951 validated_updates['fork_id'] = fork_repo.repo_id
952
952
953 # extra fields
953 # extra fields
954 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
954 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
955 if fields:
955 if fields:
956 validated_updates.update(fields)
956 validated_updates.update(fields)
957
957
958 try:
958 try:
959 RepoModel().update(repo, **validated_updates)
959 RepoModel().update(repo, **validated_updates)
960 audit_logger.store_api(
960 audit_logger.store_api(
961 'repo.edit', action_data={'old_data': old_values},
961 'repo.edit', action_data={'old_data': old_values},
962 user=apiuser, repo=repo)
962 user=apiuser, repo=repo)
963 Session().commit()
963 Session().commit()
964 return {
964 return {
965 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
965 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
966 'repository': repo.get_api_data(include_secrets=include_secrets)
966 'repository': repo.get_api_data(include_secrets=include_secrets)
967 }
967 }
968 except Exception:
968 except Exception:
969 log.exception(
969 log.exception(
970 u"Exception while trying to update the repository %s",
970 u"Exception while trying to update the repository %s",
971 repoid)
971 repoid)
972 raise JSONRPCError('failed to update repo `%s`' % repoid)
972 raise JSONRPCError('failed to update repo `%s`' % repoid)
973
973
974
974
975 @jsonrpc_method()
975 @jsonrpc_method()
976 def fork_repo(request, apiuser, repoid, fork_name,
976 def fork_repo(request, apiuser, repoid, fork_name,
977 owner=Optional(OAttr('apiuser')),
977 owner=Optional(OAttr('apiuser')),
978 description=Optional(''),
978 description=Optional(''),
979 private=Optional(False),
979 private=Optional(False),
980 clone_uri=Optional(None),
980 clone_uri=Optional(None),
981 landing_rev=Optional('rev:tip'),
981 landing_rev=Optional('rev:tip'),
982 copy_permissions=Optional(False)):
982 copy_permissions=Optional(False)):
983 """
983 """
984 Creates a fork of the specified |repo|.
984 Creates a fork of the specified |repo|.
985
985
986 * If the fork_name contains "/", fork will be created inside
986 * If the fork_name contains "/", fork will be created inside
987 a repository group or nested repository groups
987 a repository group or nested repository groups
988
988
989 For example "foo/bar/fork-repo" will create fork called "fork-repo"
989 For example "foo/bar/fork-repo" will create fork called "fork-repo"
990 inside group "foo/bar". You have to have permissions to access and
990 inside group "foo/bar". You have to have permissions to access and
991 write to the last repository group ("bar" in this example)
991 write to the last repository group ("bar" in this example)
992
992
993 This command can only be run using an |authtoken| with minimum
993 This command can only be run using an |authtoken| with minimum
994 read permissions of the forked repo, create fork permissions for an user.
994 read permissions of the forked repo, create fork permissions for an user.
995
995
996 :param apiuser: This is filled automatically from the |authtoken|.
996 :param apiuser: This is filled automatically from the |authtoken|.
997 :type apiuser: AuthUser
997 :type apiuser: AuthUser
998 :param repoid: Set repository name or repository ID.
998 :param repoid: Set repository name or repository ID.
999 :type repoid: str or int
999 :type repoid: str or int
1000 :param fork_name: Set the fork name, including it's repository group membership.
1000 :param fork_name: Set the fork name, including it's repository group membership.
1001 :type fork_name: str
1001 :type fork_name: str
1002 :param owner: Set the fork owner.
1002 :param owner: Set the fork owner.
1003 :type owner: str
1003 :type owner: str
1004 :param description: Set the fork description.
1004 :param description: Set the fork description.
1005 :type description: str
1005 :type description: str
1006 :param copy_permissions: Copy permissions from parent |repo|. The
1006 :param copy_permissions: Copy permissions from parent |repo|. The
1007 default is False.
1007 default is False.
1008 :type copy_permissions: bool
1008 :type copy_permissions: bool
1009 :param private: Make the fork private. The default is False.
1009 :param private: Make the fork private. The default is False.
1010 :type private: bool
1010 :type private: bool
1011 :param landing_rev: Set the landing revision. The default is tip.
1011 :param landing_rev: Set the landing revision. The default is tip.
1012
1012
1013 Example output:
1013 Example output:
1014
1014
1015 .. code-block:: bash
1015 .. code-block:: bash
1016
1016
1017 id : <id_for_response>
1017 id : <id_for_response>
1018 api_key : "<api_key>"
1018 api_key : "<api_key>"
1019 args: {
1019 args: {
1020 "repoid" : "<reponame or repo_id>",
1020 "repoid" : "<reponame or repo_id>",
1021 "fork_name": "<forkname>",
1021 "fork_name": "<forkname>",
1022 "owner": "<username or user_id = Optional(=apiuser)>",
1022 "owner": "<username or user_id = Optional(=apiuser)>",
1023 "description": "<description>",
1023 "description": "<description>",
1024 "copy_permissions": "<bool>",
1024 "copy_permissions": "<bool>",
1025 "private": "<bool>",
1025 "private": "<bool>",
1026 "landing_rev": "<landing_rev>"
1026 "landing_rev": "<landing_rev>"
1027 }
1027 }
1028
1028
1029 Example error output:
1029 Example error output:
1030
1030
1031 .. code-block:: bash
1031 .. code-block:: bash
1032
1032
1033 id : <id_given_in_input>
1033 id : <id_given_in_input>
1034 result: {
1034 result: {
1035 "msg": "Created fork of `<reponame>` as `<forkname>`",
1035 "msg": "Created fork of `<reponame>` as `<forkname>`",
1036 "success": true,
1036 "success": true,
1037 "task": "<celery task id or None if done sync>"
1037 "task": "<celery task id or None if done sync>"
1038 }
1038 }
1039 error: null
1039 error: null
1040
1040
1041 """
1041 """
1042
1042
1043 repo = get_repo_or_error(repoid)
1043 repo = get_repo_or_error(repoid)
1044 repo_name = repo.repo_name
1044 repo_name = repo.repo_name
1045
1045
1046 if not has_superadmin_permission(apiuser):
1046 if not has_superadmin_permission(apiuser):
1047 # check if we have at least read permission for
1047 # check if we have at least read permission for
1048 # this repo that we fork !
1048 # this repo that we fork !
1049 _perms = (
1049 _perms = (
1050 'repository.admin', 'repository.write', 'repository.read')
1050 'repository.admin', 'repository.write', 'repository.read')
1051 validate_repo_permissions(apiuser, repoid, repo, _perms)
1051 validate_repo_permissions(apiuser, repoid, repo, _perms)
1052
1052
1053 # check if the regular user has at least fork permissions as well
1053 # check if the regular user has at least fork permissions as well
1054 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1054 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1055 raise JSONRPCForbidden()
1055 raise JSONRPCForbidden()
1056
1056
1057 # check if user can set owner parameter
1057 # check if user can set owner parameter
1058 owner = validate_set_owner_permissions(apiuser, owner)
1058 owner = validate_set_owner_permissions(apiuser, owner)
1059
1059
1060 description = Optional.extract(description)
1060 description = Optional.extract(description)
1061 copy_permissions = Optional.extract(copy_permissions)
1061 copy_permissions = Optional.extract(copy_permissions)
1062 clone_uri = Optional.extract(clone_uri)
1062 clone_uri = Optional.extract(clone_uri)
1063 landing_commit_ref = Optional.extract(landing_rev)
1063 landing_commit_ref = Optional.extract(landing_rev)
1064 private = Optional.extract(private)
1064 private = Optional.extract(private)
1065
1065
1066 schema = repo_schema.RepoSchema().bind(
1066 schema = repo_schema.RepoSchema().bind(
1067 repo_type_options=rhodecode.BACKENDS.keys(),
1067 repo_type_options=rhodecode.BACKENDS.keys(),
1068 repo_type=repo.repo_type,
1068 repo_type=repo.repo_type,
1069 # user caller
1069 # user caller
1070 user=apiuser)
1070 user=apiuser)
1071
1071
1072 try:
1072 try:
1073 schema_data = schema.deserialize(dict(
1073 schema_data = schema.deserialize(dict(
1074 repo_name=fork_name,
1074 repo_name=fork_name,
1075 repo_type=repo.repo_type,
1075 repo_type=repo.repo_type,
1076 repo_owner=owner.username,
1076 repo_owner=owner.username,
1077 repo_description=description,
1077 repo_description=description,
1078 repo_landing_commit_ref=landing_commit_ref,
1078 repo_landing_commit_ref=landing_commit_ref,
1079 repo_clone_uri=clone_uri,
1079 repo_clone_uri=clone_uri,
1080 repo_private=private,
1080 repo_private=private,
1081 repo_copy_permissions=copy_permissions))
1081 repo_copy_permissions=copy_permissions))
1082 except validation_schema.Invalid as err:
1082 except validation_schema.Invalid as err:
1083 raise JSONRPCValidationError(colander_exc=err)
1083 raise JSONRPCValidationError(colander_exc=err)
1084
1084
1085 try:
1085 try:
1086 data = {
1086 data = {
1087 'fork_parent_id': repo.repo_id,
1087 'fork_parent_id': repo.repo_id,
1088
1088
1089 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1089 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1090 'repo_name_full': schema_data['repo_name'],
1090 'repo_name_full': schema_data['repo_name'],
1091 'repo_group': schema_data['repo_group']['repo_group_id'],
1091 'repo_group': schema_data['repo_group']['repo_group_id'],
1092 'repo_type': schema_data['repo_type'],
1092 'repo_type': schema_data['repo_type'],
1093 'description': schema_data['repo_description'],
1093 'description': schema_data['repo_description'],
1094 'private': schema_data['repo_private'],
1094 'private': schema_data['repo_private'],
1095 'copy_permissions': schema_data['repo_copy_permissions'],
1095 'copy_permissions': schema_data['repo_copy_permissions'],
1096 'landing_rev': schema_data['repo_landing_commit_ref'],
1096 'landing_rev': schema_data['repo_landing_commit_ref'],
1097 }
1097 }
1098
1098
1099 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1099 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1100 # no commit, it's done in RepoModel, or async via celery
1100 # no commit, it's done in RepoModel, or async via celery
1101 task_id = get_task_id(task)
1101 task_id = get_task_id(task)
1102
1102
1103 return {
1103 return {
1104 'msg': 'Created fork of `%s` as `%s`' % (
1104 'msg': 'Created fork of `%s` as `%s`' % (
1105 repo.repo_name, schema_data['repo_name']),
1105 repo.repo_name, schema_data['repo_name']),
1106 'success': True, # cannot return the repo data here since fork
1106 'success': True, # cannot return the repo data here since fork
1107 # can be done async
1107 # can be done async
1108 'task': task_id
1108 'task': task_id
1109 }
1109 }
1110 except Exception:
1110 except Exception:
1111 log.exception(
1111 log.exception(
1112 u"Exception while trying to create fork %s",
1112 u"Exception while trying to create fork %s",
1113 schema_data['repo_name'])
1113 schema_data['repo_name'])
1114 raise JSONRPCError(
1114 raise JSONRPCError(
1115 'failed to fork repository `%s` as `%s`' % (
1115 'failed to fork repository `%s` as `%s`' % (
1116 repo_name, schema_data['repo_name']))
1116 repo_name, schema_data['repo_name']))
1117
1117
1118
1118
1119 @jsonrpc_method()
1119 @jsonrpc_method()
1120 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1120 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1121 """
1121 """
1122 Deletes a repository.
1122 Deletes a repository.
1123
1123
1124 * When the `forks` parameter is set it's possible to detach or delete
1124 * When the `forks` parameter is set it's possible to detach or delete
1125 forks of deleted repository.
1125 forks of deleted repository.
1126
1126
1127 This command can only be run using an |authtoken| with admin
1127 This command can only be run using an |authtoken| with admin
1128 permissions on the |repo|.
1128 permissions on the |repo|.
1129
1129
1130 :param apiuser: This is filled automatically from the |authtoken|.
1130 :param apiuser: This is filled automatically from the |authtoken|.
1131 :type apiuser: AuthUser
1131 :type apiuser: AuthUser
1132 :param repoid: Set the repository name or repository ID.
1132 :param repoid: Set the repository name or repository ID.
1133 :type repoid: str or int
1133 :type repoid: str or int
1134 :param forks: Set to `detach` or `delete` forks from the |repo|.
1134 :param forks: Set to `detach` or `delete` forks from the |repo|.
1135 :type forks: Optional(str)
1135 :type forks: Optional(str)
1136
1136
1137 Example error output:
1137 Example error output:
1138
1138
1139 .. code-block:: bash
1139 .. code-block:: bash
1140
1140
1141 id : <id_given_in_input>
1141 id : <id_given_in_input>
1142 result: {
1142 result: {
1143 "msg": "Deleted repository `<reponame>`",
1143 "msg": "Deleted repository `<reponame>`",
1144 "success": true
1144 "success": true
1145 }
1145 }
1146 error: null
1146 error: null
1147 """
1147 """
1148
1148
1149 repo = get_repo_or_error(repoid)
1149 repo = get_repo_or_error(repoid)
1150 repo_name = repo.repo_name
1150 repo_name = repo.repo_name
1151 if not has_superadmin_permission(apiuser):
1151 if not has_superadmin_permission(apiuser):
1152 _perms = ('repository.admin',)
1152 _perms = ('repository.admin',)
1153 validate_repo_permissions(apiuser, repoid, repo, _perms)
1153 validate_repo_permissions(apiuser, repoid, repo, _perms)
1154
1154
1155 try:
1155 try:
1156 handle_forks = Optional.extract(forks)
1156 handle_forks = Optional.extract(forks)
1157 _forks_msg = ''
1157 _forks_msg = ''
1158 _forks = [f for f in repo.forks]
1158 _forks = [f for f in repo.forks]
1159 if handle_forks == 'detach':
1159 if handle_forks == 'detach':
1160 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1160 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1161 elif handle_forks == 'delete':
1161 elif handle_forks == 'delete':
1162 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1162 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1163 elif _forks:
1163 elif _forks:
1164 raise JSONRPCError(
1164 raise JSONRPCError(
1165 'Cannot delete `%s` it still contains attached forks' %
1165 'Cannot delete `%s` it still contains attached forks' %
1166 (repo.repo_name,)
1166 (repo.repo_name,)
1167 )
1167 )
1168 old_data = repo.get_api_data()
1168 old_data = repo.get_api_data()
1169 RepoModel().delete(repo, forks=forks)
1169 RepoModel().delete(repo, forks=forks)
1170
1170
1171 repo = audit_logger.RepoWrap(repo_id=None,
1171 repo = audit_logger.RepoWrap(repo_id=None,
1172 repo_name=repo.repo_name)
1172 repo_name=repo.repo_name)
1173
1173
1174 audit_logger.store_api(
1174 audit_logger.store_api(
1175 'repo.delete', action_data={'old_data': old_data},
1175 'repo.delete', action_data={'old_data': old_data},
1176 user=apiuser, repo=repo)
1176 user=apiuser, repo=repo)
1177
1177
1178 ScmModel().mark_for_invalidation(repo_name, delete=True)
1178 ScmModel().mark_for_invalidation(repo_name, delete=True)
1179 Session().commit()
1179 Session().commit()
1180 return {
1180 return {
1181 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1181 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1182 'success': True
1182 'success': True
1183 }
1183 }
1184 except Exception:
1184 except Exception:
1185 log.exception("Exception occurred while trying to delete repo")
1185 log.exception("Exception occurred while trying to delete repo")
1186 raise JSONRPCError(
1186 raise JSONRPCError(
1187 'failed to delete repository `%s`' % (repo_name,)
1187 'failed to delete repository `%s`' % (repo_name,)
1188 )
1188 )
1189
1189
1190
1190
1191 #TODO: marcink, change name ?
1191 #TODO: marcink, change name ?
1192 @jsonrpc_method()
1192 @jsonrpc_method()
1193 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1193 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1194 """
1194 """
1195 Invalidates the cache for the specified repository.
1195 Invalidates the cache for the specified repository.
1196
1196
1197 This command can only be run using an |authtoken| with admin rights to
1197 This command can only be run using an |authtoken| with admin rights to
1198 the specified repository.
1198 the specified repository.
1199
1199
1200 This command takes the following options:
1200 This command takes the following options:
1201
1201
1202 :param apiuser: This is filled automatically from |authtoken|.
1202 :param apiuser: This is filled automatically from |authtoken|.
1203 :type apiuser: AuthUser
1203 :type apiuser: AuthUser
1204 :param repoid: Sets the repository name or repository ID.
1204 :param repoid: Sets the repository name or repository ID.
1205 :type repoid: str or int
1205 :type repoid: str or int
1206 :param delete_keys: This deletes the invalidated keys instead of
1206 :param delete_keys: This deletes the invalidated keys instead of
1207 just flagging them.
1207 just flagging them.
1208 :type delete_keys: Optional(``True`` | ``False``)
1208 :type delete_keys: Optional(``True`` | ``False``)
1209
1209
1210 Example output:
1210 Example output:
1211
1211
1212 .. code-block:: bash
1212 .. code-block:: bash
1213
1213
1214 id : <id_given_in_input>
1214 id : <id_given_in_input>
1215 result : {
1215 result : {
1216 'msg': Cache for repository `<repository name>` was invalidated,
1216 'msg': Cache for repository `<repository name>` was invalidated,
1217 'repository': <repository name>
1217 'repository': <repository name>
1218 }
1218 }
1219 error : null
1219 error : null
1220
1220
1221 Example error output:
1221 Example error output:
1222
1222
1223 .. code-block:: bash
1223 .. code-block:: bash
1224
1224
1225 id : <id_given_in_input>
1225 id : <id_given_in_input>
1226 result : null
1226 result : null
1227 error : {
1227 error : {
1228 'Error occurred during cache invalidation action'
1228 'Error occurred during cache invalidation action'
1229 }
1229 }
1230
1230
1231 """
1231 """
1232
1232
1233 repo = get_repo_or_error(repoid)
1233 repo = get_repo_or_error(repoid)
1234 if not has_superadmin_permission(apiuser):
1234 if not has_superadmin_permission(apiuser):
1235 _perms = ('repository.admin', 'repository.write',)
1235 _perms = ('repository.admin', 'repository.write',)
1236 validate_repo_permissions(apiuser, repoid, repo, _perms)
1236 validate_repo_permissions(apiuser, repoid, repo, _perms)
1237
1237
1238 delete = Optional.extract(delete_keys)
1238 delete = Optional.extract(delete_keys)
1239 try:
1239 try:
1240 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1240 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1241 return {
1241 return {
1242 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1242 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1243 'repository': repo.repo_name
1243 'repository': repo.repo_name
1244 }
1244 }
1245 except Exception:
1245 except Exception:
1246 log.exception(
1246 log.exception(
1247 "Exception occurred while trying to invalidate repo cache")
1247 "Exception occurred while trying to invalidate repo cache")
1248 raise JSONRPCError(
1248 raise JSONRPCError(
1249 'Error occurred during cache invalidation action'
1249 'Error occurred during cache invalidation action'
1250 )
1250 )
1251
1251
1252
1252
1253 #TODO: marcink, change name ?
1253 #TODO: marcink, change name ?
1254 @jsonrpc_method()
1254 @jsonrpc_method()
1255 def lock(request, apiuser, repoid, locked=Optional(None),
1255 def lock(request, apiuser, repoid, locked=Optional(None),
1256 userid=Optional(OAttr('apiuser'))):
1256 userid=Optional(OAttr('apiuser'))):
1257 """
1257 """
1258 Sets the lock state of the specified |repo| by the given user.
1258 Sets the lock state of the specified |repo| by the given user.
1259 From more information, see :ref:`repo-locking`.
1259 From more information, see :ref:`repo-locking`.
1260
1260
1261 * If the ``userid`` option is not set, the repository is locked to the
1261 * If the ``userid`` option is not set, the repository is locked to the
1262 user who called the method.
1262 user who called the method.
1263 * If the ``locked`` parameter is not set, the current lock state of the
1263 * If the ``locked`` parameter is not set, the current lock state of the
1264 repository is displayed.
1264 repository is displayed.
1265
1265
1266 This command can only be run using an |authtoken| with admin rights to
1266 This command can only be run using an |authtoken| with admin rights to
1267 the specified repository.
1267 the specified repository.
1268
1268
1269 This command takes the following options:
1269 This command takes the following options:
1270
1270
1271 :param apiuser: This is filled automatically from the |authtoken|.
1271 :param apiuser: This is filled automatically from the |authtoken|.
1272 :type apiuser: AuthUser
1272 :type apiuser: AuthUser
1273 :param repoid: Sets the repository name or repository ID.
1273 :param repoid: Sets the repository name or repository ID.
1274 :type repoid: str or int
1274 :type repoid: str or int
1275 :param locked: Sets the lock state.
1275 :param locked: Sets the lock state.
1276 :type locked: Optional(``True`` | ``False``)
1276 :type locked: Optional(``True`` | ``False``)
1277 :param userid: Set the repository lock to this user.
1277 :param userid: Set the repository lock to this user.
1278 :type userid: Optional(str or int)
1278 :type userid: Optional(str or int)
1279
1279
1280 Example error output:
1280 Example error output:
1281
1281
1282 .. code-block:: bash
1282 .. code-block:: bash
1283
1283
1284 id : <id_given_in_input>
1284 id : <id_given_in_input>
1285 result : {
1285 result : {
1286 'repo': '<reponame>',
1286 'repo': '<reponame>',
1287 'locked': <bool: lock state>,
1287 'locked': <bool: lock state>,
1288 'locked_since': <int: lock timestamp>,
1288 'locked_since': <int: lock timestamp>,
1289 'locked_by': <username of person who made the lock>,
1289 'locked_by': <username of person who made the lock>,
1290 'lock_reason': <str: reason for locking>,
1290 'lock_reason': <str: reason for locking>,
1291 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1291 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1292 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1292 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1293 or
1293 or
1294 'msg': 'Repo `<repository name>` not locked.'
1294 'msg': 'Repo `<repository name>` not locked.'
1295 or
1295 or
1296 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1296 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1297 }
1297 }
1298 error : null
1298 error : null
1299
1299
1300 Example error output:
1300 Example error output:
1301
1301
1302 .. code-block:: bash
1302 .. code-block:: bash
1303
1303
1304 id : <id_given_in_input>
1304 id : <id_given_in_input>
1305 result : null
1305 result : null
1306 error : {
1306 error : {
1307 'Error occurred locking repository `<reponame>`'
1307 'Error occurred locking repository `<reponame>`'
1308 }
1308 }
1309 """
1309 """
1310
1310
1311 repo = get_repo_or_error(repoid)
1311 repo = get_repo_or_error(repoid)
1312 if not has_superadmin_permission(apiuser):
1312 if not has_superadmin_permission(apiuser):
1313 # check if we have at least write permission for this repo !
1313 # check if we have at least write permission for this repo !
1314 _perms = ('repository.admin', 'repository.write',)
1314 _perms = ('repository.admin', 'repository.write',)
1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1316
1316
1317 # make sure normal user does not pass someone else userid,
1317 # make sure normal user does not pass someone else userid,
1318 # he is not allowed to do that
1318 # he is not allowed to do that
1319 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1319 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1320 raise JSONRPCError('userid is not the same as your user')
1320 raise JSONRPCError('userid is not the same as your user')
1321
1321
1322 if isinstance(userid, Optional):
1322 if isinstance(userid, Optional):
1323 userid = apiuser.user_id
1323 userid = apiuser.user_id
1324
1324
1325 user = get_user_or_error(userid)
1325 user = get_user_or_error(userid)
1326
1326
1327 if isinstance(locked, Optional):
1327 if isinstance(locked, Optional):
1328 lockobj = repo.locked
1328 lockobj = repo.locked
1329
1329
1330 if lockobj[0] is None:
1330 if lockobj[0] is None:
1331 _d = {
1331 _d = {
1332 'repo': repo.repo_name,
1332 'repo': repo.repo_name,
1333 'locked': False,
1333 'locked': False,
1334 'locked_since': None,
1334 'locked_since': None,
1335 'locked_by': None,
1335 'locked_by': None,
1336 'lock_reason': None,
1336 'lock_reason': None,
1337 'lock_state_changed': False,
1337 'lock_state_changed': False,
1338 'msg': 'Repo `%s` not locked.' % repo.repo_name
1338 'msg': 'Repo `%s` not locked.' % repo.repo_name
1339 }
1339 }
1340 return _d
1340 return _d
1341 else:
1341 else:
1342 _user_id, _time, _reason = lockobj
1342 _user_id, _time, _reason = lockobj
1343 lock_user = get_user_or_error(userid)
1343 lock_user = get_user_or_error(userid)
1344 _d = {
1344 _d = {
1345 'repo': repo.repo_name,
1345 'repo': repo.repo_name,
1346 'locked': True,
1346 'locked': True,
1347 'locked_since': _time,
1347 'locked_since': _time,
1348 'locked_by': lock_user.username,
1348 'locked_by': lock_user.username,
1349 'lock_reason': _reason,
1349 'lock_reason': _reason,
1350 'lock_state_changed': False,
1350 'lock_state_changed': False,
1351 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1351 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1352 % (repo.repo_name, lock_user.username,
1352 % (repo.repo_name, lock_user.username,
1353 json.dumps(time_to_datetime(_time))))
1353 json.dumps(time_to_datetime(_time))))
1354 }
1354 }
1355 return _d
1355 return _d
1356
1356
1357 # force locked state through a flag
1357 # force locked state through a flag
1358 else:
1358 else:
1359 locked = str2bool(locked)
1359 locked = str2bool(locked)
1360 lock_reason = Repository.LOCK_API
1360 lock_reason = Repository.LOCK_API
1361 try:
1361 try:
1362 if locked:
1362 if locked:
1363 lock_time = time.time()
1363 lock_time = time.time()
1364 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1364 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1365 else:
1365 else:
1366 lock_time = None
1366 lock_time = None
1367 Repository.unlock(repo)
1367 Repository.unlock(repo)
1368 _d = {
1368 _d = {
1369 'repo': repo.repo_name,
1369 'repo': repo.repo_name,
1370 'locked': locked,
1370 'locked': locked,
1371 'locked_since': lock_time,
1371 'locked_since': lock_time,
1372 'locked_by': user.username,
1372 'locked_by': user.username,
1373 'lock_reason': lock_reason,
1373 'lock_reason': lock_reason,
1374 'lock_state_changed': True,
1374 'lock_state_changed': True,
1375 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1375 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1376 % (user.username, repo.repo_name, locked))
1376 % (user.username, repo.repo_name, locked))
1377 }
1377 }
1378 return _d
1378 return _d
1379 except Exception:
1379 except Exception:
1380 log.exception(
1380 log.exception(
1381 "Exception occurred while trying to lock repository")
1381 "Exception occurred while trying to lock repository")
1382 raise JSONRPCError(
1382 raise JSONRPCError(
1383 'Error occurred locking repository `%s`' % repo.repo_name
1383 'Error occurred locking repository `%s`' % repo.repo_name
1384 )
1384 )
1385
1385
1386
1386
1387 @jsonrpc_method()
1387 @jsonrpc_method()
1388 def comment_commit(
1388 def comment_commit(
1389 request, apiuser, repoid, commit_id, message, status=Optional(None),
1389 request, apiuser, repoid, commit_id, message, status=Optional(None),
1390 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1390 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1391 resolves_comment_id=Optional(None),
1391 resolves_comment_id=Optional(None),
1392 userid=Optional(OAttr('apiuser'))):
1392 userid=Optional(OAttr('apiuser'))):
1393 """
1393 """
1394 Set a commit comment, and optionally change the status of the commit.
1394 Set a commit comment, and optionally change the status of the commit.
1395
1395
1396 :param apiuser: This is filled automatically from the |authtoken|.
1396 :param apiuser: This is filled automatically from the |authtoken|.
1397 :type apiuser: AuthUser
1397 :type apiuser: AuthUser
1398 :param repoid: Set the repository name or repository ID.
1398 :param repoid: Set the repository name or repository ID.
1399 :type repoid: str or int
1399 :type repoid: str or int
1400 :param commit_id: Specify the commit_id for which to set a comment.
1400 :param commit_id: Specify the commit_id for which to set a comment.
1401 :type commit_id: str
1401 :type commit_id: str
1402 :param message: The comment text.
1402 :param message: The comment text.
1403 :type message: str
1403 :type message: str
1404 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1404 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1405 'approved', 'rejected', 'under_review'
1405 'approved', 'rejected', 'under_review'
1406 :type status: str
1406 :type status: str
1407 :param comment_type: Comment type, one of: 'note', 'todo'
1407 :param comment_type: Comment type, one of: 'note', 'todo'
1408 :type comment_type: Optional(str), default: 'note'
1408 :type comment_type: Optional(str), default: 'note'
1409 :param userid: Set the user name of the comment creator.
1409 :param userid: Set the user name of the comment creator.
1410 :type userid: Optional(str or int)
1410 :type userid: Optional(str or int)
1411
1411
1412 Example error output:
1412 Example error output:
1413
1413
1414 .. code-block:: bash
1414 .. code-block:: bash
1415
1415
1416 {
1416 {
1417 "id" : <id_given_in_input>,
1417 "id" : <id_given_in_input>,
1418 "result" : {
1418 "result" : {
1419 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1419 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1420 "status_change": null or <status>,
1420 "status_change": null or <status>,
1421 "success": true
1421 "success": true
1422 },
1422 },
1423 "error" : null
1423 "error" : null
1424 }
1424 }
1425
1425
1426 """
1426 """
1427 repo = get_repo_or_error(repoid)
1427 repo = get_repo_or_error(repoid)
1428 if not has_superadmin_permission(apiuser):
1428 if not has_superadmin_permission(apiuser):
1429 _perms = ('repository.read', 'repository.write', 'repository.admin')
1429 _perms = ('repository.read', 'repository.write', 'repository.admin')
1430 validate_repo_permissions(apiuser, repoid, repo, _perms)
1430 validate_repo_permissions(apiuser, repoid, repo, _perms)
1431
1431
1432 try:
1432 try:
1433 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1433 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1434 except Exception as e:
1434 except Exception as e:
1435 log.exception('Failed to fetch commit')
1435 log.exception('Failed to fetch commit')
1436 raise JSONRPCError(e.message)
1436 raise JSONRPCError(e.message)
1437
1437
1438 if isinstance(userid, Optional):
1438 if isinstance(userid, Optional):
1439 userid = apiuser.user_id
1439 userid = apiuser.user_id
1440
1440
1441 user = get_user_or_error(userid)
1441 user = get_user_or_error(userid)
1442 status = Optional.extract(status)
1442 status = Optional.extract(status)
1443 comment_type = Optional.extract(comment_type)
1443 comment_type = Optional.extract(comment_type)
1444 resolves_comment_id = Optional.extract(resolves_comment_id)
1444 resolves_comment_id = Optional.extract(resolves_comment_id)
1445
1445
1446 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1446 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1447 if status and status not in allowed_statuses:
1447 if status and status not in allowed_statuses:
1448 raise JSONRPCError('Bad status, must be on '
1448 raise JSONRPCError('Bad status, must be on '
1449 'of %s got %s' % (allowed_statuses, status,))
1449 'of %s got %s' % (allowed_statuses, status,))
1450
1450
1451 if resolves_comment_id:
1451 if resolves_comment_id:
1452 comment = ChangesetComment.get(resolves_comment_id)
1452 comment = ChangesetComment.get(resolves_comment_id)
1453 if not comment:
1453 if not comment:
1454 raise JSONRPCError(
1454 raise JSONRPCError(
1455 'Invalid resolves_comment_id `%s` for this commit.'
1455 'Invalid resolves_comment_id `%s` for this commit.'
1456 % resolves_comment_id)
1456 % resolves_comment_id)
1457 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1457 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1458 raise JSONRPCError(
1458 raise JSONRPCError(
1459 'Comment `%s` is wrong type for setting status to resolved.'
1459 'Comment `%s` is wrong type for setting status to resolved.'
1460 % resolves_comment_id)
1460 % resolves_comment_id)
1461
1461
1462 try:
1462 try:
1463 rc_config = SettingsModel().get_all_settings()
1463 rc_config = SettingsModel().get_all_settings()
1464 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1464 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1465 status_change_label = ChangesetStatus.get_status_lbl(status)
1465 status_change_label = ChangesetStatus.get_status_lbl(status)
1466 comment = CommentsModel().create(
1466 comment = CommentsModel().create(
1467 message, repo, user, commit_id=commit_id,
1467 message, repo, user, commit_id=commit_id,
1468 status_change=status_change_label,
1468 status_change=status_change_label,
1469 status_change_type=status,
1469 status_change_type=status,
1470 renderer=renderer,
1470 renderer=renderer,
1471 comment_type=comment_type,
1471 comment_type=comment_type,
1472 resolves_comment_id=resolves_comment_id
1472 resolves_comment_id=resolves_comment_id,
1473 auth_user=apiuser
1473 )
1474 )
1474 if status:
1475 if status:
1475 # also do a status change
1476 # also do a status change
1476 try:
1477 try:
1477 ChangesetStatusModel().set_status(
1478 ChangesetStatusModel().set_status(
1478 repo, status, user, comment, revision=commit_id,
1479 repo, status, user, comment, revision=commit_id,
1479 dont_allow_on_closed_pull_request=True
1480 dont_allow_on_closed_pull_request=True
1480 )
1481 )
1481 except StatusChangeOnClosedPullRequestError:
1482 except StatusChangeOnClosedPullRequestError:
1482 log.exception(
1483 log.exception(
1483 "Exception occurred while trying to change repo commit status")
1484 "Exception occurred while trying to change repo commit status")
1484 msg = ('Changing status on a changeset associated with '
1485 msg = ('Changing status on a changeset associated with '
1485 'a closed pull request is not allowed')
1486 'a closed pull request is not allowed')
1486 raise JSONRPCError(msg)
1487 raise JSONRPCError(msg)
1487
1488
1488 Session().commit()
1489 Session().commit()
1489 return {
1490 return {
1490 'msg': (
1491 'msg': (
1491 'Commented on commit `%s` for repository `%s`' % (
1492 'Commented on commit `%s` for repository `%s`' % (
1492 comment.revision, repo.repo_name)),
1493 comment.revision, repo.repo_name)),
1493 'status_change': status,
1494 'status_change': status,
1494 'success': True,
1495 'success': True,
1495 }
1496 }
1496 except JSONRPCError:
1497 except JSONRPCError:
1497 # catch any inside errors, and re-raise them to prevent from
1498 # catch any inside errors, and re-raise them to prevent from
1498 # below global catch to silence them
1499 # below global catch to silence them
1499 raise
1500 raise
1500 except Exception:
1501 except Exception:
1501 log.exception("Exception occurred while trying to comment on commit")
1502 log.exception("Exception occurred while trying to comment on commit")
1502 raise JSONRPCError(
1503 raise JSONRPCError(
1503 'failed to set comment on repository `%s`' % (repo.repo_name,)
1504 'failed to set comment on repository `%s`' % (repo.repo_name,)
1504 )
1505 )
1505
1506
1506
1507
1507 @jsonrpc_method()
1508 @jsonrpc_method()
1508 def grant_user_permission(request, apiuser, repoid, userid, perm):
1509 def grant_user_permission(request, apiuser, repoid, userid, perm):
1509 """
1510 """
1510 Grant permissions for the specified user on the given repository,
1511 Grant permissions for the specified user on the given repository,
1511 or update existing permissions if found.
1512 or update existing permissions if found.
1512
1513
1513 This command can only be run using an |authtoken| with admin
1514 This command can only be run using an |authtoken| with admin
1514 permissions on the |repo|.
1515 permissions on the |repo|.
1515
1516
1516 :param apiuser: This is filled automatically from the |authtoken|.
1517 :param apiuser: This is filled automatically from the |authtoken|.
1517 :type apiuser: AuthUser
1518 :type apiuser: AuthUser
1518 :param repoid: Set the repository name or repository ID.
1519 :param repoid: Set the repository name or repository ID.
1519 :type repoid: str or int
1520 :type repoid: str or int
1520 :param userid: Set the user name.
1521 :param userid: Set the user name.
1521 :type userid: str
1522 :type userid: str
1522 :param perm: Set the user permissions, using the following format
1523 :param perm: Set the user permissions, using the following format
1523 ``(repository.(none|read|write|admin))``
1524 ``(repository.(none|read|write|admin))``
1524 :type perm: str
1525 :type perm: str
1525
1526
1526 Example output:
1527 Example output:
1527
1528
1528 .. code-block:: bash
1529 .. code-block:: bash
1529
1530
1530 id : <id_given_in_input>
1531 id : <id_given_in_input>
1531 result: {
1532 result: {
1532 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1533 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1533 "success": true
1534 "success": true
1534 }
1535 }
1535 error: null
1536 error: null
1536 """
1537 """
1537
1538
1538 repo = get_repo_or_error(repoid)
1539 repo = get_repo_or_error(repoid)
1539 user = get_user_or_error(userid)
1540 user = get_user_or_error(userid)
1540 perm = get_perm_or_error(perm)
1541 perm = get_perm_or_error(perm)
1541 if not has_superadmin_permission(apiuser):
1542 if not has_superadmin_permission(apiuser):
1542 _perms = ('repository.admin',)
1543 _perms = ('repository.admin',)
1543 validate_repo_permissions(apiuser, repoid, repo, _perms)
1544 validate_repo_permissions(apiuser, repoid, repo, _perms)
1544
1545
1545 try:
1546 try:
1546
1547
1547 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1548 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1548
1549
1549 Session().commit()
1550 Session().commit()
1550 return {
1551 return {
1551 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1552 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1552 perm.permission_name, user.username, repo.repo_name
1553 perm.permission_name, user.username, repo.repo_name
1553 ),
1554 ),
1554 'success': True
1555 'success': True
1555 }
1556 }
1556 except Exception:
1557 except Exception:
1557 log.exception(
1558 log.exception(
1558 "Exception occurred while trying edit permissions for repo")
1559 "Exception occurred while trying edit permissions for repo")
1559 raise JSONRPCError(
1560 raise JSONRPCError(
1560 'failed to edit permission for user: `%s` in repo: `%s`' % (
1561 'failed to edit permission for user: `%s` in repo: `%s`' % (
1561 userid, repoid
1562 userid, repoid
1562 )
1563 )
1563 )
1564 )
1564
1565
1565
1566
1566 @jsonrpc_method()
1567 @jsonrpc_method()
1567 def revoke_user_permission(request, apiuser, repoid, userid):
1568 def revoke_user_permission(request, apiuser, repoid, userid):
1568 """
1569 """
1569 Revoke permission for a user on the specified repository.
1570 Revoke permission for a user on the specified repository.
1570
1571
1571 This command can only be run using an |authtoken| with admin
1572 This command can only be run using an |authtoken| with admin
1572 permissions on the |repo|.
1573 permissions on the |repo|.
1573
1574
1574 :param apiuser: This is filled automatically from the |authtoken|.
1575 :param apiuser: This is filled automatically from the |authtoken|.
1575 :type apiuser: AuthUser
1576 :type apiuser: AuthUser
1576 :param repoid: Set the repository name or repository ID.
1577 :param repoid: Set the repository name or repository ID.
1577 :type repoid: str or int
1578 :type repoid: str or int
1578 :param userid: Set the user name of revoked user.
1579 :param userid: Set the user name of revoked user.
1579 :type userid: str or int
1580 :type userid: str or int
1580
1581
1581 Example error output:
1582 Example error output:
1582
1583
1583 .. code-block:: bash
1584 .. code-block:: bash
1584
1585
1585 id : <id_given_in_input>
1586 id : <id_given_in_input>
1586 result: {
1587 result: {
1587 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1588 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1588 "success": true
1589 "success": true
1589 }
1590 }
1590 error: null
1591 error: null
1591 """
1592 """
1592
1593
1593 repo = get_repo_or_error(repoid)
1594 repo = get_repo_or_error(repoid)
1594 user = get_user_or_error(userid)
1595 user = get_user_or_error(userid)
1595 if not has_superadmin_permission(apiuser):
1596 if not has_superadmin_permission(apiuser):
1596 _perms = ('repository.admin',)
1597 _perms = ('repository.admin',)
1597 validate_repo_permissions(apiuser, repoid, repo, _perms)
1598 validate_repo_permissions(apiuser, repoid, repo, _perms)
1598
1599
1599 try:
1600 try:
1600 RepoModel().revoke_user_permission(repo=repo, user=user)
1601 RepoModel().revoke_user_permission(repo=repo, user=user)
1601 Session().commit()
1602 Session().commit()
1602 return {
1603 return {
1603 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1604 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1604 user.username, repo.repo_name
1605 user.username, repo.repo_name
1605 ),
1606 ),
1606 'success': True
1607 'success': True
1607 }
1608 }
1608 except Exception:
1609 except Exception:
1609 log.exception(
1610 log.exception(
1610 "Exception occurred while trying revoke permissions to repo")
1611 "Exception occurred while trying revoke permissions to repo")
1611 raise JSONRPCError(
1612 raise JSONRPCError(
1612 'failed to edit permission for user: `%s` in repo: `%s`' % (
1613 'failed to edit permission for user: `%s` in repo: `%s`' % (
1613 userid, repoid
1614 userid, repoid
1614 )
1615 )
1615 )
1616 )
1616
1617
1617
1618
1618 @jsonrpc_method()
1619 @jsonrpc_method()
1619 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1620 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1620 """
1621 """
1621 Grant permission for a user group on the specified repository,
1622 Grant permission for a user group on the specified repository,
1622 or update existing permissions.
1623 or update existing permissions.
1623
1624
1624 This command can only be run using an |authtoken| with admin
1625 This command can only be run using an |authtoken| with admin
1625 permissions on the |repo|.
1626 permissions on the |repo|.
1626
1627
1627 :param apiuser: This is filled automatically from the |authtoken|.
1628 :param apiuser: This is filled automatically from the |authtoken|.
1628 :type apiuser: AuthUser
1629 :type apiuser: AuthUser
1629 :param repoid: Set the repository name or repository ID.
1630 :param repoid: Set the repository name or repository ID.
1630 :type repoid: str or int
1631 :type repoid: str or int
1631 :param usergroupid: Specify the ID of the user group.
1632 :param usergroupid: Specify the ID of the user group.
1632 :type usergroupid: str or int
1633 :type usergroupid: str or int
1633 :param perm: Set the user group permissions using the following
1634 :param perm: Set the user group permissions using the following
1634 format: (repository.(none|read|write|admin))
1635 format: (repository.(none|read|write|admin))
1635 :type perm: str
1636 :type perm: str
1636
1637
1637 Example output:
1638 Example output:
1638
1639
1639 .. code-block:: bash
1640 .. code-block:: bash
1640
1641
1641 id : <id_given_in_input>
1642 id : <id_given_in_input>
1642 result : {
1643 result : {
1643 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1644 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1644 "success": true
1645 "success": true
1645
1646
1646 }
1647 }
1647 error : null
1648 error : null
1648
1649
1649 Example error output:
1650 Example error output:
1650
1651
1651 .. code-block:: bash
1652 .. code-block:: bash
1652
1653
1653 id : <id_given_in_input>
1654 id : <id_given_in_input>
1654 result : null
1655 result : null
1655 error : {
1656 error : {
1656 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1657 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1657 }
1658 }
1658
1659
1659 """
1660 """
1660
1661
1661 repo = get_repo_or_error(repoid)
1662 repo = get_repo_or_error(repoid)
1662 perm = get_perm_or_error(perm)
1663 perm = get_perm_or_error(perm)
1663 if not has_superadmin_permission(apiuser):
1664 if not has_superadmin_permission(apiuser):
1664 _perms = ('repository.admin',)
1665 _perms = ('repository.admin',)
1665 validate_repo_permissions(apiuser, repoid, repo, _perms)
1666 validate_repo_permissions(apiuser, repoid, repo, _perms)
1666
1667
1667 user_group = get_user_group_or_error(usergroupid)
1668 user_group = get_user_group_or_error(usergroupid)
1668 if not has_superadmin_permission(apiuser):
1669 if not has_superadmin_permission(apiuser):
1669 # check if we have at least read permission for this user group !
1670 # check if we have at least read permission for this user group !
1670 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1671 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1671 if not HasUserGroupPermissionAnyApi(*_perms)(
1672 if not HasUserGroupPermissionAnyApi(*_perms)(
1672 user=apiuser, user_group_name=user_group.users_group_name):
1673 user=apiuser, user_group_name=user_group.users_group_name):
1673 raise JSONRPCError(
1674 raise JSONRPCError(
1674 'user group `%s` does not exist' % (usergroupid,))
1675 'user group `%s` does not exist' % (usergroupid,))
1675
1676
1676 try:
1677 try:
1677 RepoModel().grant_user_group_permission(
1678 RepoModel().grant_user_group_permission(
1678 repo=repo, group_name=user_group, perm=perm)
1679 repo=repo, group_name=user_group, perm=perm)
1679
1680
1680 Session().commit()
1681 Session().commit()
1681 return {
1682 return {
1682 'msg': 'Granted perm: `%s` for user group: `%s` in '
1683 'msg': 'Granted perm: `%s` for user group: `%s` in '
1683 'repo: `%s`' % (
1684 'repo: `%s`' % (
1684 perm.permission_name, user_group.users_group_name,
1685 perm.permission_name, user_group.users_group_name,
1685 repo.repo_name
1686 repo.repo_name
1686 ),
1687 ),
1687 'success': True
1688 'success': True
1688 }
1689 }
1689 except Exception:
1690 except Exception:
1690 log.exception(
1691 log.exception(
1691 "Exception occurred while trying change permission on repo")
1692 "Exception occurred while trying change permission on repo")
1692 raise JSONRPCError(
1693 raise JSONRPCError(
1693 'failed to edit permission for user group: `%s` in '
1694 'failed to edit permission for user group: `%s` in '
1694 'repo: `%s`' % (
1695 'repo: `%s`' % (
1695 usergroupid, repo.repo_name
1696 usergroupid, repo.repo_name
1696 )
1697 )
1697 )
1698 )
1698
1699
1699
1700
1700 @jsonrpc_method()
1701 @jsonrpc_method()
1701 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1702 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1702 """
1703 """
1703 Revoke the permissions of a user group on a given repository.
1704 Revoke the permissions of a user group on a given repository.
1704
1705
1705 This command can only be run using an |authtoken| with admin
1706 This command can only be run using an |authtoken| with admin
1706 permissions on the |repo|.
1707 permissions on the |repo|.
1707
1708
1708 :param apiuser: This is filled automatically from the |authtoken|.
1709 :param apiuser: This is filled automatically from the |authtoken|.
1709 :type apiuser: AuthUser
1710 :type apiuser: AuthUser
1710 :param repoid: Set the repository name or repository ID.
1711 :param repoid: Set the repository name or repository ID.
1711 :type repoid: str or int
1712 :type repoid: str or int
1712 :param usergroupid: Specify the user group ID.
1713 :param usergroupid: Specify the user group ID.
1713 :type usergroupid: str or int
1714 :type usergroupid: str or int
1714
1715
1715 Example output:
1716 Example output:
1716
1717
1717 .. code-block:: bash
1718 .. code-block:: bash
1718
1719
1719 id : <id_given_in_input>
1720 id : <id_given_in_input>
1720 result: {
1721 result: {
1721 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1722 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1722 "success": true
1723 "success": true
1723 }
1724 }
1724 error: null
1725 error: null
1725 """
1726 """
1726
1727
1727 repo = get_repo_or_error(repoid)
1728 repo = get_repo_or_error(repoid)
1728 if not has_superadmin_permission(apiuser):
1729 if not has_superadmin_permission(apiuser):
1729 _perms = ('repository.admin',)
1730 _perms = ('repository.admin',)
1730 validate_repo_permissions(apiuser, repoid, repo, _perms)
1731 validate_repo_permissions(apiuser, repoid, repo, _perms)
1731
1732
1732 user_group = get_user_group_or_error(usergroupid)
1733 user_group = get_user_group_or_error(usergroupid)
1733 if not has_superadmin_permission(apiuser):
1734 if not has_superadmin_permission(apiuser):
1734 # check if we have at least read permission for this user group !
1735 # check if we have at least read permission for this user group !
1735 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1736 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1736 if not HasUserGroupPermissionAnyApi(*_perms)(
1737 if not HasUserGroupPermissionAnyApi(*_perms)(
1737 user=apiuser, user_group_name=user_group.users_group_name):
1738 user=apiuser, user_group_name=user_group.users_group_name):
1738 raise JSONRPCError(
1739 raise JSONRPCError(
1739 'user group `%s` does not exist' % (usergroupid,))
1740 'user group `%s` does not exist' % (usergroupid,))
1740
1741
1741 try:
1742 try:
1742 RepoModel().revoke_user_group_permission(
1743 RepoModel().revoke_user_group_permission(
1743 repo=repo, group_name=user_group)
1744 repo=repo, group_name=user_group)
1744
1745
1745 Session().commit()
1746 Session().commit()
1746 return {
1747 return {
1747 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1748 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1748 user_group.users_group_name, repo.repo_name
1749 user_group.users_group_name, repo.repo_name
1749 ),
1750 ),
1750 'success': True
1751 'success': True
1751 }
1752 }
1752 except Exception:
1753 except Exception:
1753 log.exception("Exception occurred while trying revoke "
1754 log.exception("Exception occurred while trying revoke "
1754 "user group permission on repo")
1755 "user group permission on repo")
1755 raise JSONRPCError(
1756 raise JSONRPCError(
1756 'failed to edit permission for user group: `%s` in '
1757 'failed to edit permission for user group: `%s` in '
1757 'repo: `%s`' % (
1758 'repo: `%s`' % (
1758 user_group.users_group_name, repo.repo_name
1759 user_group.users_group_name, repo.repo_name
1759 )
1760 )
1760 )
1761 )
1761
1762
1762
1763
1763 @jsonrpc_method()
1764 @jsonrpc_method()
1764 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
1765 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
1765 """
1766 """
1766 Triggers a pull on the given repository from a remote location. You
1767 Triggers a pull on the given repository from a remote location. You
1767 can use this to keep remote repositories up-to-date.
1768 can use this to keep remote repositories up-to-date.
1768
1769
1769 This command can only be run using an |authtoken| with admin
1770 This command can only be run using an |authtoken| with admin
1770 rights to the specified repository. For more information,
1771 rights to the specified repository. For more information,
1771 see :ref:`config-token-ref`.
1772 see :ref:`config-token-ref`.
1772
1773
1773 This command takes the following options:
1774 This command takes the following options:
1774
1775
1775 :param apiuser: This is filled automatically from the |authtoken|.
1776 :param apiuser: This is filled automatically from the |authtoken|.
1776 :type apiuser: AuthUser
1777 :type apiuser: AuthUser
1777 :param repoid: The repository name or repository ID.
1778 :param repoid: The repository name or repository ID.
1778 :type repoid: str or int
1779 :type repoid: str or int
1779 :param remote_uri: Optional remote URI to pass in for pull
1780 :param remote_uri: Optional remote URI to pass in for pull
1780 :type remote_uri: str
1781 :type remote_uri: str
1781
1782
1782 Example output:
1783 Example output:
1783
1784
1784 .. code-block:: bash
1785 .. code-block:: bash
1785
1786
1786 id : <id_given_in_input>
1787 id : <id_given_in_input>
1787 result : {
1788 result : {
1788 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
1789 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
1789 "repository": "<repository name>"
1790 "repository": "<repository name>"
1790 }
1791 }
1791 error : null
1792 error : null
1792
1793
1793 Example error output:
1794 Example error output:
1794
1795
1795 .. code-block:: bash
1796 .. code-block:: bash
1796
1797
1797 id : <id_given_in_input>
1798 id : <id_given_in_input>
1798 result : null
1799 result : null
1799 error : {
1800 error : {
1800 "Unable to push changes from `<remote_url>`"
1801 "Unable to push changes from `<remote_url>`"
1801 }
1802 }
1802
1803
1803 """
1804 """
1804
1805
1805 repo = get_repo_or_error(repoid)
1806 repo = get_repo_or_error(repoid)
1806 remote_uri = Optional.extract(remote_uri)
1807 remote_uri = Optional.extract(remote_uri)
1807 remote_uri_display = remote_uri or repo.clone_uri_hidden
1808 remote_uri_display = remote_uri or repo.clone_uri_hidden
1808 if not has_superadmin_permission(apiuser):
1809 if not has_superadmin_permission(apiuser):
1809 _perms = ('repository.admin',)
1810 _perms = ('repository.admin',)
1810 validate_repo_permissions(apiuser, repoid, repo, _perms)
1811 validate_repo_permissions(apiuser, repoid, repo, _perms)
1811
1812
1812 try:
1813 try:
1813 ScmModel().pull_changes(
1814 ScmModel().pull_changes(
1814 repo.repo_name, apiuser.username, remote_uri=remote_uri)
1815 repo.repo_name, apiuser.username, remote_uri=remote_uri)
1815 return {
1816 return {
1816 'msg': 'Pulled from url `%s` on repo `%s`' % (
1817 'msg': 'Pulled from url `%s` on repo `%s`' % (
1817 remote_uri_display, repo.repo_name),
1818 remote_uri_display, repo.repo_name),
1818 'repository': repo.repo_name
1819 'repository': repo.repo_name
1819 }
1820 }
1820 except Exception:
1821 except Exception:
1821 log.exception("Exception occurred while trying to "
1822 log.exception("Exception occurred while trying to "
1822 "pull changes from remote location")
1823 "pull changes from remote location")
1823 raise JSONRPCError(
1824 raise JSONRPCError(
1824 'Unable to pull changes from `%s`' % remote_uri_display
1825 'Unable to pull changes from `%s`' % remote_uri_display
1825 )
1826 )
1826
1827
1827
1828
1828 @jsonrpc_method()
1829 @jsonrpc_method()
1829 def strip(request, apiuser, repoid, revision, branch):
1830 def strip(request, apiuser, repoid, revision, branch):
1830 """
1831 """
1831 Strips the given revision from the specified repository.
1832 Strips the given revision from the specified repository.
1832
1833
1833 * This will remove the revision and all of its decendants.
1834 * This will remove the revision and all of its decendants.
1834
1835
1835 This command can only be run using an |authtoken| with admin rights to
1836 This command can only be run using an |authtoken| with admin rights to
1836 the specified repository.
1837 the specified repository.
1837
1838
1838 This command takes the following options:
1839 This command takes the following options:
1839
1840
1840 :param apiuser: This is filled automatically from the |authtoken|.
1841 :param apiuser: This is filled automatically from the |authtoken|.
1841 :type apiuser: AuthUser
1842 :type apiuser: AuthUser
1842 :param repoid: The repository name or repository ID.
1843 :param repoid: The repository name or repository ID.
1843 :type repoid: str or int
1844 :type repoid: str or int
1844 :param revision: The revision you wish to strip.
1845 :param revision: The revision you wish to strip.
1845 :type revision: str
1846 :type revision: str
1846 :param branch: The branch from which to strip the revision.
1847 :param branch: The branch from which to strip the revision.
1847 :type branch: str
1848 :type branch: str
1848
1849
1849 Example output:
1850 Example output:
1850
1851
1851 .. code-block:: bash
1852 .. code-block:: bash
1852
1853
1853 id : <id_given_in_input>
1854 id : <id_given_in_input>
1854 result : {
1855 result : {
1855 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1856 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1856 "repository": "<repository name>"
1857 "repository": "<repository name>"
1857 }
1858 }
1858 error : null
1859 error : null
1859
1860
1860 Example error output:
1861 Example error output:
1861
1862
1862 .. code-block:: bash
1863 .. code-block:: bash
1863
1864
1864 id : <id_given_in_input>
1865 id : <id_given_in_input>
1865 result : null
1866 result : null
1866 error : {
1867 error : {
1867 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1868 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1868 }
1869 }
1869
1870
1870 """
1871 """
1871
1872
1872 repo = get_repo_or_error(repoid)
1873 repo = get_repo_or_error(repoid)
1873 if not has_superadmin_permission(apiuser):
1874 if not has_superadmin_permission(apiuser):
1874 _perms = ('repository.admin',)
1875 _perms = ('repository.admin',)
1875 validate_repo_permissions(apiuser, repoid, repo, _perms)
1876 validate_repo_permissions(apiuser, repoid, repo, _perms)
1876
1877
1877 try:
1878 try:
1878 ScmModel().strip(repo, revision, branch)
1879 ScmModel().strip(repo, revision, branch)
1879 audit_logger.store_api(
1880 audit_logger.store_api(
1880 'repo.commit.strip', action_data={'commit_id': revision},
1881 'repo.commit.strip', action_data={'commit_id': revision},
1881 repo=repo,
1882 repo=repo,
1882 user=apiuser, commit=True)
1883 user=apiuser, commit=True)
1883
1884
1884 return {
1885 return {
1885 'msg': 'Stripped commit %s from repo `%s`' % (
1886 'msg': 'Stripped commit %s from repo `%s`' % (
1886 revision, repo.repo_name),
1887 revision, repo.repo_name),
1887 'repository': repo.repo_name
1888 'repository': repo.repo_name
1888 }
1889 }
1889 except Exception:
1890 except Exception:
1890 log.exception("Exception while trying to strip")
1891 log.exception("Exception while trying to strip")
1891 raise JSONRPCError(
1892 raise JSONRPCError(
1892 'Unable to strip commit %s from repo `%s`' % (
1893 'Unable to strip commit %s from repo `%s`' % (
1893 revision, repo.repo_name)
1894 revision, repo.repo_name)
1894 )
1895 )
1895
1896
1896
1897
1897 @jsonrpc_method()
1898 @jsonrpc_method()
1898 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1899 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1899 """
1900 """
1900 Returns all settings for a repository. If key is given it only returns the
1901 Returns all settings for a repository. If key is given it only returns the
1901 setting identified by the key or null.
1902 setting identified by the key or null.
1902
1903
1903 :param apiuser: This is filled automatically from the |authtoken|.
1904 :param apiuser: This is filled automatically from the |authtoken|.
1904 :type apiuser: AuthUser
1905 :type apiuser: AuthUser
1905 :param repoid: The repository name or repository id.
1906 :param repoid: The repository name or repository id.
1906 :type repoid: str or int
1907 :type repoid: str or int
1907 :param key: Key of the setting to return.
1908 :param key: Key of the setting to return.
1908 :type: key: Optional(str)
1909 :type: key: Optional(str)
1909
1910
1910 Example output:
1911 Example output:
1911
1912
1912 .. code-block:: bash
1913 .. code-block:: bash
1913
1914
1914 {
1915 {
1915 "error": null,
1916 "error": null,
1916 "id": 237,
1917 "id": 237,
1917 "result": {
1918 "result": {
1918 "extensions_largefiles": true,
1919 "extensions_largefiles": true,
1919 "extensions_evolve": true,
1920 "extensions_evolve": true,
1920 "hooks_changegroup_push_logger": true,
1921 "hooks_changegroup_push_logger": true,
1921 "hooks_changegroup_repo_size": false,
1922 "hooks_changegroup_repo_size": false,
1922 "hooks_outgoing_pull_logger": true,
1923 "hooks_outgoing_pull_logger": true,
1923 "phases_publish": "True",
1924 "phases_publish": "True",
1924 "rhodecode_hg_use_rebase_for_merging": true,
1925 "rhodecode_hg_use_rebase_for_merging": true,
1925 "rhodecode_pr_merge_enabled": true,
1926 "rhodecode_pr_merge_enabled": true,
1926 "rhodecode_use_outdated_comments": true
1927 "rhodecode_use_outdated_comments": true
1927 }
1928 }
1928 }
1929 }
1929 """
1930 """
1930
1931
1931 # Restrict access to this api method to admins only.
1932 # Restrict access to this api method to admins only.
1932 if not has_superadmin_permission(apiuser):
1933 if not has_superadmin_permission(apiuser):
1933 raise JSONRPCForbidden()
1934 raise JSONRPCForbidden()
1934
1935
1935 try:
1936 try:
1936 repo = get_repo_or_error(repoid)
1937 repo = get_repo_or_error(repoid)
1937 settings_model = VcsSettingsModel(repo=repo)
1938 settings_model = VcsSettingsModel(repo=repo)
1938 settings = settings_model.get_global_settings()
1939 settings = settings_model.get_global_settings()
1939 settings.update(settings_model.get_repo_settings())
1940 settings.update(settings_model.get_repo_settings())
1940
1941
1941 # If only a single setting is requested fetch it from all settings.
1942 # If only a single setting is requested fetch it from all settings.
1942 key = Optional.extract(key)
1943 key = Optional.extract(key)
1943 if key is not None:
1944 if key is not None:
1944 settings = settings.get(key, None)
1945 settings = settings.get(key, None)
1945 except Exception:
1946 except Exception:
1946 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1947 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1947 log.exception(msg)
1948 log.exception(msg)
1948 raise JSONRPCError(msg)
1949 raise JSONRPCError(msg)
1949
1950
1950 return settings
1951 return settings
1951
1952
1952
1953
1953 @jsonrpc_method()
1954 @jsonrpc_method()
1954 def set_repo_settings(request, apiuser, repoid, settings):
1955 def set_repo_settings(request, apiuser, repoid, settings):
1955 """
1956 """
1956 Update repository settings. Returns true on success.
1957 Update repository settings. Returns true on success.
1957
1958
1958 :param apiuser: This is filled automatically from the |authtoken|.
1959 :param apiuser: This is filled automatically from the |authtoken|.
1959 :type apiuser: AuthUser
1960 :type apiuser: AuthUser
1960 :param repoid: The repository name or repository id.
1961 :param repoid: The repository name or repository id.
1961 :type repoid: str or int
1962 :type repoid: str or int
1962 :param settings: The new settings for the repository.
1963 :param settings: The new settings for the repository.
1963 :type: settings: dict
1964 :type: settings: dict
1964
1965
1965 Example output:
1966 Example output:
1966
1967
1967 .. code-block:: bash
1968 .. code-block:: bash
1968
1969
1969 {
1970 {
1970 "error": null,
1971 "error": null,
1971 "id": 237,
1972 "id": 237,
1972 "result": true
1973 "result": true
1973 }
1974 }
1974 """
1975 """
1975 # Restrict access to this api method to admins only.
1976 # Restrict access to this api method to admins only.
1976 if not has_superadmin_permission(apiuser):
1977 if not has_superadmin_permission(apiuser):
1977 raise JSONRPCForbidden()
1978 raise JSONRPCForbidden()
1978
1979
1979 if type(settings) is not dict:
1980 if type(settings) is not dict:
1980 raise JSONRPCError('Settings have to be a JSON Object.')
1981 raise JSONRPCError('Settings have to be a JSON Object.')
1981
1982
1982 try:
1983 try:
1983 settings_model = VcsSettingsModel(repo=repoid)
1984 settings_model = VcsSettingsModel(repo=repoid)
1984
1985
1985 # Merge global, repo and incoming settings.
1986 # Merge global, repo and incoming settings.
1986 new_settings = settings_model.get_global_settings()
1987 new_settings = settings_model.get_global_settings()
1987 new_settings.update(settings_model.get_repo_settings())
1988 new_settings.update(settings_model.get_repo_settings())
1988 new_settings.update(settings)
1989 new_settings.update(settings)
1989
1990
1990 # Update the settings.
1991 # Update the settings.
1991 inherit_global_settings = new_settings.get(
1992 inherit_global_settings = new_settings.get(
1992 'inherit_global_settings', False)
1993 'inherit_global_settings', False)
1993 settings_model.create_or_update_repo_settings(
1994 settings_model.create_or_update_repo_settings(
1994 new_settings, inherit_global_settings=inherit_global_settings)
1995 new_settings, inherit_global_settings=inherit_global_settings)
1995 Session().commit()
1996 Session().commit()
1996 except Exception:
1997 except Exception:
1997 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1998 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1998 log.exception(msg)
1999 log.exception(msg)
1999 raise JSONRPCError(msg)
2000 raise JSONRPCError(msg)
2000
2001
2001 # Indicate success.
2002 # Indicate success.
2002 return True
2003 return True
2003
2004
2004
2005
2005 @jsonrpc_method()
2006 @jsonrpc_method()
2006 def maintenance(request, apiuser, repoid):
2007 def maintenance(request, apiuser, repoid):
2007 """
2008 """
2008 Triggers a maintenance on the given repository.
2009 Triggers a maintenance on the given repository.
2009
2010
2010 This command can only be run using an |authtoken| with admin
2011 This command can only be run using an |authtoken| with admin
2011 rights to the specified repository. For more information,
2012 rights to the specified repository. For more information,
2012 see :ref:`config-token-ref`.
2013 see :ref:`config-token-ref`.
2013
2014
2014 This command takes the following options:
2015 This command takes the following options:
2015
2016
2016 :param apiuser: This is filled automatically from the |authtoken|.
2017 :param apiuser: This is filled automatically from the |authtoken|.
2017 :type apiuser: AuthUser
2018 :type apiuser: AuthUser
2018 :param repoid: The repository name or repository ID.
2019 :param repoid: The repository name or repository ID.
2019 :type repoid: str or int
2020 :type repoid: str or int
2020
2021
2021 Example output:
2022 Example output:
2022
2023
2023 .. code-block:: bash
2024 .. code-block:: bash
2024
2025
2025 id : <id_given_in_input>
2026 id : <id_given_in_input>
2026 result : {
2027 result : {
2027 "msg": "executed maintenance command",
2028 "msg": "executed maintenance command",
2028 "executed_actions": [
2029 "executed_actions": [
2029 <action_message>, <action_message2>...
2030 <action_message>, <action_message2>...
2030 ],
2031 ],
2031 "repository": "<repository name>"
2032 "repository": "<repository name>"
2032 }
2033 }
2033 error : null
2034 error : null
2034
2035
2035 Example error output:
2036 Example error output:
2036
2037
2037 .. code-block:: bash
2038 .. code-block:: bash
2038
2039
2039 id : <id_given_in_input>
2040 id : <id_given_in_input>
2040 result : null
2041 result : null
2041 error : {
2042 error : {
2042 "Unable to execute maintenance on `<reponame>`"
2043 "Unable to execute maintenance on `<reponame>`"
2043 }
2044 }
2044
2045
2045 """
2046 """
2046
2047
2047 repo = get_repo_or_error(repoid)
2048 repo = get_repo_or_error(repoid)
2048 if not has_superadmin_permission(apiuser):
2049 if not has_superadmin_permission(apiuser):
2049 _perms = ('repository.admin',)
2050 _perms = ('repository.admin',)
2050 validate_repo_permissions(apiuser, repoid, repo, _perms)
2051 validate_repo_permissions(apiuser, repoid, repo, _perms)
2051
2052
2052 try:
2053 try:
2053 maintenance = repo_maintenance.RepoMaintenance()
2054 maintenance = repo_maintenance.RepoMaintenance()
2054 executed_actions = maintenance.execute(repo)
2055 executed_actions = maintenance.execute(repo)
2055
2056
2056 return {
2057 return {
2057 'msg': 'executed maintenance command',
2058 'msg': 'executed maintenance command',
2058 'executed_actions': executed_actions,
2059 'executed_actions': executed_actions,
2059 'repository': repo.repo_name
2060 'repository': repo.repo_name
2060 }
2061 }
2061 except Exception:
2062 except Exception:
2062 log.exception("Exception occurred while trying to run maintenance")
2063 log.exception("Exception occurred while trying to run maintenance")
2063 raise JSONRPCError(
2064 raise JSONRPCError(
2064 'Unable to execute maintenance on `%s`' % repo.repo_name)
2065 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,589 +1,590 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
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
31
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35
35
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.diffs import cache_diff, load_cached_diff, diff_cache_exist
37 from rhodecode.lib.diffs import cache_diff, load_cached_diff, diff_cache_exist
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 import rhodecode.lib.helpers as h
39 import rhodecode.lib.helpers as h
40 from rhodecode.lib.utils2 import safe_unicode, str2bool
40 from rhodecode.lib.utils2 import safe_unicode, str2bool
41 from rhodecode.lib.vcs.backends.base import EmptyCommit
41 from rhodecode.lib.vcs.backends.base import EmptyCommit
42 from rhodecode.lib.vcs.exceptions import (
42 from rhodecode.lib.vcs.exceptions import (
43 RepositoryError, CommitDoesNotExistError)
43 RepositoryError, CommitDoesNotExistError)
44 from rhodecode.model.db import ChangesetComment, ChangesetStatus
44 from rhodecode.model.db import ChangesetComment, ChangesetStatus
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.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.settings import VcsSettingsModel
48 from rhodecode.model.settings import VcsSettingsModel
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 def _update_with_GET(params, request):
53 def _update_with_GET(params, request):
54 for k in ['diff1', 'diff2', 'diff']:
54 for k in ['diff1', 'diff2', 'diff']:
55 params[k] += request.GET.getall(k)
55 params[k] += request.GET.getall(k)
56
56
57
57
58 def get_ignore_ws(fid, request):
58 def get_ignore_ws(fid, request):
59 ig_ws_global = request.GET.get('ignorews')
59 ig_ws_global = request.GET.get('ignorews')
60 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
60 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
61 if ig_ws:
61 if ig_ws:
62 try:
62 try:
63 return int(ig_ws[0].split(':')[-1])
63 return int(ig_ws[0].split(':')[-1])
64 except Exception:
64 except Exception:
65 pass
65 pass
66 return ig_ws_global
66 return ig_ws_global
67
67
68
68
69 def _ignorews_url(request, fileid=None):
69 def _ignorews_url(request, fileid=None):
70 _ = request.translate
70 _ = request.translate
71 fileid = str(fileid) if fileid else None
71 fileid = str(fileid) if fileid else None
72 params = collections.defaultdict(list)
72 params = collections.defaultdict(list)
73 _update_with_GET(params, request)
73 _update_with_GET(params, request)
74 label = _('Show whitespace')
74 label = _('Show whitespace')
75 tooltiplbl = _('Show whitespace for all diffs')
75 tooltiplbl = _('Show whitespace for all diffs')
76 ig_ws = get_ignore_ws(fileid, request)
76 ig_ws = get_ignore_ws(fileid, request)
77 ln_ctx = get_line_ctx(fileid, request)
77 ln_ctx = get_line_ctx(fileid, request)
78
78
79 if ig_ws is None:
79 if ig_ws is None:
80 params['ignorews'] += [1]
80 params['ignorews'] += [1]
81 label = _('Ignore whitespace')
81 label = _('Ignore whitespace')
82 tooltiplbl = _('Ignore whitespace for all diffs')
82 tooltiplbl = _('Ignore whitespace for all diffs')
83 ctx_key = 'context'
83 ctx_key = 'context'
84 ctx_val = ln_ctx
84 ctx_val = ln_ctx
85
85
86 # if we have passed in ln_ctx pass it along to our params
86 # if we have passed in ln_ctx pass it along to our params
87 if ln_ctx:
87 if ln_ctx:
88 params[ctx_key] += [ctx_val]
88 params[ctx_key] += [ctx_val]
89
89
90 if fileid:
90 if fileid:
91 params['anchor'] = 'a_' + fileid
91 params['anchor'] = 'a_' + fileid
92 return h.link_to(label, request.current_route_path(_query=params),
92 return h.link_to(label, request.current_route_path(_query=params),
93 title=tooltiplbl, class_='tooltip')
93 title=tooltiplbl, class_='tooltip')
94
94
95
95
96 def get_line_ctx(fid, request):
96 def get_line_ctx(fid, request):
97 ln_ctx_global = request.GET.get('context')
97 ln_ctx_global = request.GET.get('context')
98 if fid:
98 if fid:
99 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
99 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
100 else:
100 else:
101 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
101 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
102 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
102 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
103 if ln_ctx:
103 if ln_ctx:
104 ln_ctx = [ln_ctx]
104 ln_ctx = [ln_ctx]
105
105
106 if ln_ctx:
106 if ln_ctx:
107 retval = ln_ctx[0].split(':')[-1]
107 retval = ln_ctx[0].split(':')[-1]
108 else:
108 else:
109 retval = ln_ctx_global
109 retval = ln_ctx_global
110
110
111 try:
111 try:
112 return int(retval)
112 return int(retval)
113 except Exception:
113 except Exception:
114 return 3
114 return 3
115
115
116
116
117 def _context_url(request, fileid=None):
117 def _context_url(request, fileid=None):
118 """
118 """
119 Generates a url for context lines.
119 Generates a url for context lines.
120
120
121 :param fileid:
121 :param fileid:
122 """
122 """
123
123
124 _ = request.translate
124 _ = request.translate
125 fileid = str(fileid) if fileid else None
125 fileid = str(fileid) if fileid else None
126 ig_ws = get_ignore_ws(fileid, request)
126 ig_ws = get_ignore_ws(fileid, request)
127 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
127 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
128
128
129 params = collections.defaultdict(list)
129 params = collections.defaultdict(list)
130 _update_with_GET(params, request)
130 _update_with_GET(params, request)
131
131
132 if ln_ctx > 0:
132 if ln_ctx > 0:
133 params['context'] += [ln_ctx]
133 params['context'] += [ln_ctx]
134
134
135 if ig_ws:
135 if ig_ws:
136 ig_ws_key = 'ignorews'
136 ig_ws_key = 'ignorews'
137 ig_ws_val = 1
137 ig_ws_val = 1
138 params[ig_ws_key] += [ig_ws_val]
138 params[ig_ws_key] += [ig_ws_val]
139
139
140 lbl = _('Increase context')
140 lbl = _('Increase context')
141 tooltiplbl = _('Increase context for all diffs')
141 tooltiplbl = _('Increase context for all diffs')
142
142
143 if fileid:
143 if fileid:
144 params['anchor'] = 'a_' + fileid
144 params['anchor'] = 'a_' + fileid
145 return h.link_to(lbl, request.current_route_path(_query=params),
145 return h.link_to(lbl, request.current_route_path(_query=params),
146 title=tooltiplbl, class_='tooltip')
146 title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class RepoCommitsView(RepoAppView):
149 class RepoCommitsView(RepoAppView):
150 def load_default_context(self):
150 def load_default_context(self):
151 c = self._get_local_tmpl_context(include_app_defaults=True)
151 c = self._get_local_tmpl_context(include_app_defaults=True)
152 c.rhodecode_repo = self.rhodecode_vcs_repo
152 c.rhodecode_repo = self.rhodecode_vcs_repo
153
153
154 return c
154 return c
155
155
156 def _is_diff_cache_enabled(self, target_repo):
156 def _is_diff_cache_enabled(self, target_repo):
157 caching_enabled = self._get_general_setting(
157 caching_enabled = self._get_general_setting(
158 target_repo, 'rhodecode_diff_cache')
158 target_repo, 'rhodecode_diff_cache')
159 log.debug('Diff caching enabled: %s', caching_enabled)
159 log.debug('Diff caching enabled: %s', caching_enabled)
160 return caching_enabled
160 return caching_enabled
161
161
162 def _commit(self, commit_id_range, method):
162 def _commit(self, commit_id_range, method):
163 _ = self.request.translate
163 _ = self.request.translate
164 c = self.load_default_context()
164 c = self.load_default_context()
165 c.ignorews_url = _ignorews_url
165 c.ignorews_url = _ignorews_url
166 c.context_url = _context_url
166 c.context_url = _context_url
167 c.fulldiff = self.request.GET.get('fulldiff')
167 c.fulldiff = self.request.GET.get('fulldiff')
168
168
169 # fetch global flags of ignore ws or context lines
169 # fetch global flags of ignore ws or context lines
170 context_lcl = get_line_ctx('', self.request)
170 context_lcl = get_line_ctx('', self.request)
171 ign_whitespace_lcl = get_ignore_ws('', self.request)
171 ign_whitespace_lcl = get_ignore_ws('', self.request)
172
172
173 # diff_limit will cut off the whole diff if the limit is applied
173 # diff_limit will cut off the whole diff if the limit is applied
174 # otherwise it will just hide the big files from the front-end
174 # otherwise it will just hide the big files from the front-end
175 diff_limit = c.visual.cut_off_limit_diff
175 diff_limit = c.visual.cut_off_limit_diff
176 file_limit = c.visual.cut_off_limit_file
176 file_limit = c.visual.cut_off_limit_file
177
177
178 # get ranges of commit ids if preset
178 # get ranges of commit ids if preset
179 commit_range = commit_id_range.split('...')[:2]
179 commit_range = commit_id_range.split('...')[:2]
180
180
181 try:
181 try:
182 pre_load = ['affected_files', 'author', 'branch', 'date',
182 pre_load = ['affected_files', 'author', 'branch', 'date',
183 'message', 'parents']
183 'message', 'parents']
184
184
185 if len(commit_range) == 2:
185 if len(commit_range) == 2:
186 commits = self.rhodecode_vcs_repo.get_commits(
186 commits = self.rhodecode_vcs_repo.get_commits(
187 start_id=commit_range[0], end_id=commit_range[1],
187 start_id=commit_range[0], end_id=commit_range[1],
188 pre_load=pre_load)
188 pre_load=pre_load)
189 commits = list(commits)
189 commits = list(commits)
190 else:
190 else:
191 commits = [self.rhodecode_vcs_repo.get_commit(
191 commits = [self.rhodecode_vcs_repo.get_commit(
192 commit_id=commit_id_range, pre_load=pre_load)]
192 commit_id=commit_id_range, pre_load=pre_load)]
193
193
194 c.commit_ranges = commits
194 c.commit_ranges = commits
195 if not c.commit_ranges:
195 if not c.commit_ranges:
196 raise RepositoryError(
196 raise RepositoryError(
197 'The commit range returned an empty result')
197 'The commit range returned an empty result')
198 except CommitDoesNotExistError:
198 except CommitDoesNotExistError:
199 msg = _('No such commit exists for this repository')
199 msg = _('No such commit exists for this repository')
200 h.flash(msg, category='error')
200 h.flash(msg, category='error')
201 raise HTTPNotFound()
201 raise HTTPNotFound()
202 except Exception:
202 except Exception:
203 log.exception("General failure")
203 log.exception("General failure")
204 raise HTTPNotFound()
204 raise HTTPNotFound()
205
205
206 c.changes = OrderedDict()
206 c.changes = OrderedDict()
207 c.lines_added = 0
207 c.lines_added = 0
208 c.lines_deleted = 0
208 c.lines_deleted = 0
209
209
210 # auto collapse if we have more than limit
210 # auto collapse if we have more than limit
211 collapse_limit = diffs.DiffProcessor._collapse_commits_over
211 collapse_limit = diffs.DiffProcessor._collapse_commits_over
212 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
212 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
213
213
214 c.commit_statuses = ChangesetStatus.STATUSES
214 c.commit_statuses = ChangesetStatus.STATUSES
215 c.inline_comments = []
215 c.inline_comments = []
216 c.files = []
216 c.files = []
217
217
218 c.statuses = []
218 c.statuses = []
219 c.comments = []
219 c.comments = []
220 c.unresolved_comments = []
220 c.unresolved_comments = []
221 if len(c.commit_ranges) == 1:
221 if len(c.commit_ranges) == 1:
222 commit = c.commit_ranges[0]
222 commit = c.commit_ranges[0]
223 c.comments = CommentsModel().get_comments(
223 c.comments = CommentsModel().get_comments(
224 self.db_repo.repo_id,
224 self.db_repo.repo_id,
225 revision=commit.raw_id)
225 revision=commit.raw_id)
226 c.statuses.append(ChangesetStatusModel().get_status(
226 c.statuses.append(ChangesetStatusModel().get_status(
227 self.db_repo.repo_id, commit.raw_id))
227 self.db_repo.repo_id, commit.raw_id))
228 # comments from PR
228 # comments from PR
229 statuses = ChangesetStatusModel().get_statuses(
229 statuses = ChangesetStatusModel().get_statuses(
230 self.db_repo.repo_id, commit.raw_id,
230 self.db_repo.repo_id, commit.raw_id,
231 with_revisions=True)
231 with_revisions=True)
232 prs = set(st.pull_request for st in statuses
232 prs = set(st.pull_request for st in statuses
233 if st.pull_request is not None)
233 if st.pull_request is not None)
234 # from associated statuses, check the pull requests, and
234 # from associated statuses, check the pull requests, and
235 # show comments from them
235 # show comments from them
236 for pr in prs:
236 for pr in prs:
237 c.comments.extend(pr.comments)
237 c.comments.extend(pr.comments)
238
238
239 c.unresolved_comments = CommentsModel()\
239 c.unresolved_comments = CommentsModel()\
240 .get_commit_unresolved_todos(commit.raw_id)
240 .get_commit_unresolved_todos(commit.raw_id)
241
241
242 diff = None
242 diff = None
243 # Iterate over ranges (default commit view is always one commit)
243 # Iterate over ranges (default commit view is always one commit)
244 for commit in c.commit_ranges:
244 for commit in c.commit_ranges:
245 c.changes[commit.raw_id] = []
245 c.changes[commit.raw_id] = []
246
246
247 commit2 = commit
247 commit2 = commit
248 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
248 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
249
249
250 if method == 'show':
250 if method == 'show':
251 inline_comments = CommentsModel().get_inline_comments(
251 inline_comments = CommentsModel().get_inline_comments(
252 self.db_repo.repo_id, revision=commit.raw_id)
252 self.db_repo.repo_id, revision=commit.raw_id)
253 c.inline_cnt = CommentsModel().get_inline_comments_count(
253 c.inline_cnt = CommentsModel().get_inline_comments_count(
254 inline_comments)
254 inline_comments)
255 c.inline_comments = inline_comments
255 c.inline_comments = inline_comments
256
256
257 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
257 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
258 self.db_repo)
258 self.db_repo)
259 cache_file_path = diff_cache_exist(
259 cache_file_path = diff_cache_exist(
260 cache_path, 'diff', commit.raw_id,
260 cache_path, 'diff', commit.raw_id,
261 ign_whitespace_lcl, context_lcl, c.fulldiff)
261 ign_whitespace_lcl, context_lcl, c.fulldiff)
262
262
263 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
263 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
264 force_recache = str2bool(self.request.GET.get('force_recache'))
264 force_recache = str2bool(self.request.GET.get('force_recache'))
265
265
266 cached_diff = None
266 cached_diff = None
267 if caching_enabled:
267 if caching_enabled:
268 cached_diff = load_cached_diff(cache_file_path)
268 cached_diff = load_cached_diff(cache_file_path)
269
269
270 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
270 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
271 if not force_recache and has_proper_diff_cache:
271 if not force_recache and has_proper_diff_cache:
272 diffset = cached_diff['diff']
272 diffset = cached_diff['diff']
273 else:
273 else:
274 vcs_diff = self.rhodecode_vcs_repo.get_diff(
274 vcs_diff = self.rhodecode_vcs_repo.get_diff(
275 commit1, commit2,
275 commit1, commit2,
276 ignore_whitespace=ign_whitespace_lcl,
276 ignore_whitespace=ign_whitespace_lcl,
277 context=context_lcl)
277 context=context_lcl)
278
278
279 diff_processor = diffs.DiffProcessor(
279 diff_processor = diffs.DiffProcessor(
280 vcs_diff, format='newdiff', diff_limit=diff_limit,
280 vcs_diff, format='newdiff', diff_limit=diff_limit,
281 file_limit=file_limit, show_full_diff=c.fulldiff)
281 file_limit=file_limit, show_full_diff=c.fulldiff)
282
282
283 _parsed = diff_processor.prepare()
283 _parsed = diff_processor.prepare()
284
284
285 diffset = codeblocks.DiffSet(
285 diffset = codeblocks.DiffSet(
286 repo_name=self.db_repo_name,
286 repo_name=self.db_repo_name,
287 source_node_getter=codeblocks.diffset_node_getter(commit1),
287 source_node_getter=codeblocks.diffset_node_getter(commit1),
288 target_node_getter=codeblocks.diffset_node_getter(commit2))
288 target_node_getter=codeblocks.diffset_node_getter(commit2))
289
289
290 diffset = self.path_filter.render_patchset_filtered(
290 diffset = self.path_filter.render_patchset_filtered(
291 diffset, _parsed, commit1.raw_id, commit2.raw_id)
291 diffset, _parsed, commit1.raw_id, commit2.raw_id)
292
292
293 # save cached diff
293 # save cached diff
294 if caching_enabled:
294 if caching_enabled:
295 cache_diff(cache_file_path, diffset, None)
295 cache_diff(cache_file_path, diffset, None)
296
296
297 c.limited_diff = diffset.limited_diff
297 c.limited_diff = diffset.limited_diff
298 c.changes[commit.raw_id] = diffset
298 c.changes[commit.raw_id] = diffset
299 else:
299 else:
300 # TODO(marcink): no cache usage here...
300 # TODO(marcink): no cache usage here...
301 _diff = self.rhodecode_vcs_repo.get_diff(
301 _diff = self.rhodecode_vcs_repo.get_diff(
302 commit1, commit2,
302 commit1, commit2,
303 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
303 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
304 diff_processor = diffs.DiffProcessor(
304 diff_processor = diffs.DiffProcessor(
305 _diff, format='newdiff', diff_limit=diff_limit,
305 _diff, format='newdiff', diff_limit=diff_limit,
306 file_limit=file_limit, show_full_diff=c.fulldiff)
306 file_limit=file_limit, show_full_diff=c.fulldiff)
307 # downloads/raw we only need RAW diff nothing else
307 # downloads/raw we only need RAW diff nothing else
308 diff = self.path_filter.get_raw_patch(diff_processor)
308 diff = self.path_filter.get_raw_patch(diff_processor)
309 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
309 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
310
310
311 # sort comments by how they were generated
311 # sort comments by how they were generated
312 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
312 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
313
313
314 if len(c.commit_ranges) == 1:
314 if len(c.commit_ranges) == 1:
315 c.commit = c.commit_ranges[0]
315 c.commit = c.commit_ranges[0]
316 c.parent_tmpl = ''.join(
316 c.parent_tmpl = ''.join(
317 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
317 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
318
318
319 if method == 'download':
319 if method == 'download':
320 response = Response(diff)
320 response = Response(diff)
321 response.content_type = 'text/plain'
321 response.content_type = 'text/plain'
322 response.content_disposition = (
322 response.content_disposition = (
323 'attachment; filename=%s.diff' % commit_id_range[:12])
323 'attachment; filename=%s.diff' % commit_id_range[:12])
324 return response
324 return response
325 elif method == 'patch':
325 elif method == 'patch':
326 c.diff = safe_unicode(diff)
326 c.diff = safe_unicode(diff)
327 patch = render(
327 patch = render(
328 'rhodecode:templates/changeset/patch_changeset.mako',
328 'rhodecode:templates/changeset/patch_changeset.mako',
329 self._get_template_context(c), self.request)
329 self._get_template_context(c), self.request)
330 response = Response(patch)
330 response = Response(patch)
331 response.content_type = 'text/plain'
331 response.content_type = 'text/plain'
332 return response
332 return response
333 elif method == 'raw':
333 elif method == 'raw':
334 response = Response(diff)
334 response = Response(diff)
335 response.content_type = 'text/plain'
335 response.content_type = 'text/plain'
336 return response
336 return response
337 elif method == 'show':
337 elif method == 'show':
338 if len(c.commit_ranges) == 1:
338 if len(c.commit_ranges) == 1:
339 html = render(
339 html = render(
340 'rhodecode:templates/changeset/changeset.mako',
340 'rhodecode:templates/changeset/changeset.mako',
341 self._get_template_context(c), self.request)
341 self._get_template_context(c), self.request)
342 return Response(html)
342 return Response(html)
343 else:
343 else:
344 c.ancestor = None
344 c.ancestor = None
345 c.target_repo = self.db_repo
345 c.target_repo = self.db_repo
346 html = render(
346 html = render(
347 'rhodecode:templates/changeset/changeset_range.mako',
347 'rhodecode:templates/changeset/changeset_range.mako',
348 self._get_template_context(c), self.request)
348 self._get_template_context(c), self.request)
349 return Response(html)
349 return Response(html)
350
350
351 raise HTTPBadRequest()
351 raise HTTPBadRequest()
352
352
353 @LoginRequired()
353 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
354 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
355 'repository.read', 'repository.write', 'repository.admin')
356 @view_config(
356 @view_config(
357 route_name='repo_commit', request_method='GET',
357 route_name='repo_commit', request_method='GET',
358 renderer=None)
358 renderer=None)
359 def repo_commit_show(self):
359 def repo_commit_show(self):
360 commit_id = self.request.matchdict['commit_id']
360 commit_id = self.request.matchdict['commit_id']
361 return self._commit(commit_id, method='show')
361 return self._commit(commit_id, method='show')
362
362
363 @LoginRequired()
363 @LoginRequired()
364 @HasRepoPermissionAnyDecorator(
364 @HasRepoPermissionAnyDecorator(
365 'repository.read', 'repository.write', 'repository.admin')
365 'repository.read', 'repository.write', 'repository.admin')
366 @view_config(
366 @view_config(
367 route_name='repo_commit_raw', request_method='GET',
367 route_name='repo_commit_raw', request_method='GET',
368 renderer=None)
368 renderer=None)
369 @view_config(
369 @view_config(
370 route_name='repo_commit_raw_deprecated', request_method='GET',
370 route_name='repo_commit_raw_deprecated', request_method='GET',
371 renderer=None)
371 renderer=None)
372 def repo_commit_raw(self):
372 def repo_commit_raw(self):
373 commit_id = self.request.matchdict['commit_id']
373 commit_id = self.request.matchdict['commit_id']
374 return self._commit(commit_id, method='raw')
374 return self._commit(commit_id, method='raw')
375
375
376 @LoginRequired()
376 @LoginRequired()
377 @HasRepoPermissionAnyDecorator(
377 @HasRepoPermissionAnyDecorator(
378 'repository.read', 'repository.write', 'repository.admin')
378 'repository.read', 'repository.write', 'repository.admin')
379 @view_config(
379 @view_config(
380 route_name='repo_commit_patch', request_method='GET',
380 route_name='repo_commit_patch', request_method='GET',
381 renderer=None)
381 renderer=None)
382 def repo_commit_patch(self):
382 def repo_commit_patch(self):
383 commit_id = self.request.matchdict['commit_id']
383 commit_id = self.request.matchdict['commit_id']
384 return self._commit(commit_id, method='patch')
384 return self._commit(commit_id, method='patch')
385
385
386 @LoginRequired()
386 @LoginRequired()
387 @HasRepoPermissionAnyDecorator(
387 @HasRepoPermissionAnyDecorator(
388 'repository.read', 'repository.write', 'repository.admin')
388 'repository.read', 'repository.write', 'repository.admin')
389 @view_config(
389 @view_config(
390 route_name='repo_commit_download', request_method='GET',
390 route_name='repo_commit_download', request_method='GET',
391 renderer=None)
391 renderer=None)
392 def repo_commit_download(self):
392 def repo_commit_download(self):
393 commit_id = self.request.matchdict['commit_id']
393 commit_id = self.request.matchdict['commit_id']
394 return self._commit(commit_id, method='download')
394 return self._commit(commit_id, method='download')
395
395
396 @LoginRequired()
396 @LoginRequired()
397 @NotAnonymous()
397 @NotAnonymous()
398 @HasRepoPermissionAnyDecorator(
398 @HasRepoPermissionAnyDecorator(
399 'repository.read', 'repository.write', 'repository.admin')
399 'repository.read', 'repository.write', 'repository.admin')
400 @CSRFRequired()
400 @CSRFRequired()
401 @view_config(
401 @view_config(
402 route_name='repo_commit_comment_create', request_method='POST',
402 route_name='repo_commit_comment_create', request_method='POST',
403 renderer='json_ext')
403 renderer='json_ext')
404 def repo_commit_comment_create(self):
404 def repo_commit_comment_create(self):
405 _ = self.request.translate
405 _ = self.request.translate
406 commit_id = self.request.matchdict['commit_id']
406 commit_id = self.request.matchdict['commit_id']
407
407
408 c = self.load_default_context()
408 c = self.load_default_context()
409 status = self.request.POST.get('changeset_status', None)
409 status = self.request.POST.get('changeset_status', None)
410 text = self.request.POST.get('text')
410 text = self.request.POST.get('text')
411 comment_type = self.request.POST.get('comment_type')
411 comment_type = self.request.POST.get('comment_type')
412 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
412 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
413
413
414 if status:
414 if status:
415 text = text or (_('Status change %(transition_icon)s %(status)s')
415 text = text or (_('Status change %(transition_icon)s %(status)s')
416 % {'transition_icon': '>',
416 % {'transition_icon': '>',
417 'status': ChangesetStatus.get_status_lbl(status)})
417 'status': ChangesetStatus.get_status_lbl(status)})
418
418
419 multi_commit_ids = []
419 multi_commit_ids = []
420 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
420 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
421 if _commit_id not in ['', None, EmptyCommit.raw_id]:
421 if _commit_id not in ['', None, EmptyCommit.raw_id]:
422 if _commit_id not in multi_commit_ids:
422 if _commit_id not in multi_commit_ids:
423 multi_commit_ids.append(_commit_id)
423 multi_commit_ids.append(_commit_id)
424
424
425 commit_ids = multi_commit_ids or [commit_id]
425 commit_ids = multi_commit_ids or [commit_id]
426
426
427 comment = None
427 comment = None
428 for current_id in filter(None, commit_ids):
428 for current_id in filter(None, commit_ids):
429 comment = CommentsModel().create(
429 comment = CommentsModel().create(
430 text=text,
430 text=text,
431 repo=self.db_repo.repo_id,
431 repo=self.db_repo.repo_id,
432 user=self._rhodecode_db_user.user_id,
432 user=self._rhodecode_db_user.user_id,
433 commit_id=current_id,
433 commit_id=current_id,
434 f_path=self.request.POST.get('f_path'),
434 f_path=self.request.POST.get('f_path'),
435 line_no=self.request.POST.get('line'),
435 line_no=self.request.POST.get('line'),
436 status_change=(ChangesetStatus.get_status_lbl(status)
436 status_change=(ChangesetStatus.get_status_lbl(status)
437 if status else None),
437 if status else None),
438 status_change_type=status,
438 status_change_type=status,
439 comment_type=comment_type,
439 comment_type=comment_type,
440 resolves_comment_id=resolves_comment_id
440 resolves_comment_id=resolves_comment_id,
441 auth_user=self._rhodecode_user
441 )
442 )
442
443
443 # get status if set !
444 # get status if set !
444 if status:
445 if status:
445 # if latest status was from pull request and it's closed
446 # if latest status was from pull request and it's closed
446 # disallow changing status !
447 # disallow changing status !
447 # dont_allow_on_closed_pull_request = True !
448 # dont_allow_on_closed_pull_request = True !
448
449
449 try:
450 try:
450 ChangesetStatusModel().set_status(
451 ChangesetStatusModel().set_status(
451 self.db_repo.repo_id,
452 self.db_repo.repo_id,
452 status,
453 status,
453 self._rhodecode_db_user.user_id,
454 self._rhodecode_db_user.user_id,
454 comment,
455 comment,
455 revision=current_id,
456 revision=current_id,
456 dont_allow_on_closed_pull_request=True
457 dont_allow_on_closed_pull_request=True
457 )
458 )
458 except StatusChangeOnClosedPullRequestError:
459 except StatusChangeOnClosedPullRequestError:
459 msg = _('Changing the status of a commit associated with '
460 msg = _('Changing the status of a commit associated with '
460 'a closed pull request is not allowed')
461 'a closed pull request is not allowed')
461 log.exception(msg)
462 log.exception(msg)
462 h.flash(msg, category='warning')
463 h.flash(msg, category='warning')
463 raise HTTPFound(h.route_path(
464 raise HTTPFound(h.route_path(
464 'repo_commit', repo_name=self.db_repo_name,
465 'repo_commit', repo_name=self.db_repo_name,
465 commit_id=current_id))
466 commit_id=current_id))
466
467
467 # finalize, commit and redirect
468 # finalize, commit and redirect
468 Session().commit()
469 Session().commit()
469
470
470 data = {
471 data = {
471 'target_id': h.safeid(h.safe_unicode(
472 'target_id': h.safeid(h.safe_unicode(
472 self.request.POST.get('f_path'))),
473 self.request.POST.get('f_path'))),
473 }
474 }
474 if comment:
475 if comment:
475 c.co = comment
476 c.co = comment
476 rendered_comment = render(
477 rendered_comment = render(
477 'rhodecode:templates/changeset/changeset_comment_block.mako',
478 'rhodecode:templates/changeset/changeset_comment_block.mako',
478 self._get_template_context(c), self.request)
479 self._get_template_context(c), self.request)
479
480
480 data.update(comment.get_dict())
481 data.update(comment.get_dict())
481 data.update({'rendered_text': rendered_comment})
482 data.update({'rendered_text': rendered_comment})
482
483
483 return data
484 return data
484
485
485 @LoginRequired()
486 @LoginRequired()
486 @NotAnonymous()
487 @NotAnonymous()
487 @HasRepoPermissionAnyDecorator(
488 @HasRepoPermissionAnyDecorator(
488 'repository.read', 'repository.write', 'repository.admin')
489 'repository.read', 'repository.write', 'repository.admin')
489 @CSRFRequired()
490 @CSRFRequired()
490 @view_config(
491 @view_config(
491 route_name='repo_commit_comment_preview', request_method='POST',
492 route_name='repo_commit_comment_preview', request_method='POST',
492 renderer='string', xhr=True)
493 renderer='string', xhr=True)
493 def repo_commit_comment_preview(self):
494 def repo_commit_comment_preview(self):
494 # Technically a CSRF token is not needed as no state changes with this
495 # Technically a CSRF token is not needed as no state changes with this
495 # call. However, as this is a POST is better to have it, so automated
496 # call. However, as this is a POST is better to have it, so automated
496 # tools don't flag it as potential CSRF.
497 # tools don't flag it as potential CSRF.
497 # Post is required because the payload could be bigger than the maximum
498 # Post is required because the payload could be bigger than the maximum
498 # allowed by GET.
499 # allowed by GET.
499
500
500 text = self.request.POST.get('text')
501 text = self.request.POST.get('text')
501 renderer = self.request.POST.get('renderer') or 'rst'
502 renderer = self.request.POST.get('renderer') or 'rst'
502 if text:
503 if text:
503 return h.render(text, renderer=renderer, mentions=True)
504 return h.render(text, renderer=renderer, mentions=True)
504 return ''
505 return ''
505
506
506 @LoginRequired()
507 @LoginRequired()
507 @NotAnonymous()
508 @NotAnonymous()
508 @HasRepoPermissionAnyDecorator(
509 @HasRepoPermissionAnyDecorator(
509 'repository.read', 'repository.write', 'repository.admin')
510 'repository.read', 'repository.write', 'repository.admin')
510 @CSRFRequired()
511 @CSRFRequired()
511 @view_config(
512 @view_config(
512 route_name='repo_commit_comment_delete', request_method='POST',
513 route_name='repo_commit_comment_delete', request_method='POST',
513 renderer='json_ext')
514 renderer='json_ext')
514 def repo_commit_comment_delete(self):
515 def repo_commit_comment_delete(self):
515 commit_id = self.request.matchdict['commit_id']
516 commit_id = self.request.matchdict['commit_id']
516 comment_id = self.request.matchdict['comment_id']
517 comment_id = self.request.matchdict['comment_id']
517
518
518 comment = ChangesetComment.get_or_404(comment_id)
519 comment = ChangesetComment.get_or_404(comment_id)
519 if not comment:
520 if not comment:
520 log.debug('Comment with id:%s not found, skipping', comment_id)
521 log.debug('Comment with id:%s not found, skipping', comment_id)
521 # comment already deleted in another call probably
522 # comment already deleted in another call probably
522 return True
523 return True
523
524
524 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
525 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
525 super_admin = h.HasPermissionAny('hg.admin')()
526 super_admin = h.HasPermissionAny('hg.admin')()
526 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
527 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
527 is_repo_comment = comment.repo.repo_name == self.db_repo_name
528 is_repo_comment = comment.repo.repo_name == self.db_repo_name
528 comment_repo_admin = is_repo_admin and is_repo_comment
529 comment_repo_admin = is_repo_admin and is_repo_comment
529
530
530 if super_admin or comment_owner or comment_repo_admin:
531 if super_admin or comment_owner or comment_repo_admin:
531 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
532 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
532 Session().commit()
533 Session().commit()
533 return True
534 return True
534 else:
535 else:
535 log.warning('No permissions for user %s to delete comment_id: %s',
536 log.warning('No permissions for user %s to delete comment_id: %s',
536 self._rhodecode_db_user, comment_id)
537 self._rhodecode_db_user, comment_id)
537 raise HTTPNotFound()
538 raise HTTPNotFound()
538
539
539 @LoginRequired()
540 @LoginRequired()
540 @HasRepoPermissionAnyDecorator(
541 @HasRepoPermissionAnyDecorator(
541 'repository.read', 'repository.write', 'repository.admin')
542 'repository.read', 'repository.write', 'repository.admin')
542 @view_config(
543 @view_config(
543 route_name='repo_commit_data', request_method='GET',
544 route_name='repo_commit_data', request_method='GET',
544 renderer='json_ext', xhr=True)
545 renderer='json_ext', xhr=True)
545 def repo_commit_data(self):
546 def repo_commit_data(self):
546 commit_id = self.request.matchdict['commit_id']
547 commit_id = self.request.matchdict['commit_id']
547 self.load_default_context()
548 self.load_default_context()
548
549
549 try:
550 try:
550 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
551 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
551 except CommitDoesNotExistError as e:
552 except CommitDoesNotExistError as e:
552 return EmptyCommit(message=str(e))
553 return EmptyCommit(message=str(e))
553
554
554 @LoginRequired()
555 @LoginRequired()
555 @HasRepoPermissionAnyDecorator(
556 @HasRepoPermissionAnyDecorator(
556 'repository.read', 'repository.write', 'repository.admin')
557 'repository.read', 'repository.write', 'repository.admin')
557 @view_config(
558 @view_config(
558 route_name='repo_commit_children', request_method='GET',
559 route_name='repo_commit_children', request_method='GET',
559 renderer='json_ext', xhr=True)
560 renderer='json_ext', xhr=True)
560 def repo_commit_children(self):
561 def repo_commit_children(self):
561 commit_id = self.request.matchdict['commit_id']
562 commit_id = self.request.matchdict['commit_id']
562 self.load_default_context()
563 self.load_default_context()
563
564
564 try:
565 try:
565 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
566 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
566 children = commit.children
567 children = commit.children
567 except CommitDoesNotExistError:
568 except CommitDoesNotExistError:
568 children = []
569 children = []
569
570
570 result = {"results": children}
571 result = {"results": children}
571 return result
572 return result
572
573
573 @LoginRequired()
574 @LoginRequired()
574 @HasRepoPermissionAnyDecorator(
575 @HasRepoPermissionAnyDecorator(
575 'repository.read', 'repository.write', 'repository.admin')
576 'repository.read', 'repository.write', 'repository.admin')
576 @view_config(
577 @view_config(
577 route_name='repo_commit_parents', request_method='GET',
578 route_name='repo_commit_parents', request_method='GET',
578 renderer='json_ext')
579 renderer='json_ext')
579 def repo_commit_parents(self):
580 def repo_commit_parents(self):
580 commit_id = self.request.matchdict['commit_id']
581 commit_id = self.request.matchdict['commit_id']
581 self.load_default_context()
582 self.load_default_context()
582
583
583 try:
584 try:
584 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
585 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
585 parents = commit.parents
586 parents = commit.parents
586 except CommitDoesNotExistError:
587 except CommitDoesNotExistError:
587 parents = []
588 parents = []
588 result = {"results": parents}
589 result = {"results": parents}
589 return result
590 return result
@@ -1,1298 +1,1298 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 RepositoryRequirementError, EmptyRepositoryError)
45 RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 ChangesetComment, ChangesetStatus, Repository)
49 ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64
64
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, source=source, opened_by=opened_by,
79 repo_name, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, source=source, statuses=statuses,
83 repo_name, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, source=source, opened_by=opened_by,
87 repo_name, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, source=source, opened_by=opened_by,
96 repo_name, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, source=source, statuses=statuses,
100 repo_name, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.target_repo.repo_name),
111 pr.pull_request_id, pr.target_repo.repo_name),
112 'name_raw': pr.pull_request_id,
112 'name_raw': pr.pull_request_id,
113 'status': _render('pullrequest_status',
113 'status': _render('pullrequest_status',
114 pr.calculated_review_status()),
114 pr.calculated_review_status()),
115 'title': _render(
115 'title': _render(
116 'pullrequest_title', pr.title, pr.description),
116 'pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
117 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
118 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
119 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
121 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
122 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'author': _render('pullrequest_author',
124 'author': _render('pullrequest_author',
125 pr.author.full_contact, ),
125 pr.author.full_contact, ),
126 'author_raw': pr.author.full_name,
126 'author_raw': pr.author.full_name,
127 'comments': _render('pullrequest_comments', len(comments)),
127 'comments': _render('pullrequest_comments', len(comments)),
128 'comments_raw': len(comments),
128 'comments_raw': len(comments),
129 'closed': pr.is_closed(),
129 'closed': pr.is_closed(),
130 })
130 })
131
131
132 data = ({
132 data = ({
133 'draw': draw,
133 'draw': draw,
134 'data': data,
134 'data': data,
135 'recordsTotal': pull_requests_total_count,
135 'recordsTotal': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
137 })
137 })
138 return data
138 return data
139
139
140 @LoginRequired()
140 @LoginRequired()
141 @HasRepoPermissionAnyDecorator(
141 @HasRepoPermissionAnyDecorator(
142 'repository.read', 'repository.write', 'repository.admin')
142 'repository.read', 'repository.write', 'repository.admin')
143 @view_config(
143 @view_config(
144 route_name='pullrequest_show_all', request_method='GET',
144 route_name='pullrequest_show_all', request_method='GET',
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 def pull_request_list(self):
146 def pull_request_list(self):
147 c = self.load_default_context()
147 c = self.load_default_context()
148
148
149 req_get = self.request.GET
149 req_get = self.request.GET
150 c.source = str2bool(req_get.get('source'))
150 c.source = str2bool(req_get.get('source'))
151 c.closed = str2bool(req_get.get('closed'))
151 c.closed = str2bool(req_get.get('closed'))
152 c.my = str2bool(req_get.get('my'))
152 c.my = str2bool(req_get.get('my'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155
155
156 c.active = 'open'
156 c.active = 'open'
157 if c.my:
157 if c.my:
158 c.active = 'my'
158 c.active = 'my'
159 if c.closed:
159 if c.closed:
160 c.active = 'closed'
160 c.active = 'closed'
161 if c.awaiting_review and not c.source:
161 if c.awaiting_review and not c.source:
162 c.active = 'awaiting'
162 c.active = 'awaiting'
163 if c.source and not c.awaiting_review:
163 if c.source and not c.awaiting_review:
164 c.active = 'source'
164 c.active = 'source'
165 if c.awaiting_my_review:
165 if c.awaiting_my_review:
166 c.active = 'awaiting_my'
166 c.active = 'awaiting_my'
167
167
168 return self._get_template_context(c)
168 return self._get_template_context(c)
169
169
170 @LoginRequired()
170 @LoginRequired()
171 @HasRepoPermissionAnyDecorator(
171 @HasRepoPermissionAnyDecorator(
172 'repository.read', 'repository.write', 'repository.admin')
172 'repository.read', 'repository.write', 'repository.admin')
173 @view_config(
173 @view_config(
174 route_name='pullrequest_show_all_data', request_method='GET',
174 route_name='pullrequest_show_all_data', request_method='GET',
175 renderer='json_ext', xhr=True)
175 renderer='json_ext', xhr=True)
176 def pull_request_list_data(self):
176 def pull_request_list_data(self):
177 self.load_default_context()
177 self.load_default_context()
178
178
179 # additional filters
179 # additional filters
180 req_get = self.request.GET
180 req_get = self.request.GET
181 source = str2bool(req_get.get('source'))
181 source = str2bool(req_get.get('source'))
182 closed = str2bool(req_get.get('closed'))
182 closed = str2bool(req_get.get('closed'))
183 my = str2bool(req_get.get('my'))
183 my = str2bool(req_get.get('my'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186
186
187 filter_type = 'awaiting_review' if awaiting_review \
187 filter_type = 'awaiting_review' if awaiting_review \
188 else 'awaiting_my_review' if awaiting_my_review \
188 else 'awaiting_my_review' if awaiting_my_review \
189 else None
189 else None
190
190
191 opened_by = None
191 opened_by = None
192 if my:
192 if my:
193 opened_by = [self._rhodecode_user.user_id]
193 opened_by = [self._rhodecode_user.user_id]
194
194
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 if closed:
196 if closed:
197 statuses = [PullRequest.STATUS_CLOSED]
197 statuses = [PullRequest.STATUS_CLOSED]
198
198
199 data = self._get_pull_requests_list(
199 data = self._get_pull_requests_list(
200 repo_name=self.db_repo_name, source=source,
200 repo_name=self.db_repo_name, source=source,
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202
202
203 return data
203 return data
204
204
205 def _is_diff_cache_enabled(self, target_repo):
205 def _is_diff_cache_enabled(self, target_repo):
206 caching_enabled = self._get_general_setting(
206 caching_enabled = self._get_general_setting(
207 target_repo, 'rhodecode_diff_cache')
207 target_repo, 'rhodecode_diff_cache')
208 log.debug('Diff caching enabled: %s', caching_enabled)
208 log.debug('Diff caching enabled: %s', caching_enabled)
209 return caching_enabled
209 return caching_enabled
210
210
211 def _get_diffset(self, source_repo_name, source_repo,
211 def _get_diffset(self, source_repo_name, source_repo,
212 source_ref_id, target_ref_id,
212 source_ref_id, target_ref_id,
213 target_commit, source_commit, diff_limit, file_limit,
213 target_commit, source_commit, diff_limit, file_limit,
214 fulldiff):
214 fulldiff):
215
215
216 vcs_diff = PullRequestModel().get_diff(
216 vcs_diff = PullRequestModel().get_diff(
217 source_repo, source_ref_id, target_ref_id)
217 source_repo, source_ref_id, target_ref_id)
218
218
219 diff_processor = diffs.DiffProcessor(
219 diff_processor = diffs.DiffProcessor(
220 vcs_diff, format='newdiff', diff_limit=diff_limit,
220 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 file_limit=file_limit, show_full_diff=fulldiff)
221 file_limit=file_limit, show_full_diff=fulldiff)
222
222
223 _parsed = diff_processor.prepare()
223 _parsed = diff_processor.prepare()
224
224
225 diffset = codeblocks.DiffSet(
225 diffset = codeblocks.DiffSet(
226 repo_name=self.db_repo_name,
226 repo_name=self.db_repo_name,
227 source_repo_name=source_repo_name,
227 source_repo_name=source_repo_name,
228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 )
230 )
231 diffset = self.path_filter.render_patchset_filtered(
231 diffset = self.path_filter.render_patchset_filtered(
232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233
233
234 return diffset
234 return diffset
235
235
236 @LoginRequired()
236 @LoginRequired()
237 @HasRepoPermissionAnyDecorator(
237 @HasRepoPermissionAnyDecorator(
238 'repository.read', 'repository.write', 'repository.admin')
238 'repository.read', 'repository.write', 'repository.admin')
239 @view_config(
239 @view_config(
240 route_name='pullrequest_show', request_method='GET',
240 route_name='pullrequest_show', request_method='GET',
241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
242 def pull_request_show(self):
242 def pull_request_show(self):
243 pull_request_id = self.request.matchdict['pull_request_id']
243 pull_request_id = self.request.matchdict['pull_request_id']
244
244
245 c = self.load_default_context()
245 c = self.load_default_context()
246
246
247 version = self.request.GET.get('version')
247 version = self.request.GET.get('version')
248 from_version = self.request.GET.get('from_version') or version
248 from_version = self.request.GET.get('from_version') or version
249 merge_checks = self.request.GET.get('merge_checks')
249 merge_checks = self.request.GET.get('merge_checks')
250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
251
251
252 (pull_request_latest,
252 (pull_request_latest,
253 pull_request_at_ver,
253 pull_request_at_ver,
254 pull_request_display_obj,
254 pull_request_display_obj,
255 at_version) = PullRequestModel().get_pr_version(
255 at_version) = PullRequestModel().get_pr_version(
256 pull_request_id, version=version)
256 pull_request_id, version=version)
257 pr_closed = pull_request_latest.is_closed()
257 pr_closed = pull_request_latest.is_closed()
258
258
259 if pr_closed and (version or from_version):
259 if pr_closed and (version or from_version):
260 # not allow to browse versions
260 # not allow to browse versions
261 raise HTTPFound(h.route_path(
261 raise HTTPFound(h.route_path(
262 'pullrequest_show', repo_name=self.db_repo_name,
262 'pullrequest_show', repo_name=self.db_repo_name,
263 pull_request_id=pull_request_id))
263 pull_request_id=pull_request_id))
264
264
265 versions = pull_request_display_obj.versions()
265 versions = pull_request_display_obj.versions()
266
266
267 c.at_version = at_version
267 c.at_version = at_version
268 c.at_version_num = (at_version
268 c.at_version_num = (at_version
269 if at_version and at_version != 'latest'
269 if at_version and at_version != 'latest'
270 else None)
270 else None)
271 c.at_version_pos = ChangesetComment.get_index_from_version(
271 c.at_version_pos = ChangesetComment.get_index_from_version(
272 c.at_version_num, versions)
272 c.at_version_num, versions)
273
273
274 (prev_pull_request_latest,
274 (prev_pull_request_latest,
275 prev_pull_request_at_ver,
275 prev_pull_request_at_ver,
276 prev_pull_request_display_obj,
276 prev_pull_request_display_obj,
277 prev_at_version) = PullRequestModel().get_pr_version(
277 prev_at_version) = PullRequestModel().get_pr_version(
278 pull_request_id, version=from_version)
278 pull_request_id, version=from_version)
279
279
280 c.from_version = prev_at_version
280 c.from_version = prev_at_version
281 c.from_version_num = (prev_at_version
281 c.from_version_num = (prev_at_version
282 if prev_at_version and prev_at_version != 'latest'
282 if prev_at_version and prev_at_version != 'latest'
283 else None)
283 else None)
284 c.from_version_pos = ChangesetComment.get_index_from_version(
284 c.from_version_pos = ChangesetComment.get_index_from_version(
285 c.from_version_num, versions)
285 c.from_version_num, versions)
286
286
287 # define if we're in COMPARE mode or VIEW at version mode
287 # define if we're in COMPARE mode or VIEW at version mode
288 compare = at_version != prev_at_version
288 compare = at_version != prev_at_version
289
289
290 # pull_requests repo_name we opened it against
290 # pull_requests repo_name we opened it against
291 # ie. target_repo must match
291 # ie. target_repo must match
292 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
292 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
293 raise HTTPNotFound()
293 raise HTTPNotFound()
294
294
295 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
295 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
296 pull_request_at_ver)
296 pull_request_at_ver)
297
297
298 c.pull_request = pull_request_display_obj
298 c.pull_request = pull_request_display_obj
299 c.pull_request_latest = pull_request_latest
299 c.pull_request_latest = pull_request_latest
300
300
301 if compare or (at_version and not at_version == 'latest'):
301 if compare or (at_version and not at_version == 'latest'):
302 c.allowed_to_change_status = False
302 c.allowed_to_change_status = False
303 c.allowed_to_update = False
303 c.allowed_to_update = False
304 c.allowed_to_merge = False
304 c.allowed_to_merge = False
305 c.allowed_to_delete = False
305 c.allowed_to_delete = False
306 c.allowed_to_comment = False
306 c.allowed_to_comment = False
307 c.allowed_to_close = False
307 c.allowed_to_close = False
308 else:
308 else:
309 can_change_status = PullRequestModel().check_user_change_status(
309 can_change_status = PullRequestModel().check_user_change_status(
310 pull_request_at_ver, self._rhodecode_user)
310 pull_request_at_ver, self._rhodecode_user)
311 c.allowed_to_change_status = can_change_status and not pr_closed
311 c.allowed_to_change_status = can_change_status and not pr_closed
312
312
313 c.allowed_to_update = PullRequestModel().check_user_update(
313 c.allowed_to_update = PullRequestModel().check_user_update(
314 pull_request_latest, self._rhodecode_user) and not pr_closed
314 pull_request_latest, self._rhodecode_user) and not pr_closed
315 c.allowed_to_merge = PullRequestModel().check_user_merge(
315 c.allowed_to_merge = PullRequestModel().check_user_merge(
316 pull_request_latest, self._rhodecode_user) and not pr_closed
316 pull_request_latest, self._rhodecode_user) and not pr_closed
317 c.allowed_to_delete = PullRequestModel().check_user_delete(
317 c.allowed_to_delete = PullRequestModel().check_user_delete(
318 pull_request_latest, self._rhodecode_user) and not pr_closed
318 pull_request_latest, self._rhodecode_user) and not pr_closed
319 c.allowed_to_comment = not pr_closed
319 c.allowed_to_comment = not pr_closed
320 c.allowed_to_close = c.allowed_to_merge and not pr_closed
320 c.allowed_to_close = c.allowed_to_merge and not pr_closed
321
321
322 c.forbid_adding_reviewers = False
322 c.forbid_adding_reviewers = False
323 c.forbid_author_to_review = False
323 c.forbid_author_to_review = False
324 c.forbid_commit_author_to_review = False
324 c.forbid_commit_author_to_review = False
325
325
326 if pull_request_latest.reviewer_data and \
326 if pull_request_latest.reviewer_data and \
327 'rules' in pull_request_latest.reviewer_data:
327 'rules' in pull_request_latest.reviewer_data:
328 rules = pull_request_latest.reviewer_data['rules'] or {}
328 rules = pull_request_latest.reviewer_data['rules'] or {}
329 try:
329 try:
330 c.forbid_adding_reviewers = rules.get(
330 c.forbid_adding_reviewers = rules.get(
331 'forbid_adding_reviewers')
331 'forbid_adding_reviewers')
332 c.forbid_author_to_review = rules.get(
332 c.forbid_author_to_review = rules.get(
333 'forbid_author_to_review')
333 'forbid_author_to_review')
334 c.forbid_commit_author_to_review = rules.get(
334 c.forbid_commit_author_to_review = rules.get(
335 'forbid_commit_author_to_review')
335 'forbid_commit_author_to_review')
336 except Exception:
336 except Exception:
337 pass
337 pass
338
338
339 # check merge capabilities
339 # check merge capabilities
340 _merge_check = MergeCheck.validate(
340 _merge_check = MergeCheck.validate(
341 pull_request_latest, user=self._rhodecode_user,
341 pull_request_latest, user=self._rhodecode_user,
342 translator=self.request.translate)
342 translator=self.request.translate)
343 c.pr_merge_errors = _merge_check.error_details
343 c.pr_merge_errors = _merge_check.error_details
344 c.pr_merge_possible = not _merge_check.failed
344 c.pr_merge_possible = not _merge_check.failed
345 c.pr_merge_message = _merge_check.merge_msg
345 c.pr_merge_message = _merge_check.merge_msg
346
346
347 c.pr_merge_info = MergeCheck.get_merge_conditions(
347 c.pr_merge_info = MergeCheck.get_merge_conditions(
348 pull_request_latest, translator=self.request.translate)
348 pull_request_latest, translator=self.request.translate)
349
349
350 c.pull_request_review_status = _merge_check.review_status
350 c.pull_request_review_status = _merge_check.review_status
351 if merge_checks:
351 if merge_checks:
352 self.request.override_renderer = \
352 self.request.override_renderer = \
353 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
353 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
354 return self._get_template_context(c)
354 return self._get_template_context(c)
355
355
356 comments_model = CommentsModel()
356 comments_model = CommentsModel()
357
357
358 # reviewers and statuses
358 # reviewers and statuses
359 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
359 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
360 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
360 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
361
361
362 # GENERAL COMMENTS with versions #
362 # GENERAL COMMENTS with versions #
363 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
363 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
364 q = q.order_by(ChangesetComment.comment_id.asc())
364 q = q.order_by(ChangesetComment.comment_id.asc())
365 general_comments = q
365 general_comments = q
366
366
367 # pick comments we want to render at current version
367 # pick comments we want to render at current version
368 c.comment_versions = comments_model.aggregate_comments(
368 c.comment_versions = comments_model.aggregate_comments(
369 general_comments, versions, c.at_version_num)
369 general_comments, versions, c.at_version_num)
370 c.comments = c.comment_versions[c.at_version_num]['until']
370 c.comments = c.comment_versions[c.at_version_num]['until']
371
371
372 # INLINE COMMENTS with versions #
372 # INLINE COMMENTS with versions #
373 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
373 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
374 q = q.order_by(ChangesetComment.comment_id.asc())
374 q = q.order_by(ChangesetComment.comment_id.asc())
375 inline_comments = q
375 inline_comments = q
376
376
377 c.inline_versions = comments_model.aggregate_comments(
377 c.inline_versions = comments_model.aggregate_comments(
378 inline_comments, versions, c.at_version_num, inline=True)
378 inline_comments, versions, c.at_version_num, inline=True)
379
379
380 # inject latest version
380 # inject latest version
381 latest_ver = PullRequest.get_pr_display_object(
381 latest_ver = PullRequest.get_pr_display_object(
382 pull_request_latest, pull_request_latest)
382 pull_request_latest, pull_request_latest)
383
383
384 c.versions = versions + [latest_ver]
384 c.versions = versions + [latest_ver]
385
385
386 # if we use version, then do not show later comments
386 # if we use version, then do not show later comments
387 # than current version
387 # than current version
388 display_inline_comments = collections.defaultdict(
388 display_inline_comments = collections.defaultdict(
389 lambda: collections.defaultdict(list))
389 lambda: collections.defaultdict(list))
390 for co in inline_comments:
390 for co in inline_comments:
391 if c.at_version_num:
391 if c.at_version_num:
392 # pick comments that are at least UPTO given version, so we
392 # pick comments that are at least UPTO given version, so we
393 # don't render comments for higher version
393 # don't render comments for higher version
394 should_render = co.pull_request_version_id and \
394 should_render = co.pull_request_version_id and \
395 co.pull_request_version_id <= c.at_version_num
395 co.pull_request_version_id <= c.at_version_num
396 else:
396 else:
397 # showing all, for 'latest'
397 # showing all, for 'latest'
398 should_render = True
398 should_render = True
399
399
400 if should_render:
400 if should_render:
401 display_inline_comments[co.f_path][co.line_no].append(co)
401 display_inline_comments[co.f_path][co.line_no].append(co)
402
402
403 # load diff data into template context, if we use compare mode then
403 # load diff data into template context, if we use compare mode then
404 # diff is calculated based on changes between versions of PR
404 # diff is calculated based on changes between versions of PR
405
405
406 source_repo = pull_request_at_ver.source_repo
406 source_repo = pull_request_at_ver.source_repo
407 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
407 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
408
408
409 target_repo = pull_request_at_ver.target_repo
409 target_repo = pull_request_at_ver.target_repo
410 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
410 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
411
411
412 if compare:
412 if compare:
413 # in compare switch the diff base to latest commit from prev version
413 # in compare switch the diff base to latest commit from prev version
414 target_ref_id = prev_pull_request_display_obj.revisions[0]
414 target_ref_id = prev_pull_request_display_obj.revisions[0]
415
415
416 # despite opening commits for bookmarks/branches/tags, we always
416 # despite opening commits for bookmarks/branches/tags, we always
417 # convert this to rev to prevent changes after bookmark or branch change
417 # convert this to rev to prevent changes after bookmark or branch change
418 c.source_ref_type = 'rev'
418 c.source_ref_type = 'rev'
419 c.source_ref = source_ref_id
419 c.source_ref = source_ref_id
420
420
421 c.target_ref_type = 'rev'
421 c.target_ref_type = 'rev'
422 c.target_ref = target_ref_id
422 c.target_ref = target_ref_id
423
423
424 c.source_repo = source_repo
424 c.source_repo = source_repo
425 c.target_repo = target_repo
425 c.target_repo = target_repo
426
426
427 c.commit_ranges = []
427 c.commit_ranges = []
428 source_commit = EmptyCommit()
428 source_commit = EmptyCommit()
429 target_commit = EmptyCommit()
429 target_commit = EmptyCommit()
430 c.missing_requirements = False
430 c.missing_requirements = False
431
431
432 source_scm = source_repo.scm_instance()
432 source_scm = source_repo.scm_instance()
433 target_scm = target_repo.scm_instance()
433 target_scm = target_repo.scm_instance()
434
434
435 # try first shadow repo, fallback to regular repo
435 # try first shadow repo, fallback to regular repo
436 try:
436 try:
437 commits_source_repo = pull_request_latest.get_shadow_repo()
437 commits_source_repo = pull_request_latest.get_shadow_repo()
438 except Exception:
438 except Exception:
439 log.debug('Failed to get shadow repo', exc_info=True)
439 log.debug('Failed to get shadow repo', exc_info=True)
440 commits_source_repo = source_scm
440 commits_source_repo = source_scm
441
441
442 c.commits_source_repo = commits_source_repo
442 c.commits_source_repo = commits_source_repo
443 c.ancestor = None # set it to None, to hide it from PR view
443 c.ancestor = None # set it to None, to hide it from PR view
444
444
445 # empty version means latest, so we keep this to prevent
445 # empty version means latest, so we keep this to prevent
446 # double caching
446 # double caching
447 version_normalized = version or 'latest'
447 version_normalized = version or 'latest'
448 from_version_normalized = from_version or 'latest'
448 from_version_normalized = from_version or 'latest'
449
449
450 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
450 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
451 target_repo)
451 target_repo)
452 cache_file_path = diff_cache_exist(
452 cache_file_path = diff_cache_exist(
453 cache_path, 'pull_request', pull_request_id, version_normalized,
453 cache_path, 'pull_request', pull_request_id, version_normalized,
454 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
454 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
455
455
456 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
456 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
457 force_recache = str2bool(self.request.GET.get('force_recache'))
457 force_recache = str2bool(self.request.GET.get('force_recache'))
458
458
459 cached_diff = None
459 cached_diff = None
460 if caching_enabled:
460 if caching_enabled:
461 cached_diff = load_cached_diff(cache_file_path)
461 cached_diff = load_cached_diff(cache_file_path)
462
462
463 has_proper_commit_cache = (
463 has_proper_commit_cache = (
464 cached_diff and cached_diff.get('commits')
464 cached_diff and cached_diff.get('commits')
465 and len(cached_diff.get('commits', [])) == 5
465 and len(cached_diff.get('commits', [])) == 5
466 and cached_diff.get('commits')[0]
466 and cached_diff.get('commits')[0]
467 and cached_diff.get('commits')[3])
467 and cached_diff.get('commits')[3])
468 if not force_recache and has_proper_commit_cache:
468 if not force_recache and has_proper_commit_cache:
469 diff_commit_cache = \
469 diff_commit_cache = \
470 (ancestor_commit, commit_cache, missing_requirements,
470 (ancestor_commit, commit_cache, missing_requirements,
471 source_commit, target_commit) = cached_diff['commits']
471 source_commit, target_commit) = cached_diff['commits']
472 else:
472 else:
473 diff_commit_cache = \
473 diff_commit_cache = \
474 (ancestor_commit, commit_cache, missing_requirements,
474 (ancestor_commit, commit_cache, missing_requirements,
475 source_commit, target_commit) = self.get_commits(
475 source_commit, target_commit) = self.get_commits(
476 commits_source_repo,
476 commits_source_repo,
477 pull_request_at_ver,
477 pull_request_at_ver,
478 source_commit,
478 source_commit,
479 source_ref_id,
479 source_ref_id,
480 source_scm,
480 source_scm,
481 target_commit,
481 target_commit,
482 target_ref_id,
482 target_ref_id,
483 target_scm)
483 target_scm)
484
484
485 # register our commit range
485 # register our commit range
486 for comm in commit_cache.values():
486 for comm in commit_cache.values():
487 c.commit_ranges.append(comm)
487 c.commit_ranges.append(comm)
488
488
489 c.missing_requirements = missing_requirements
489 c.missing_requirements = missing_requirements
490 c.ancestor_commit = ancestor_commit
490 c.ancestor_commit = ancestor_commit
491 c.statuses = source_repo.statuses(
491 c.statuses = source_repo.statuses(
492 [x.raw_id for x in c.commit_ranges])
492 [x.raw_id for x in c.commit_ranges])
493
493
494 # auto collapse if we have more than limit
494 # auto collapse if we have more than limit
495 collapse_limit = diffs.DiffProcessor._collapse_commits_over
495 collapse_limit = diffs.DiffProcessor._collapse_commits_over
496 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
496 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
497 c.compare_mode = compare
497 c.compare_mode = compare
498
498
499 # diff_limit is the old behavior, will cut off the whole diff
499 # diff_limit is the old behavior, will cut off the whole diff
500 # if the limit is applied otherwise will just hide the
500 # if the limit is applied otherwise will just hide the
501 # big files from the front-end
501 # big files from the front-end
502 diff_limit = c.visual.cut_off_limit_diff
502 diff_limit = c.visual.cut_off_limit_diff
503 file_limit = c.visual.cut_off_limit_file
503 file_limit = c.visual.cut_off_limit_file
504
504
505 c.missing_commits = False
505 c.missing_commits = False
506 if (c.missing_requirements
506 if (c.missing_requirements
507 or isinstance(source_commit, EmptyCommit)
507 or isinstance(source_commit, EmptyCommit)
508 or source_commit == target_commit):
508 or source_commit == target_commit):
509
509
510 c.missing_commits = True
510 c.missing_commits = True
511 else:
511 else:
512 c.inline_comments = display_inline_comments
512 c.inline_comments = display_inline_comments
513
513
514 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
514 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
515 if not force_recache and has_proper_diff_cache:
515 if not force_recache and has_proper_diff_cache:
516 c.diffset = cached_diff['diff']
516 c.diffset = cached_diff['diff']
517 (ancestor_commit, commit_cache, missing_requirements,
517 (ancestor_commit, commit_cache, missing_requirements,
518 source_commit, target_commit) = cached_diff['commits']
518 source_commit, target_commit) = cached_diff['commits']
519 else:
519 else:
520 c.diffset = self._get_diffset(
520 c.diffset = self._get_diffset(
521 c.source_repo.repo_name, commits_source_repo,
521 c.source_repo.repo_name, commits_source_repo,
522 source_ref_id, target_ref_id,
522 source_ref_id, target_ref_id,
523 target_commit, source_commit,
523 target_commit, source_commit,
524 diff_limit, file_limit, c.fulldiff)
524 diff_limit, file_limit, c.fulldiff)
525
525
526 # save cached diff
526 # save cached diff
527 if caching_enabled:
527 if caching_enabled:
528 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
528 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
529
529
530 c.limited_diff = c.diffset.limited_diff
530 c.limited_diff = c.diffset.limited_diff
531
531
532 # calculate removed files that are bound to comments
532 # calculate removed files that are bound to comments
533 comment_deleted_files = [
533 comment_deleted_files = [
534 fname for fname in display_inline_comments
534 fname for fname in display_inline_comments
535 if fname not in c.diffset.file_stats]
535 if fname not in c.diffset.file_stats]
536
536
537 c.deleted_files_comments = collections.defaultdict(dict)
537 c.deleted_files_comments = collections.defaultdict(dict)
538 for fname, per_line_comments in display_inline_comments.items():
538 for fname, per_line_comments in display_inline_comments.items():
539 if fname in comment_deleted_files:
539 if fname in comment_deleted_files:
540 c.deleted_files_comments[fname]['stats'] = 0
540 c.deleted_files_comments[fname]['stats'] = 0
541 c.deleted_files_comments[fname]['comments'] = list()
541 c.deleted_files_comments[fname]['comments'] = list()
542 for lno, comments in per_line_comments.items():
542 for lno, comments in per_line_comments.items():
543 c.deleted_files_comments[fname]['comments'].extend(
543 c.deleted_files_comments[fname]['comments'].extend(
544 comments)
544 comments)
545
545
546 # this is a hack to properly display links, when creating PR, the
546 # this is a hack to properly display links, when creating PR, the
547 # compare view and others uses different notation, and
547 # compare view and others uses different notation, and
548 # compare_commits.mako renders links based on the target_repo.
548 # compare_commits.mako renders links based on the target_repo.
549 # We need to swap that here to generate it properly on the html side
549 # We need to swap that here to generate it properly on the html side
550 c.target_repo = c.source_repo
550 c.target_repo = c.source_repo
551
551
552 c.commit_statuses = ChangesetStatus.STATUSES
552 c.commit_statuses = ChangesetStatus.STATUSES
553
553
554 c.show_version_changes = not pr_closed
554 c.show_version_changes = not pr_closed
555 if c.show_version_changes:
555 if c.show_version_changes:
556 cur_obj = pull_request_at_ver
556 cur_obj = pull_request_at_ver
557 prev_obj = prev_pull_request_at_ver
557 prev_obj = prev_pull_request_at_ver
558
558
559 old_commit_ids = prev_obj.revisions
559 old_commit_ids = prev_obj.revisions
560 new_commit_ids = cur_obj.revisions
560 new_commit_ids = cur_obj.revisions
561 commit_changes = PullRequestModel()._calculate_commit_id_changes(
561 commit_changes = PullRequestModel()._calculate_commit_id_changes(
562 old_commit_ids, new_commit_ids)
562 old_commit_ids, new_commit_ids)
563 c.commit_changes_summary = commit_changes
563 c.commit_changes_summary = commit_changes
564
564
565 # calculate the diff for commits between versions
565 # calculate the diff for commits between versions
566 c.commit_changes = []
566 c.commit_changes = []
567 mark = lambda cs, fw: list(
567 mark = lambda cs, fw: list(
568 h.itertools.izip_longest([], cs, fillvalue=fw))
568 h.itertools.izip_longest([], cs, fillvalue=fw))
569 for c_type, raw_id in mark(commit_changes.added, 'a') \
569 for c_type, raw_id in mark(commit_changes.added, 'a') \
570 + mark(commit_changes.removed, 'r') \
570 + mark(commit_changes.removed, 'r') \
571 + mark(commit_changes.common, 'c'):
571 + mark(commit_changes.common, 'c'):
572
572
573 if raw_id in commit_cache:
573 if raw_id in commit_cache:
574 commit = commit_cache[raw_id]
574 commit = commit_cache[raw_id]
575 else:
575 else:
576 try:
576 try:
577 commit = commits_source_repo.get_commit(raw_id)
577 commit = commits_source_repo.get_commit(raw_id)
578 except CommitDoesNotExistError:
578 except CommitDoesNotExistError:
579 # in case we fail extracting still use "dummy" commit
579 # in case we fail extracting still use "dummy" commit
580 # for display in commit diff
580 # for display in commit diff
581 commit = h.AttributeDict(
581 commit = h.AttributeDict(
582 {'raw_id': raw_id,
582 {'raw_id': raw_id,
583 'message': 'EMPTY or MISSING COMMIT'})
583 'message': 'EMPTY or MISSING COMMIT'})
584 c.commit_changes.append([c_type, commit])
584 c.commit_changes.append([c_type, commit])
585
585
586 # current user review statuses for each version
586 # current user review statuses for each version
587 c.review_versions = {}
587 c.review_versions = {}
588 if self._rhodecode_user.user_id in allowed_reviewers:
588 if self._rhodecode_user.user_id in allowed_reviewers:
589 for co in general_comments:
589 for co in general_comments:
590 if co.author.user_id == self._rhodecode_user.user_id:
590 if co.author.user_id == self._rhodecode_user.user_id:
591 status = co.status_change
591 status = co.status_change
592 if status:
592 if status:
593 _ver_pr = status[0].comment.pull_request_version_id
593 _ver_pr = status[0].comment.pull_request_version_id
594 c.review_versions[_ver_pr] = status[0]
594 c.review_versions[_ver_pr] = status[0]
595
595
596 return self._get_template_context(c)
596 return self._get_template_context(c)
597
597
598 def get_commits(
598 def get_commits(
599 self, commits_source_repo, pull_request_at_ver, source_commit,
599 self, commits_source_repo, pull_request_at_ver, source_commit,
600 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
600 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
601 commit_cache = collections.OrderedDict()
601 commit_cache = collections.OrderedDict()
602 missing_requirements = False
602 missing_requirements = False
603 try:
603 try:
604 pre_load = ["author", "branch", "date", "message"]
604 pre_load = ["author", "branch", "date", "message"]
605 show_revs = pull_request_at_ver.revisions
605 show_revs = pull_request_at_ver.revisions
606 for rev in show_revs:
606 for rev in show_revs:
607 comm = commits_source_repo.get_commit(
607 comm = commits_source_repo.get_commit(
608 commit_id=rev, pre_load=pre_load)
608 commit_id=rev, pre_load=pre_load)
609 commit_cache[comm.raw_id] = comm
609 commit_cache[comm.raw_id] = comm
610
610
611 # Order here matters, we first need to get target, and then
611 # Order here matters, we first need to get target, and then
612 # the source
612 # the source
613 target_commit = commits_source_repo.get_commit(
613 target_commit = commits_source_repo.get_commit(
614 commit_id=safe_str(target_ref_id))
614 commit_id=safe_str(target_ref_id))
615
615
616 source_commit = commits_source_repo.get_commit(
616 source_commit = commits_source_repo.get_commit(
617 commit_id=safe_str(source_ref_id))
617 commit_id=safe_str(source_ref_id))
618 except CommitDoesNotExistError:
618 except CommitDoesNotExistError:
619 log.warning(
619 log.warning(
620 'Failed to get commit from `{}` repo'.format(
620 'Failed to get commit from `{}` repo'.format(
621 commits_source_repo), exc_info=True)
621 commits_source_repo), exc_info=True)
622 except RepositoryRequirementError:
622 except RepositoryRequirementError:
623 log.warning(
623 log.warning(
624 'Failed to get all required data from repo', exc_info=True)
624 'Failed to get all required data from repo', exc_info=True)
625 missing_requirements = True
625 missing_requirements = True
626 ancestor_commit = None
626 ancestor_commit = None
627 try:
627 try:
628 ancestor_id = source_scm.get_common_ancestor(
628 ancestor_id = source_scm.get_common_ancestor(
629 source_commit.raw_id, target_commit.raw_id, target_scm)
629 source_commit.raw_id, target_commit.raw_id, target_scm)
630 ancestor_commit = source_scm.get_commit(ancestor_id)
630 ancestor_commit = source_scm.get_commit(ancestor_id)
631 except Exception:
631 except Exception:
632 ancestor_commit = None
632 ancestor_commit = None
633 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
633 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
634
634
635 def assure_not_empty_repo(self):
635 def assure_not_empty_repo(self):
636 _ = self.request.translate
636 _ = self.request.translate
637
637
638 try:
638 try:
639 self.db_repo.scm_instance().get_commit()
639 self.db_repo.scm_instance().get_commit()
640 except EmptyRepositoryError:
640 except EmptyRepositoryError:
641 h.flash(h.literal(_('There are no commits yet')),
641 h.flash(h.literal(_('There are no commits yet')),
642 category='warning')
642 category='warning')
643 raise HTTPFound(
643 raise HTTPFound(
644 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
644 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
645
645
646 @LoginRequired()
646 @LoginRequired()
647 @NotAnonymous()
647 @NotAnonymous()
648 @HasRepoPermissionAnyDecorator(
648 @HasRepoPermissionAnyDecorator(
649 'repository.read', 'repository.write', 'repository.admin')
649 'repository.read', 'repository.write', 'repository.admin')
650 @view_config(
650 @view_config(
651 route_name='pullrequest_new', request_method='GET',
651 route_name='pullrequest_new', request_method='GET',
652 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
652 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
653 def pull_request_new(self):
653 def pull_request_new(self):
654 _ = self.request.translate
654 _ = self.request.translate
655 c = self.load_default_context()
655 c = self.load_default_context()
656
656
657 self.assure_not_empty_repo()
657 self.assure_not_empty_repo()
658 source_repo = self.db_repo
658 source_repo = self.db_repo
659
659
660 commit_id = self.request.GET.get('commit')
660 commit_id = self.request.GET.get('commit')
661 branch_ref = self.request.GET.get('branch')
661 branch_ref = self.request.GET.get('branch')
662 bookmark_ref = self.request.GET.get('bookmark')
662 bookmark_ref = self.request.GET.get('bookmark')
663
663
664 try:
664 try:
665 source_repo_data = PullRequestModel().generate_repo_data(
665 source_repo_data = PullRequestModel().generate_repo_data(
666 source_repo, commit_id=commit_id,
666 source_repo, commit_id=commit_id,
667 branch=branch_ref, bookmark=bookmark_ref,
667 branch=branch_ref, bookmark=bookmark_ref,
668 translator=self.request.translate)
668 translator=self.request.translate)
669 except CommitDoesNotExistError as e:
669 except CommitDoesNotExistError as e:
670 log.exception(e)
670 log.exception(e)
671 h.flash(_('Commit does not exist'), 'error')
671 h.flash(_('Commit does not exist'), 'error')
672 raise HTTPFound(
672 raise HTTPFound(
673 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
673 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
674
674
675 default_target_repo = source_repo
675 default_target_repo = source_repo
676
676
677 if source_repo.parent:
677 if source_repo.parent:
678 parent_vcs_obj = source_repo.parent.scm_instance()
678 parent_vcs_obj = source_repo.parent.scm_instance()
679 if parent_vcs_obj and not parent_vcs_obj.is_empty():
679 if parent_vcs_obj and not parent_vcs_obj.is_empty():
680 # change default if we have a parent repo
680 # change default if we have a parent repo
681 default_target_repo = source_repo.parent
681 default_target_repo = source_repo.parent
682
682
683 target_repo_data = PullRequestModel().generate_repo_data(
683 target_repo_data = PullRequestModel().generate_repo_data(
684 default_target_repo, translator=self.request.translate)
684 default_target_repo, translator=self.request.translate)
685
685
686 selected_source_ref = source_repo_data['refs']['selected_ref']
686 selected_source_ref = source_repo_data['refs']['selected_ref']
687 title_source_ref = ''
687 title_source_ref = ''
688 if selected_source_ref:
688 if selected_source_ref:
689 title_source_ref = selected_source_ref.split(':', 2)[1]
689 title_source_ref = selected_source_ref.split(':', 2)[1]
690 c.default_title = PullRequestModel().generate_pullrequest_title(
690 c.default_title = PullRequestModel().generate_pullrequest_title(
691 source=source_repo.repo_name,
691 source=source_repo.repo_name,
692 source_ref=title_source_ref,
692 source_ref=title_source_ref,
693 target=default_target_repo.repo_name
693 target=default_target_repo.repo_name
694 )
694 )
695
695
696 c.default_repo_data = {
696 c.default_repo_data = {
697 'source_repo_name': source_repo.repo_name,
697 'source_repo_name': source_repo.repo_name,
698 'source_refs_json': json.dumps(source_repo_data),
698 'source_refs_json': json.dumps(source_repo_data),
699 'target_repo_name': default_target_repo.repo_name,
699 'target_repo_name': default_target_repo.repo_name,
700 'target_refs_json': json.dumps(target_repo_data),
700 'target_refs_json': json.dumps(target_repo_data),
701 }
701 }
702 c.default_source_ref = selected_source_ref
702 c.default_source_ref = selected_source_ref
703
703
704 return self._get_template_context(c)
704 return self._get_template_context(c)
705
705
706 @LoginRequired()
706 @LoginRequired()
707 @NotAnonymous()
707 @NotAnonymous()
708 @HasRepoPermissionAnyDecorator(
708 @HasRepoPermissionAnyDecorator(
709 'repository.read', 'repository.write', 'repository.admin')
709 'repository.read', 'repository.write', 'repository.admin')
710 @view_config(
710 @view_config(
711 route_name='pullrequest_repo_refs', request_method='GET',
711 route_name='pullrequest_repo_refs', request_method='GET',
712 renderer='json_ext', xhr=True)
712 renderer='json_ext', xhr=True)
713 def pull_request_repo_refs(self):
713 def pull_request_repo_refs(self):
714 self.load_default_context()
714 self.load_default_context()
715 target_repo_name = self.request.matchdict['target_repo_name']
715 target_repo_name = self.request.matchdict['target_repo_name']
716 repo = Repository.get_by_repo_name(target_repo_name)
716 repo = Repository.get_by_repo_name(target_repo_name)
717 if not repo:
717 if not repo:
718 raise HTTPNotFound()
718 raise HTTPNotFound()
719
719
720 target_perm = HasRepoPermissionAny(
720 target_perm = HasRepoPermissionAny(
721 'repository.read', 'repository.write', 'repository.admin')(
721 'repository.read', 'repository.write', 'repository.admin')(
722 target_repo_name)
722 target_repo_name)
723 if not target_perm:
723 if not target_perm:
724 raise HTTPNotFound()
724 raise HTTPNotFound()
725
725
726 return PullRequestModel().generate_repo_data(
726 return PullRequestModel().generate_repo_data(
727 repo, translator=self.request.translate)
727 repo, translator=self.request.translate)
728
728
729 @LoginRequired()
729 @LoginRequired()
730 @NotAnonymous()
730 @NotAnonymous()
731 @HasRepoPermissionAnyDecorator(
731 @HasRepoPermissionAnyDecorator(
732 'repository.read', 'repository.write', 'repository.admin')
732 'repository.read', 'repository.write', 'repository.admin')
733 @view_config(
733 @view_config(
734 route_name='pullrequest_repo_destinations', request_method='GET',
734 route_name='pullrequest_repo_destinations', request_method='GET',
735 renderer='json_ext', xhr=True)
735 renderer='json_ext', xhr=True)
736 def pull_request_repo_destinations(self):
736 def pull_request_repo_destinations(self):
737 _ = self.request.translate
737 _ = self.request.translate
738 filter_query = self.request.GET.get('query')
738 filter_query = self.request.GET.get('query')
739
739
740 query = Repository.query() \
740 query = Repository.query() \
741 .order_by(func.length(Repository.repo_name)) \
741 .order_by(func.length(Repository.repo_name)) \
742 .filter(
742 .filter(
743 or_(Repository.repo_name == self.db_repo.repo_name,
743 or_(Repository.repo_name == self.db_repo.repo_name,
744 Repository.fork_id == self.db_repo.repo_id))
744 Repository.fork_id == self.db_repo.repo_id))
745
745
746 if filter_query:
746 if filter_query:
747 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
747 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
748 query = query.filter(
748 query = query.filter(
749 Repository.repo_name.ilike(ilike_expression))
749 Repository.repo_name.ilike(ilike_expression))
750
750
751 add_parent = False
751 add_parent = False
752 if self.db_repo.parent:
752 if self.db_repo.parent:
753 if filter_query in self.db_repo.parent.repo_name:
753 if filter_query in self.db_repo.parent.repo_name:
754 parent_vcs_obj = self.db_repo.parent.scm_instance()
754 parent_vcs_obj = self.db_repo.parent.scm_instance()
755 if parent_vcs_obj and not parent_vcs_obj.is_empty():
755 if parent_vcs_obj and not parent_vcs_obj.is_empty():
756 add_parent = True
756 add_parent = True
757
757
758 limit = 20 - 1 if add_parent else 20
758 limit = 20 - 1 if add_parent else 20
759 all_repos = query.limit(limit).all()
759 all_repos = query.limit(limit).all()
760 if add_parent:
760 if add_parent:
761 all_repos += [self.db_repo.parent]
761 all_repos += [self.db_repo.parent]
762
762
763 repos = []
763 repos = []
764 for obj in ScmModel().get_repos(all_repos):
764 for obj in ScmModel().get_repos(all_repos):
765 repos.append({
765 repos.append({
766 'id': obj['name'],
766 'id': obj['name'],
767 'text': obj['name'],
767 'text': obj['name'],
768 'type': 'repo',
768 'type': 'repo',
769 'obj': obj['dbrepo']
769 'obj': obj['dbrepo']
770 })
770 })
771
771
772 data = {
772 data = {
773 'more': False,
773 'more': False,
774 'results': [{
774 'results': [{
775 'text': _('Repositories'),
775 'text': _('Repositories'),
776 'children': repos
776 'children': repos
777 }] if repos else []
777 }] if repos else []
778 }
778 }
779 return data
779 return data
780
780
781 @LoginRequired()
781 @LoginRequired()
782 @NotAnonymous()
782 @NotAnonymous()
783 @HasRepoPermissionAnyDecorator(
783 @HasRepoPermissionAnyDecorator(
784 'repository.read', 'repository.write', 'repository.admin')
784 'repository.read', 'repository.write', 'repository.admin')
785 @CSRFRequired()
785 @CSRFRequired()
786 @view_config(
786 @view_config(
787 route_name='pullrequest_create', request_method='POST',
787 route_name='pullrequest_create', request_method='POST',
788 renderer=None)
788 renderer=None)
789 def pull_request_create(self):
789 def pull_request_create(self):
790 _ = self.request.translate
790 _ = self.request.translate
791 self.assure_not_empty_repo()
791 self.assure_not_empty_repo()
792 self.load_default_context()
792 self.load_default_context()
793
793
794 controls = peppercorn.parse(self.request.POST.items())
794 controls = peppercorn.parse(self.request.POST.items())
795
795
796 try:
796 try:
797 form = PullRequestForm(
797 form = PullRequestForm(
798 self.request.translate, self.db_repo.repo_id)()
798 self.request.translate, self.db_repo.repo_id)()
799 _form = form.to_python(controls)
799 _form = form.to_python(controls)
800 except formencode.Invalid as errors:
800 except formencode.Invalid as errors:
801 if errors.error_dict.get('revisions'):
801 if errors.error_dict.get('revisions'):
802 msg = 'Revisions: %s' % errors.error_dict['revisions']
802 msg = 'Revisions: %s' % errors.error_dict['revisions']
803 elif errors.error_dict.get('pullrequest_title'):
803 elif errors.error_dict.get('pullrequest_title'):
804 msg = errors.error_dict.get('pullrequest_title')
804 msg = errors.error_dict.get('pullrequest_title')
805 else:
805 else:
806 msg = _('Error creating pull request: {}').format(errors)
806 msg = _('Error creating pull request: {}').format(errors)
807 log.exception(msg)
807 log.exception(msg)
808 h.flash(msg, 'error')
808 h.flash(msg, 'error')
809
809
810 # would rather just go back to form ...
810 # would rather just go back to form ...
811 raise HTTPFound(
811 raise HTTPFound(
812 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
812 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
813
813
814 source_repo = _form['source_repo']
814 source_repo = _form['source_repo']
815 source_ref = _form['source_ref']
815 source_ref = _form['source_ref']
816 target_repo = _form['target_repo']
816 target_repo = _form['target_repo']
817 target_ref = _form['target_ref']
817 target_ref = _form['target_ref']
818 commit_ids = _form['revisions'][::-1]
818 commit_ids = _form['revisions'][::-1]
819
819
820 # find the ancestor for this pr
820 # find the ancestor for this pr
821 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
821 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
822 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
822 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
823
823
824 # re-check permissions again here
824 # re-check permissions again here
825 # source_repo we must have read permissions
825 # source_repo we must have read permissions
826
826
827 source_perm = HasRepoPermissionAny(
827 source_perm = HasRepoPermissionAny(
828 'repository.read',
828 'repository.read',
829 'repository.write', 'repository.admin')(source_db_repo.repo_name)
829 'repository.write', 'repository.admin')(source_db_repo.repo_name)
830 if not source_perm:
830 if not source_perm:
831 msg = _('Not Enough permissions to source repo `{}`.'.format(
831 msg = _('Not Enough permissions to source repo `{}`.'.format(
832 source_db_repo.repo_name))
832 source_db_repo.repo_name))
833 h.flash(msg, category='error')
833 h.flash(msg, category='error')
834 # copy the args back to redirect
834 # copy the args back to redirect
835 org_query = self.request.GET.mixed()
835 org_query = self.request.GET.mixed()
836 raise HTTPFound(
836 raise HTTPFound(
837 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
837 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
838 _query=org_query))
838 _query=org_query))
839
839
840 # target repo we must have read permissions, and also later on
840 # target repo we must have read permissions, and also later on
841 # we want to check branch permissions here
841 # we want to check branch permissions here
842 target_perm = HasRepoPermissionAny(
842 target_perm = HasRepoPermissionAny(
843 'repository.read',
843 'repository.read',
844 'repository.write', 'repository.admin')(target_db_repo.repo_name)
844 'repository.write', 'repository.admin')(target_db_repo.repo_name)
845 if not target_perm:
845 if not target_perm:
846 msg = _('Not Enough permissions to target repo `{}`.'.format(
846 msg = _('Not Enough permissions to target repo `{}`.'.format(
847 target_db_repo.repo_name))
847 target_db_repo.repo_name))
848 h.flash(msg, category='error')
848 h.flash(msg, category='error')
849 # copy the args back to redirect
849 # copy the args back to redirect
850 org_query = self.request.GET.mixed()
850 org_query = self.request.GET.mixed()
851 raise HTTPFound(
851 raise HTTPFound(
852 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
852 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
853 _query=org_query))
853 _query=org_query))
854
854
855 source_scm = source_db_repo.scm_instance()
855 source_scm = source_db_repo.scm_instance()
856 target_scm = target_db_repo.scm_instance()
856 target_scm = target_db_repo.scm_instance()
857
857
858 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
858 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
859 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
859 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
860
860
861 ancestor = source_scm.get_common_ancestor(
861 ancestor = source_scm.get_common_ancestor(
862 source_commit.raw_id, target_commit.raw_id, target_scm)
862 source_commit.raw_id, target_commit.raw_id, target_scm)
863
863
864 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
864 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
865 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
865 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
866
866
867 pullrequest_title = _form['pullrequest_title']
867 pullrequest_title = _form['pullrequest_title']
868 title_source_ref = source_ref.split(':', 2)[1]
868 title_source_ref = source_ref.split(':', 2)[1]
869 if not pullrequest_title:
869 if not pullrequest_title:
870 pullrequest_title = PullRequestModel().generate_pullrequest_title(
870 pullrequest_title = PullRequestModel().generate_pullrequest_title(
871 source=source_repo,
871 source=source_repo,
872 source_ref=title_source_ref,
872 source_ref=title_source_ref,
873 target=target_repo
873 target=target_repo
874 )
874 )
875
875
876 description = _form['pullrequest_desc']
876 description = _form['pullrequest_desc']
877
877
878 get_default_reviewers_data, validate_default_reviewers = \
878 get_default_reviewers_data, validate_default_reviewers = \
879 PullRequestModel().get_reviewer_functions()
879 PullRequestModel().get_reviewer_functions()
880
880
881 # recalculate reviewers logic, to make sure we can validate this
881 # recalculate reviewers logic, to make sure we can validate this
882 reviewer_rules = get_default_reviewers_data(
882 reviewer_rules = get_default_reviewers_data(
883 self._rhodecode_db_user, source_db_repo,
883 self._rhodecode_db_user, source_db_repo,
884 source_commit, target_db_repo, target_commit)
884 source_commit, target_db_repo, target_commit)
885
885
886 given_reviewers = _form['review_members']
886 given_reviewers = _form['review_members']
887 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
887 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
888
888
889 try:
889 try:
890 pull_request = PullRequestModel().create(
890 pull_request = PullRequestModel().create(
891 self._rhodecode_user.user_id, source_repo, source_ref,
891 self._rhodecode_user.user_id, source_repo, source_ref,
892 target_repo, target_ref, commit_ids, reviewers,
892 target_repo, target_ref, commit_ids, reviewers,
893 pullrequest_title, description, reviewer_rules
893 pullrequest_title, description, reviewer_rules
894 )
894 )
895 Session().commit()
895 Session().commit()
896
896
897 h.flash(_('Successfully opened new pull request'),
897 h.flash(_('Successfully opened new pull request'),
898 category='success')
898 category='success')
899 except Exception:
899 except Exception:
900 msg = _('Error occurred during creation of this pull request.')
900 msg = _('Error occurred during creation of this pull request.')
901 log.exception(msg)
901 log.exception(msg)
902 h.flash(msg, category='error')
902 h.flash(msg, category='error')
903
903
904 # copy the args back to redirect
904 # copy the args back to redirect
905 org_query = self.request.GET.mixed()
905 org_query = self.request.GET.mixed()
906 raise HTTPFound(
906 raise HTTPFound(
907 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
907 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
908 _query=org_query))
908 _query=org_query))
909
909
910 raise HTTPFound(
910 raise HTTPFound(
911 h.route_path('pullrequest_show', repo_name=target_repo,
911 h.route_path('pullrequest_show', repo_name=target_repo,
912 pull_request_id=pull_request.pull_request_id))
912 pull_request_id=pull_request.pull_request_id))
913
913
914 @LoginRequired()
914 @LoginRequired()
915 @NotAnonymous()
915 @NotAnonymous()
916 @HasRepoPermissionAnyDecorator(
916 @HasRepoPermissionAnyDecorator(
917 'repository.read', 'repository.write', 'repository.admin')
917 'repository.read', 'repository.write', 'repository.admin')
918 @CSRFRequired()
918 @CSRFRequired()
919 @view_config(
919 @view_config(
920 route_name='pullrequest_update', request_method='POST',
920 route_name='pullrequest_update', request_method='POST',
921 renderer='json_ext')
921 renderer='json_ext')
922 def pull_request_update(self):
922 def pull_request_update(self):
923 pull_request = PullRequest.get_or_404(
923 pull_request = PullRequest.get_or_404(
924 self.request.matchdict['pull_request_id'])
924 self.request.matchdict['pull_request_id'])
925 _ = self.request.translate
925 _ = self.request.translate
926
926
927 self.load_default_context()
927 self.load_default_context()
928
928
929 if pull_request.is_closed():
929 if pull_request.is_closed():
930 log.debug('update: forbidden because pull request is closed')
930 log.debug('update: forbidden because pull request is closed')
931 msg = _(u'Cannot update closed pull requests.')
931 msg = _(u'Cannot update closed pull requests.')
932 h.flash(msg, category='error')
932 h.flash(msg, category='error')
933 return True
933 return True
934
934
935 # only owner or admin can update it
935 # only owner or admin can update it
936 allowed_to_update = PullRequestModel().check_user_update(
936 allowed_to_update = PullRequestModel().check_user_update(
937 pull_request, self._rhodecode_user)
937 pull_request, self._rhodecode_user)
938 if allowed_to_update:
938 if allowed_to_update:
939 controls = peppercorn.parse(self.request.POST.items())
939 controls = peppercorn.parse(self.request.POST.items())
940
940
941 if 'review_members' in controls:
941 if 'review_members' in controls:
942 self._update_reviewers(
942 self._update_reviewers(
943 pull_request, controls['review_members'],
943 pull_request, controls['review_members'],
944 pull_request.reviewer_data)
944 pull_request.reviewer_data)
945 elif str2bool(self.request.POST.get('update_commits', 'false')):
945 elif str2bool(self.request.POST.get('update_commits', 'false')):
946 self._update_commits(pull_request)
946 self._update_commits(pull_request)
947 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
947 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
948 self._edit_pull_request(pull_request)
948 self._edit_pull_request(pull_request)
949 else:
949 else:
950 raise HTTPBadRequest()
950 raise HTTPBadRequest()
951 return True
951 return True
952 raise HTTPForbidden()
952 raise HTTPForbidden()
953
953
954 def _edit_pull_request(self, pull_request):
954 def _edit_pull_request(self, pull_request):
955 _ = self.request.translate
955 _ = self.request.translate
956 try:
956 try:
957 PullRequestModel().edit(
957 PullRequestModel().edit(
958 pull_request, self.request.POST.get('title'),
958 pull_request, self.request.POST.get('title'),
959 self.request.POST.get('description'), self._rhodecode_user)
959 self.request.POST.get('description'), self._rhodecode_user)
960 except ValueError:
960 except ValueError:
961 msg = _(u'Cannot update closed pull requests.')
961 msg = _(u'Cannot update closed pull requests.')
962 h.flash(msg, category='error')
962 h.flash(msg, category='error')
963 return
963 return
964 else:
964 else:
965 Session().commit()
965 Session().commit()
966
966
967 msg = _(u'Pull request title & description updated.')
967 msg = _(u'Pull request title & description updated.')
968 h.flash(msg, category='success')
968 h.flash(msg, category='success')
969 return
969 return
970
970
971 def _update_commits(self, pull_request):
971 def _update_commits(self, pull_request):
972 _ = self.request.translate
972 _ = self.request.translate
973 resp = PullRequestModel().update_commits(pull_request)
973 resp = PullRequestModel().update_commits(pull_request)
974
974
975 if resp.executed:
975 if resp.executed:
976
976
977 if resp.target_changed and resp.source_changed:
977 if resp.target_changed and resp.source_changed:
978 changed = 'target and source repositories'
978 changed = 'target and source repositories'
979 elif resp.target_changed and not resp.source_changed:
979 elif resp.target_changed and not resp.source_changed:
980 changed = 'target repository'
980 changed = 'target repository'
981 elif not resp.target_changed and resp.source_changed:
981 elif not resp.target_changed and resp.source_changed:
982 changed = 'source repository'
982 changed = 'source repository'
983 else:
983 else:
984 changed = 'nothing'
984 changed = 'nothing'
985
985
986 msg = _(
986 msg = _(
987 u'Pull request updated to "{source_commit_id}" with '
987 u'Pull request updated to "{source_commit_id}" with '
988 u'{count_added} added, {count_removed} removed commits. '
988 u'{count_added} added, {count_removed} removed commits. '
989 u'Source of changes: {change_source}')
989 u'Source of changes: {change_source}')
990 msg = msg.format(
990 msg = msg.format(
991 source_commit_id=pull_request.source_ref_parts.commit_id,
991 source_commit_id=pull_request.source_ref_parts.commit_id,
992 count_added=len(resp.changes.added),
992 count_added=len(resp.changes.added),
993 count_removed=len(resp.changes.removed),
993 count_removed=len(resp.changes.removed),
994 change_source=changed)
994 change_source=changed)
995 h.flash(msg, category='success')
995 h.flash(msg, category='success')
996
996
997 channel = '/repo${}$/pr/{}'.format(
997 channel = '/repo${}$/pr/{}'.format(
998 pull_request.target_repo.repo_name,
998 pull_request.target_repo.repo_name,
999 pull_request.pull_request_id)
999 pull_request.pull_request_id)
1000 message = msg + (
1000 message = msg + (
1001 ' - <a onclick="window.location.reload()">'
1001 ' - <a onclick="window.location.reload()">'
1002 '<strong>{}</strong></a>'.format(_('Reload page')))
1002 '<strong>{}</strong></a>'.format(_('Reload page')))
1003 channelstream.post_message(
1003 channelstream.post_message(
1004 channel, message, self._rhodecode_user.username,
1004 channel, message, self._rhodecode_user.username,
1005 registry=self.request.registry)
1005 registry=self.request.registry)
1006 else:
1006 else:
1007 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1007 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1008 warning_reasons = [
1008 warning_reasons = [
1009 UpdateFailureReason.NO_CHANGE,
1009 UpdateFailureReason.NO_CHANGE,
1010 UpdateFailureReason.WRONG_REF_TYPE,
1010 UpdateFailureReason.WRONG_REF_TYPE,
1011 ]
1011 ]
1012 category = 'warning' if resp.reason in warning_reasons else 'error'
1012 category = 'warning' if resp.reason in warning_reasons else 'error'
1013 h.flash(msg, category=category)
1013 h.flash(msg, category=category)
1014
1014
1015 @LoginRequired()
1015 @LoginRequired()
1016 @NotAnonymous()
1016 @NotAnonymous()
1017 @HasRepoPermissionAnyDecorator(
1017 @HasRepoPermissionAnyDecorator(
1018 'repository.read', 'repository.write', 'repository.admin')
1018 'repository.read', 'repository.write', 'repository.admin')
1019 @CSRFRequired()
1019 @CSRFRequired()
1020 @view_config(
1020 @view_config(
1021 route_name='pullrequest_merge', request_method='POST',
1021 route_name='pullrequest_merge', request_method='POST',
1022 renderer='json_ext')
1022 renderer='json_ext')
1023 def pull_request_merge(self):
1023 def pull_request_merge(self):
1024 """
1024 """
1025 Merge will perform a server-side merge of the specified
1025 Merge will perform a server-side merge of the specified
1026 pull request, if the pull request is approved and mergeable.
1026 pull request, if the pull request is approved and mergeable.
1027 After successful merging, the pull request is automatically
1027 After successful merging, the pull request is automatically
1028 closed, with a relevant comment.
1028 closed, with a relevant comment.
1029 """
1029 """
1030 pull_request = PullRequest.get_or_404(
1030 pull_request = PullRequest.get_or_404(
1031 self.request.matchdict['pull_request_id'])
1031 self.request.matchdict['pull_request_id'])
1032
1032
1033 self.load_default_context()
1033 self.load_default_context()
1034 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1034 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1035 translator=self.request.translate)
1035 translator=self.request.translate)
1036 merge_possible = not check.failed
1036 merge_possible = not check.failed
1037
1037
1038 for err_type, error_msg in check.errors:
1038 for err_type, error_msg in check.errors:
1039 h.flash(error_msg, category=err_type)
1039 h.flash(error_msg, category=err_type)
1040
1040
1041 if merge_possible:
1041 if merge_possible:
1042 log.debug("Pre-conditions checked, trying to merge.")
1042 log.debug("Pre-conditions checked, trying to merge.")
1043 extras = vcs_operation_context(
1043 extras = vcs_operation_context(
1044 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1044 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1045 username=self._rhodecode_db_user.username, action='push',
1045 username=self._rhodecode_db_user.username, action='push',
1046 scm=pull_request.target_repo.repo_type)
1046 scm=pull_request.target_repo.repo_type)
1047 self._merge_pull_request(
1047 self._merge_pull_request(
1048 pull_request, self._rhodecode_db_user, extras)
1048 pull_request, self._rhodecode_db_user, extras)
1049 else:
1049 else:
1050 log.debug("Pre-conditions failed, NOT merging.")
1050 log.debug("Pre-conditions failed, NOT merging.")
1051
1051
1052 raise HTTPFound(
1052 raise HTTPFound(
1053 h.route_path('pullrequest_show',
1053 h.route_path('pullrequest_show',
1054 repo_name=pull_request.target_repo.repo_name,
1054 repo_name=pull_request.target_repo.repo_name,
1055 pull_request_id=pull_request.pull_request_id))
1055 pull_request_id=pull_request.pull_request_id))
1056
1056
1057 def _merge_pull_request(self, pull_request, user, extras):
1057 def _merge_pull_request(self, pull_request, user, extras):
1058 _ = self.request.translate
1058 _ = self.request.translate
1059 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1059 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1060
1060
1061 if merge_resp.executed:
1061 if merge_resp.executed:
1062 log.debug("The merge was successful, closing the pull request.")
1062 log.debug("The merge was successful, closing the pull request.")
1063 PullRequestModel().close_pull_request(
1063 PullRequestModel().close_pull_request(
1064 pull_request.pull_request_id, user)
1064 pull_request.pull_request_id, user)
1065 Session().commit()
1065 Session().commit()
1066 msg = _('Pull request was successfully merged and closed.')
1066 msg = _('Pull request was successfully merged and closed.')
1067 h.flash(msg, category='success')
1067 h.flash(msg, category='success')
1068 else:
1068 else:
1069 log.debug(
1069 log.debug(
1070 "The merge was not successful. Merge response: %s",
1070 "The merge was not successful. Merge response: %s",
1071 merge_resp)
1071 merge_resp)
1072 msg = PullRequestModel().merge_status_message(
1072 msg = PullRequestModel().merge_status_message(
1073 merge_resp.failure_reason)
1073 merge_resp.failure_reason)
1074 h.flash(msg, category='error')
1074 h.flash(msg, category='error')
1075
1075
1076 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1076 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1077 _ = self.request.translate
1077 _ = self.request.translate
1078 get_default_reviewers_data, validate_default_reviewers = \
1078 get_default_reviewers_data, validate_default_reviewers = \
1079 PullRequestModel().get_reviewer_functions()
1079 PullRequestModel().get_reviewer_functions()
1080
1080
1081 try:
1081 try:
1082 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1082 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1083 except ValueError as e:
1083 except ValueError as e:
1084 log.error('Reviewers Validation: {}'.format(e))
1084 log.error('Reviewers Validation: {}'.format(e))
1085 h.flash(e, category='error')
1085 h.flash(e, category='error')
1086 return
1086 return
1087
1087
1088 PullRequestModel().update_reviewers(
1088 PullRequestModel().update_reviewers(
1089 pull_request, reviewers, self._rhodecode_user)
1089 pull_request, reviewers, self._rhodecode_user)
1090 h.flash(_('Pull request reviewers updated.'), category='success')
1090 h.flash(_('Pull request reviewers updated.'), category='success')
1091 Session().commit()
1091 Session().commit()
1092
1092
1093 @LoginRequired()
1093 @LoginRequired()
1094 @NotAnonymous()
1094 @NotAnonymous()
1095 @HasRepoPermissionAnyDecorator(
1095 @HasRepoPermissionAnyDecorator(
1096 'repository.read', 'repository.write', 'repository.admin')
1096 'repository.read', 'repository.write', 'repository.admin')
1097 @CSRFRequired()
1097 @CSRFRequired()
1098 @view_config(
1098 @view_config(
1099 route_name='pullrequest_delete', request_method='POST',
1099 route_name='pullrequest_delete', request_method='POST',
1100 renderer='json_ext')
1100 renderer='json_ext')
1101 def pull_request_delete(self):
1101 def pull_request_delete(self):
1102 _ = self.request.translate
1102 _ = self.request.translate
1103
1103
1104 pull_request = PullRequest.get_or_404(
1104 pull_request = PullRequest.get_or_404(
1105 self.request.matchdict['pull_request_id'])
1105 self.request.matchdict['pull_request_id'])
1106 self.load_default_context()
1106 self.load_default_context()
1107
1107
1108 pr_closed = pull_request.is_closed()
1108 pr_closed = pull_request.is_closed()
1109 allowed_to_delete = PullRequestModel().check_user_delete(
1109 allowed_to_delete = PullRequestModel().check_user_delete(
1110 pull_request, self._rhodecode_user) and not pr_closed
1110 pull_request, self._rhodecode_user) and not pr_closed
1111
1111
1112 # only owner can delete it !
1112 # only owner can delete it !
1113 if allowed_to_delete:
1113 if allowed_to_delete:
1114 PullRequestModel().delete(pull_request, self._rhodecode_user)
1114 PullRequestModel().delete(pull_request, self._rhodecode_user)
1115 Session().commit()
1115 Session().commit()
1116 h.flash(_('Successfully deleted pull request'),
1116 h.flash(_('Successfully deleted pull request'),
1117 category='success')
1117 category='success')
1118 raise HTTPFound(h.route_path('pullrequest_show_all',
1118 raise HTTPFound(h.route_path('pullrequest_show_all',
1119 repo_name=self.db_repo_name))
1119 repo_name=self.db_repo_name))
1120
1120
1121 log.warning('user %s tried to delete pull request without access',
1121 log.warning('user %s tried to delete pull request without access',
1122 self._rhodecode_user)
1122 self._rhodecode_user)
1123 raise HTTPNotFound()
1123 raise HTTPNotFound()
1124
1124
1125 @LoginRequired()
1125 @LoginRequired()
1126 @NotAnonymous()
1126 @NotAnonymous()
1127 @HasRepoPermissionAnyDecorator(
1127 @HasRepoPermissionAnyDecorator(
1128 'repository.read', 'repository.write', 'repository.admin')
1128 'repository.read', 'repository.write', 'repository.admin')
1129 @CSRFRequired()
1129 @CSRFRequired()
1130 @view_config(
1130 @view_config(
1131 route_name='pullrequest_comment_create', request_method='POST',
1131 route_name='pullrequest_comment_create', request_method='POST',
1132 renderer='json_ext')
1132 renderer='json_ext')
1133 def pull_request_comment_create(self):
1133 def pull_request_comment_create(self):
1134 _ = self.request.translate
1134 _ = self.request.translate
1135
1135
1136 pull_request = PullRequest.get_or_404(
1136 pull_request = PullRequest.get_or_404(
1137 self.request.matchdict['pull_request_id'])
1137 self.request.matchdict['pull_request_id'])
1138 pull_request_id = pull_request.pull_request_id
1138 pull_request_id = pull_request.pull_request_id
1139
1139
1140 if pull_request.is_closed():
1140 if pull_request.is_closed():
1141 log.debug('comment: forbidden because pull request is closed')
1141 log.debug('comment: forbidden because pull request is closed')
1142 raise HTTPForbidden()
1142 raise HTTPForbidden()
1143
1143
1144 allowed_to_comment = PullRequestModel().check_user_comment(
1144 allowed_to_comment = PullRequestModel().check_user_comment(
1145 pull_request, self._rhodecode_user)
1145 pull_request, self._rhodecode_user)
1146 if not allowed_to_comment:
1146 if not allowed_to_comment:
1147 log.debug(
1147 log.debug(
1148 'comment: forbidden because pull request is from forbidden repo')
1148 'comment: forbidden because pull request is from forbidden repo')
1149 raise HTTPForbidden()
1149 raise HTTPForbidden()
1150
1150
1151 c = self.load_default_context()
1151 c = self.load_default_context()
1152
1152
1153 status = self.request.POST.get('changeset_status', None)
1153 status = self.request.POST.get('changeset_status', None)
1154 text = self.request.POST.get('text')
1154 text = self.request.POST.get('text')
1155 comment_type = self.request.POST.get('comment_type')
1155 comment_type = self.request.POST.get('comment_type')
1156 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1156 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1157 close_pull_request = self.request.POST.get('close_pull_request')
1157 close_pull_request = self.request.POST.get('close_pull_request')
1158
1158
1159 # the logic here should work like following, if we submit close
1159 # the logic here should work like following, if we submit close
1160 # pr comment, use `close_pull_request_with_comment` function
1160 # pr comment, use `close_pull_request_with_comment` function
1161 # else handle regular comment logic
1161 # else handle regular comment logic
1162
1162
1163 if close_pull_request:
1163 if close_pull_request:
1164 # only owner or admin or person with write permissions
1164 # only owner or admin or person with write permissions
1165 allowed_to_close = PullRequestModel().check_user_update(
1165 allowed_to_close = PullRequestModel().check_user_update(
1166 pull_request, self._rhodecode_user)
1166 pull_request, self._rhodecode_user)
1167 if not allowed_to_close:
1167 if not allowed_to_close:
1168 log.debug('comment: forbidden because not allowed to close '
1168 log.debug('comment: forbidden because not allowed to close '
1169 'pull request %s', pull_request_id)
1169 'pull request %s', pull_request_id)
1170 raise HTTPForbidden()
1170 raise HTTPForbidden()
1171 comment, status = PullRequestModel().close_pull_request_with_comment(
1171 comment, status = PullRequestModel().close_pull_request_with_comment(
1172 pull_request, self._rhodecode_user, self.db_repo, message=text)
1172 pull_request, self._rhodecode_user, self.db_repo, message=text)
1173 Session().flush()
1173 Session().flush()
1174 events.trigger(
1174 events.trigger(
1175 events.PullRequestCommentEvent(pull_request, comment))
1175 events.PullRequestCommentEvent(pull_request, comment))
1176
1176
1177 else:
1177 else:
1178 # regular comment case, could be inline, or one with status.
1178 # regular comment case, could be inline, or one with status.
1179 # for that one we check also permissions
1179 # for that one we check also permissions
1180
1180
1181 allowed_to_change_status = PullRequestModel().check_user_change_status(
1181 allowed_to_change_status = PullRequestModel().check_user_change_status(
1182 pull_request, self._rhodecode_user)
1182 pull_request, self._rhodecode_user)
1183
1183
1184 if status and allowed_to_change_status:
1184 if status and allowed_to_change_status:
1185 message = (_('Status change %(transition_icon)s %(status)s')
1185 message = (_('Status change %(transition_icon)s %(status)s')
1186 % {'transition_icon': '>',
1186 % {'transition_icon': '>',
1187 'status': ChangesetStatus.get_status_lbl(status)})
1187 'status': ChangesetStatus.get_status_lbl(status)})
1188 text = text or message
1188 text = text or message
1189
1189
1190 comment = CommentsModel().create(
1190 comment = CommentsModel().create(
1191 text=text,
1191 text=text,
1192 repo=self.db_repo.repo_id,
1192 repo=self.db_repo.repo_id,
1193 user=self._rhodecode_user.user_id,
1193 user=self._rhodecode_user.user_id,
1194 pull_request=pull_request,
1194 pull_request=pull_request,
1195 f_path=self.request.POST.get('f_path'),
1195 f_path=self.request.POST.get('f_path'),
1196 line_no=self.request.POST.get('line'),
1196 line_no=self.request.POST.get('line'),
1197 status_change=(ChangesetStatus.get_status_lbl(status)
1197 status_change=(ChangesetStatus.get_status_lbl(status)
1198 if status and allowed_to_change_status else None),
1198 if status and allowed_to_change_status else None),
1199 status_change_type=(status
1199 status_change_type=(status
1200 if status and allowed_to_change_status else None),
1200 if status and allowed_to_change_status else None),
1201 comment_type=comment_type,
1201 comment_type=comment_type,
1202 resolves_comment_id=resolves_comment_id
1202 resolves_comment_id=resolves_comment_id
1203 )
1203 )
1204
1204
1205 if allowed_to_change_status:
1205 if allowed_to_change_status:
1206 # calculate old status before we change it
1206 # calculate old status before we change it
1207 old_calculated_status = pull_request.calculated_review_status()
1207 old_calculated_status = pull_request.calculated_review_status()
1208
1208
1209 # get status if set !
1209 # get status if set !
1210 if status:
1210 if status:
1211 ChangesetStatusModel().set_status(
1211 ChangesetStatusModel().set_status(
1212 self.db_repo.repo_id,
1212 self.db_repo.repo_id,
1213 status,
1213 status,
1214 self._rhodecode_user.user_id,
1214 self._rhodecode_user.user_id,
1215 comment,
1215 comment,
1216 pull_request=pull_request
1216 pull_request=pull_request
1217 )
1217 )
1218
1218
1219 Session().flush()
1219 Session().flush()
1220 # this is somehow required to get access to some relationship
1220 # this is somehow required to get access to some relationship
1221 # loaded on comment
1221 # loaded on comment
1222 Session().refresh(comment)
1222 Session().refresh(comment)
1223
1223
1224 events.trigger(
1224 events.trigger(
1225 events.PullRequestCommentEvent(pull_request, comment))
1225 events.PullRequestCommentEvent(pull_request, comment))
1226
1226
1227 # we now calculate the status of pull request, and based on that
1227 # we now calculate the status of pull request, and based on that
1228 # calculation we set the commits status
1228 # calculation we set the commits status
1229 calculated_status = pull_request.calculated_review_status()
1229 calculated_status = pull_request.calculated_review_status()
1230 if old_calculated_status != calculated_status:
1230 if old_calculated_status != calculated_status:
1231 PullRequestModel()._trigger_pull_request_hook(
1231 PullRequestModel()._trigger_pull_request_hook(
1232 pull_request, self._rhodecode_user, 'review_status_change')
1232 pull_request, self._rhodecode_user, 'review_status_change')
1233
1233
1234 Session().commit()
1234 Session().commit()
1235
1235
1236 data = {
1236 data = {
1237 'target_id': h.safeid(h.safe_unicode(
1237 'target_id': h.safeid(h.safe_unicode(
1238 self.request.POST.get('f_path'))),
1238 self.request.POST.get('f_path'))),
1239 }
1239 }
1240 if comment:
1240 if comment:
1241 c.co = comment
1241 c.co = comment
1242 rendered_comment = render(
1242 rendered_comment = render(
1243 'rhodecode:templates/changeset/changeset_comment_block.mako',
1243 'rhodecode:templates/changeset/changeset_comment_block.mako',
1244 self._get_template_context(c), self.request)
1244 self._get_template_context(c), self.request)
1245
1245
1246 data.update(comment.get_dict())
1246 data.update(comment.get_dict())
1247 data.update({'rendered_text': rendered_comment})
1247 data.update({'rendered_text': rendered_comment})
1248
1248
1249 return data
1249 return data
1250
1250
1251 @LoginRequired()
1251 @LoginRequired()
1252 @NotAnonymous()
1252 @NotAnonymous()
1253 @HasRepoPermissionAnyDecorator(
1253 @HasRepoPermissionAnyDecorator(
1254 'repository.read', 'repository.write', 'repository.admin')
1254 'repository.read', 'repository.write', 'repository.admin')
1255 @CSRFRequired()
1255 @CSRFRequired()
1256 @view_config(
1256 @view_config(
1257 route_name='pullrequest_comment_delete', request_method='POST',
1257 route_name='pullrequest_comment_delete', request_method='POST',
1258 renderer='json_ext')
1258 renderer='json_ext')
1259 def pull_request_comment_delete(self):
1259 def pull_request_comment_delete(self):
1260 pull_request = PullRequest.get_or_404(
1260 pull_request = PullRequest.get_or_404(
1261 self.request.matchdict['pull_request_id'])
1261 self.request.matchdict['pull_request_id'])
1262
1262
1263 comment = ChangesetComment.get_or_404(
1263 comment = ChangesetComment.get_or_404(
1264 self.request.matchdict['comment_id'])
1264 self.request.matchdict['comment_id'])
1265 comment_id = comment.comment_id
1265 comment_id = comment.comment_id
1266
1266
1267 if pull_request.is_closed():
1267 if pull_request.is_closed():
1268 log.debug('comment: forbidden because pull request is closed')
1268 log.debug('comment: forbidden because pull request is closed')
1269 raise HTTPForbidden()
1269 raise HTTPForbidden()
1270
1270
1271 if not comment:
1271 if not comment:
1272 log.debug('Comment with id:%s not found, skipping', comment_id)
1272 log.debug('Comment with id:%s not found, skipping', comment_id)
1273 # comment already deleted in another call probably
1273 # comment already deleted in another call probably
1274 return True
1274 return True
1275
1275
1276 if comment.pull_request.is_closed():
1276 if comment.pull_request.is_closed():
1277 # don't allow deleting comments on closed pull request
1277 # don't allow deleting comments on closed pull request
1278 raise HTTPForbidden()
1278 raise HTTPForbidden()
1279
1279
1280 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1280 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1281 super_admin = h.HasPermissionAny('hg.admin')()
1281 super_admin = h.HasPermissionAny('hg.admin')()
1282 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1282 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1283 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1283 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1284 comment_repo_admin = is_repo_admin and is_repo_comment
1284 comment_repo_admin = is_repo_admin and is_repo_comment
1285
1285
1286 if super_admin or comment_owner or comment_repo_admin:
1286 if super_admin or comment_owner or comment_repo_admin:
1287 old_calculated_status = comment.pull_request.calculated_review_status()
1287 old_calculated_status = comment.pull_request.calculated_review_status()
1288 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1288 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1289 Session().commit()
1289 Session().commit()
1290 calculated_status = comment.pull_request.calculated_review_status()
1290 calculated_status = comment.pull_request.calculated_review_status()
1291 if old_calculated_status != calculated_status:
1291 if old_calculated_status != calculated_status:
1292 PullRequestModel()._trigger_pull_request_hook(
1292 PullRequestModel()._trigger_pull_request_hook(
1293 comment.pull_request, self._rhodecode_user, 'review_status_change')
1293 comment.pull_request, self._rhodecode_user, 'review_status_change')
1294 return True
1294 return True
1295 else:
1295 else:
1296 log.warning('No permissions for user %s to delete comment_id: %s',
1296 log.warning('No permissions for user %s to delete comment_id: %s',
1297 self._rhodecode_db_user, comment_id)
1297 self._rhodecode_db_user, comment_id)
1298 raise HTTPNotFound()
1298 raise HTTPNotFound()
@@ -1,660 +1,662 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from pyramid.threadlocal import get_current_registry, get_current_request
29 from pyramid.threadlocal import get_current_registry, get_current_request
30 from sqlalchemy.sql.expression import null
30 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.functions import coalesce
31 from sqlalchemy.sql.functions import coalesce
32
32
33 from rhodecode.lib import helpers as h, diffs, channelstream
33 from rhodecode.lib import helpers as h, diffs, channelstream
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 from rhodecode.model import BaseModel
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import (
37 from rhodecode.model.db import (
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 from rhodecode.model.notification import NotificationModel
39 from rhodecode.model.notification import NotificationModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.settings import VcsSettingsModel
41 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.model.notification import EmailNotificationModel
42 from rhodecode.model.notification import EmailNotificationModel
43 from rhodecode.model.validation_schema.schemas import comment_schema
43 from rhodecode.model.validation_schema.schemas import comment_schema
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class CommentsModel(BaseModel):
49 class CommentsModel(BaseModel):
50
50
51 cls = ChangesetComment
51 cls = ChangesetComment
52
52
53 DIFF_CONTEXT_BEFORE = 3
53 DIFF_CONTEXT_BEFORE = 3
54 DIFF_CONTEXT_AFTER = 3
54 DIFF_CONTEXT_AFTER = 3
55
55
56 def __get_commit_comment(self, changeset_comment):
56 def __get_commit_comment(self, changeset_comment):
57 return self._get_instance(ChangesetComment, changeset_comment)
57 return self._get_instance(ChangesetComment, changeset_comment)
58
58
59 def __get_pull_request(self, pull_request):
59 def __get_pull_request(self, pull_request):
60 return self._get_instance(PullRequest, pull_request)
60 return self._get_instance(PullRequest, pull_request)
61
61
62 def _extract_mentions(self, s):
62 def _extract_mentions(self, s):
63 user_objects = []
63 user_objects = []
64 for username in extract_mentioned_users(s):
64 for username in extract_mentioned_users(s):
65 user_obj = User.get_by_username(username, case_insensitive=True)
65 user_obj = User.get_by_username(username, case_insensitive=True)
66 if user_obj:
66 if user_obj:
67 user_objects.append(user_obj)
67 user_objects.append(user_obj)
68 return user_objects
68 return user_objects
69
69
70 def _get_renderer(self, global_renderer='rst', request=None):
70 def _get_renderer(self, global_renderer='rst', request=None):
71 request = request or get_current_request()
71 request = request or get_current_request()
72
72
73 try:
73 try:
74 global_renderer = request.call_context.visual.default_renderer
74 global_renderer = request.call_context.visual.default_renderer
75 except AttributeError:
75 except AttributeError:
76 log.debug("Renderer not set, falling back "
76 log.debug("Renderer not set, falling back "
77 "to default renderer '%s'", global_renderer)
77 "to default renderer '%s'", global_renderer)
78 except Exception:
78 except Exception:
79 log.error(traceback.format_exc())
79 log.error(traceback.format_exc())
80 return global_renderer
80 return global_renderer
81
81
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 # group by versions, and count until, and display objects
83 # group by versions, and count until, and display objects
84
84
85 comment_groups = collections.defaultdict(list)
85 comment_groups = collections.defaultdict(list)
86 [comment_groups[
86 [comment_groups[
87 _co.pull_request_version_id].append(_co) for _co in comments]
87 _co.pull_request_version_id].append(_co) for _co in comments]
88
88
89 def yield_comments(pos):
89 def yield_comments(pos):
90 for co in comment_groups[pos]:
90 for co in comment_groups[pos]:
91 yield co
91 yield co
92
92
93 comment_versions = collections.defaultdict(
93 comment_versions = collections.defaultdict(
94 lambda: collections.defaultdict(list))
94 lambda: collections.defaultdict(list))
95 prev_prvid = -1
95 prev_prvid = -1
96 # fake last entry with None, to aggregate on "latest" version which
96 # fake last entry with None, to aggregate on "latest" version which
97 # doesn't have an pull_request_version_id
97 # doesn't have an pull_request_version_id
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 prvid = ver.pull_request_version_id
99 prvid = ver.pull_request_version_id
100 if prev_prvid == -1:
100 if prev_prvid == -1:
101 prev_prvid = prvid
101 prev_prvid = prvid
102
102
103 for co in yield_comments(prvid):
103 for co in yield_comments(prvid):
104 comment_versions[prvid]['at'].append(co)
104 comment_versions[prvid]['at'].append(co)
105
105
106 # save until
106 # save until
107 current = comment_versions[prvid]['at']
107 current = comment_versions[prvid]['at']
108 prev_until = comment_versions[prev_prvid]['until']
108 prev_until = comment_versions[prev_prvid]['until']
109 cur_until = prev_until + current
109 cur_until = prev_until + current
110 comment_versions[prvid]['until'].extend(cur_until)
110 comment_versions[prvid]['until'].extend(cur_until)
111
111
112 # save outdated
112 # save outdated
113 if inline:
113 if inline:
114 outdated = [x for x in cur_until
114 outdated = [x for x in cur_until
115 if x.outdated_at_version(show_version)]
115 if x.outdated_at_version(show_version)]
116 else:
116 else:
117 outdated = [x for x in cur_until
117 outdated = [x for x in cur_until
118 if x.older_than_version(show_version)]
118 if x.older_than_version(show_version)]
119 display = [x for x in cur_until if x not in outdated]
119 display = [x for x in cur_until if x not in outdated]
120
120
121 comment_versions[prvid]['outdated'] = outdated
121 comment_versions[prvid]['outdated'] = outdated
122 comment_versions[prvid]['display'] = display
122 comment_versions[prvid]['display'] = display
123
123
124 prev_prvid = prvid
124 prev_prvid = prvid
125
125
126 return comment_versions
126 return comment_versions
127
127
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
129
129
130 todos = Session().query(ChangesetComment) \
130 todos = Session().query(ChangesetComment) \
131 .filter(ChangesetComment.pull_request == pull_request) \
131 .filter(ChangesetComment.pull_request == pull_request) \
132 .filter(ChangesetComment.resolved_by == None) \
132 .filter(ChangesetComment.resolved_by == None) \
133 .filter(ChangesetComment.comment_type
133 .filter(ChangesetComment.comment_type
134 == ChangesetComment.COMMENT_TYPE_TODO)
134 == ChangesetComment.COMMENT_TYPE_TODO)
135
135
136 if not show_outdated:
136 if not show_outdated:
137 todos = todos.filter(
137 todos = todos.filter(
138 coalesce(ChangesetComment.display_state, '') !=
138 coalesce(ChangesetComment.display_state, '') !=
139 ChangesetComment.COMMENT_OUTDATED)
139 ChangesetComment.COMMENT_OUTDATED)
140
140
141 todos = todos.all()
141 todos = todos.all()
142
142
143 return todos
143 return todos
144
144
145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
146
146
147 todos = Session().query(ChangesetComment) \
147 todos = Session().query(ChangesetComment) \
148 .filter(ChangesetComment.revision == commit_id) \
148 .filter(ChangesetComment.revision == commit_id) \
149 .filter(ChangesetComment.resolved_by == None) \
149 .filter(ChangesetComment.resolved_by == None) \
150 .filter(ChangesetComment.comment_type
150 .filter(ChangesetComment.comment_type
151 == ChangesetComment.COMMENT_TYPE_TODO)
151 == ChangesetComment.COMMENT_TYPE_TODO)
152
152
153 if not show_outdated:
153 if not show_outdated:
154 todos = todos.filter(
154 todos = todos.filter(
155 coalesce(ChangesetComment.display_state, '') !=
155 coalesce(ChangesetComment.display_state, '') !=
156 ChangesetComment.COMMENT_OUTDATED)
156 ChangesetComment.COMMENT_OUTDATED)
157
157
158 todos = todos.all()
158 todos = todos.all()
159
159
160 return todos
160 return todos
161
161
162 def _log_audit_action(self, action, action_data, user, comment):
162 def _log_audit_action(self, action, action_data, auth_user, comment):
163 audit_logger.store(
163 audit_logger.store(
164 action=action,
164 action=action,
165 action_data=action_data,
165 action_data=action_data,
166 user=user,
166 user=auth_user,
167 repo=comment.repo)
167 repo=comment.repo)
168
168
169 def create(self, text, repo, user, commit_id=None, pull_request=None,
169 def create(self, text, repo, user, commit_id=None, pull_request=None,
170 f_path=None, line_no=None, status_change=None,
170 f_path=None, line_no=None, status_change=None,
171 status_change_type=None, comment_type=None,
171 status_change_type=None, comment_type=None,
172 resolves_comment_id=None, closing_pr=False, send_email=True,
172 resolves_comment_id=None, closing_pr=False, send_email=True,
173 renderer=None):
173 renderer=None, auth_user=None):
174 """
174 """
175 Creates new comment for commit or pull request.
175 Creates new comment for commit or pull request.
176 IF status_change is not none this comment is associated with a
176 IF status_change is not none this comment is associated with a
177 status change of commit or commit associated with pull request
177 status change of commit or commit associated with pull request
178
178
179 :param text:
179 :param text:
180 :param repo:
180 :param repo:
181 :param user:
181 :param user:
182 :param commit_id:
182 :param commit_id:
183 :param pull_request:
183 :param pull_request:
184 :param f_path:
184 :param f_path:
185 :param line_no:
185 :param line_no:
186 :param status_change: Label for status change
186 :param status_change: Label for status change
187 :param comment_type: Type of comment
187 :param comment_type: Type of comment
188 :param status_change_type: type of status change
188 :param status_change_type: type of status change
189 :param closing_pr:
189 :param closing_pr:
190 :param send_email:
190 :param send_email:
191 :param renderer: pick renderer for this comment
191 :param renderer: pick renderer for this comment
192 """
192 """
193
194 auth_user = auth_user or user
193 if not text:
195 if not text:
194 log.warning('Missing text for comment, skipping...')
196 log.warning('Missing text for comment, skipping...')
195 return
197 return
196 request = get_current_request()
198 request = get_current_request()
197 _ = request.translate
199 _ = request.translate
198
200
199 if not renderer:
201 if not renderer:
200 renderer = self._get_renderer(request=request)
202 renderer = self._get_renderer(request=request)
201
203
202 repo = self._get_repo(repo)
204 repo = self._get_repo(repo)
203 user = self._get_user(user)
205 user = self._get_user(user)
204
206
205 schema = comment_schema.CommentSchema()
207 schema = comment_schema.CommentSchema()
206 validated_kwargs = schema.deserialize(dict(
208 validated_kwargs = schema.deserialize(dict(
207 comment_body=text,
209 comment_body=text,
208 comment_type=comment_type,
210 comment_type=comment_type,
209 comment_file=f_path,
211 comment_file=f_path,
210 comment_line=line_no,
212 comment_line=line_no,
211 renderer_type=renderer,
213 renderer_type=renderer,
212 status_change=status_change_type,
214 status_change=status_change_type,
213 resolves_comment_id=resolves_comment_id,
215 resolves_comment_id=resolves_comment_id,
214 repo=repo.repo_id,
216 repo=repo.repo_id,
215 user=user.user_id,
217 user=user.user_id,
216 ))
218 ))
217
219
218 comment = ChangesetComment()
220 comment = ChangesetComment()
219 comment.renderer = validated_kwargs['renderer_type']
221 comment.renderer = validated_kwargs['renderer_type']
220 comment.text = validated_kwargs['comment_body']
222 comment.text = validated_kwargs['comment_body']
221 comment.f_path = validated_kwargs['comment_file']
223 comment.f_path = validated_kwargs['comment_file']
222 comment.line_no = validated_kwargs['comment_line']
224 comment.line_no = validated_kwargs['comment_line']
223 comment.comment_type = validated_kwargs['comment_type']
225 comment.comment_type = validated_kwargs['comment_type']
224
226
225 comment.repo = repo
227 comment.repo = repo
226 comment.author = user
228 comment.author = user
227 resolved_comment = self.__get_commit_comment(
229 resolved_comment = self.__get_commit_comment(
228 validated_kwargs['resolves_comment_id'])
230 validated_kwargs['resolves_comment_id'])
229 # check if the comment actually belongs to this PR
231 # check if the comment actually belongs to this PR
230 if resolved_comment and resolved_comment.pull_request and \
232 if resolved_comment and resolved_comment.pull_request and \
231 resolved_comment.pull_request != pull_request:
233 resolved_comment.pull_request != pull_request:
232 # comment not bound to this pull request, forbid
234 # comment not bound to this pull request, forbid
233 resolved_comment = None
235 resolved_comment = None
234 comment.resolved_comment = resolved_comment
236 comment.resolved_comment = resolved_comment
235
237
236 pull_request_id = pull_request
238 pull_request_id = pull_request
237
239
238 commit_obj = None
240 commit_obj = None
239 pull_request_obj = None
241 pull_request_obj = None
240
242
241 if commit_id:
243 if commit_id:
242 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
244 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
243 # do a lookup, so we don't pass something bad here
245 # do a lookup, so we don't pass something bad here
244 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
246 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
245 comment.revision = commit_obj.raw_id
247 comment.revision = commit_obj.raw_id
246
248
247 elif pull_request_id:
249 elif pull_request_id:
248 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
250 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
249 pull_request_obj = self.__get_pull_request(pull_request_id)
251 pull_request_obj = self.__get_pull_request(pull_request_id)
250 comment.pull_request = pull_request_obj
252 comment.pull_request = pull_request_obj
251 else:
253 else:
252 raise Exception('Please specify commit or pull_request_id')
254 raise Exception('Please specify commit or pull_request_id')
253
255
254 Session().add(comment)
256 Session().add(comment)
255 Session().flush()
257 Session().flush()
256 kwargs = {
258 kwargs = {
257 'user': user,
259 'user': user,
258 'renderer_type': renderer,
260 'renderer_type': renderer,
259 'repo_name': repo.repo_name,
261 'repo_name': repo.repo_name,
260 'status_change': status_change,
262 'status_change': status_change,
261 'status_change_type': status_change_type,
263 'status_change_type': status_change_type,
262 'comment_body': text,
264 'comment_body': text,
263 'comment_file': f_path,
265 'comment_file': f_path,
264 'comment_line': line_no,
266 'comment_line': line_no,
265 'comment_type': comment_type or 'note'
267 'comment_type': comment_type or 'note'
266 }
268 }
267
269
268 if commit_obj:
270 if commit_obj:
269 recipients = ChangesetComment.get_users(
271 recipients = ChangesetComment.get_users(
270 revision=commit_obj.raw_id)
272 revision=commit_obj.raw_id)
271 # add commit author if it's in RhodeCode system
273 # add commit author if it's in RhodeCode system
272 cs_author = User.get_from_cs_author(commit_obj.author)
274 cs_author = User.get_from_cs_author(commit_obj.author)
273 if not cs_author:
275 if not cs_author:
274 # use repo owner if we cannot extract the author correctly
276 # use repo owner if we cannot extract the author correctly
275 cs_author = repo.user
277 cs_author = repo.user
276 recipients += [cs_author]
278 recipients += [cs_author]
277
279
278 commit_comment_url = self.get_url(comment, request=request)
280 commit_comment_url = self.get_url(comment, request=request)
279
281
280 target_repo_url = h.link_to(
282 target_repo_url = h.link_to(
281 repo.repo_name,
283 repo.repo_name,
282 h.route_url('repo_summary', repo_name=repo.repo_name))
284 h.route_url('repo_summary', repo_name=repo.repo_name))
283
285
284 # commit specifics
286 # commit specifics
285 kwargs.update({
287 kwargs.update({
286 'commit': commit_obj,
288 'commit': commit_obj,
287 'commit_message': commit_obj.message,
289 'commit_message': commit_obj.message,
288 'commit_target_repo': target_repo_url,
290 'commit_target_repo': target_repo_url,
289 'commit_comment_url': commit_comment_url,
291 'commit_comment_url': commit_comment_url,
290 })
292 })
291
293
292 elif pull_request_obj:
294 elif pull_request_obj:
293 # get the current participants of this pull request
295 # get the current participants of this pull request
294 recipients = ChangesetComment.get_users(
296 recipients = ChangesetComment.get_users(
295 pull_request_id=pull_request_obj.pull_request_id)
297 pull_request_id=pull_request_obj.pull_request_id)
296 # add pull request author
298 # add pull request author
297 recipients += [pull_request_obj.author]
299 recipients += [pull_request_obj.author]
298
300
299 # add the reviewers to notification
301 # add the reviewers to notification
300 recipients += [x.user for x in pull_request_obj.reviewers]
302 recipients += [x.user for x in pull_request_obj.reviewers]
301
303
302 pr_target_repo = pull_request_obj.target_repo
304 pr_target_repo = pull_request_obj.target_repo
303 pr_source_repo = pull_request_obj.source_repo
305 pr_source_repo = pull_request_obj.source_repo
304
306
305 pr_comment_url = h.route_url(
307 pr_comment_url = h.route_url(
306 'pullrequest_show',
308 'pullrequest_show',
307 repo_name=pr_target_repo.repo_name,
309 repo_name=pr_target_repo.repo_name,
308 pull_request_id=pull_request_obj.pull_request_id,
310 pull_request_id=pull_request_obj.pull_request_id,
309 _anchor='comment-%s' % comment.comment_id)
311 _anchor='comment-%s' % comment.comment_id)
310
312
311 # set some variables for email notification
313 # set some variables for email notification
312 pr_target_repo_url = h.route_url(
314 pr_target_repo_url = h.route_url(
313 'repo_summary', repo_name=pr_target_repo.repo_name)
315 'repo_summary', repo_name=pr_target_repo.repo_name)
314
316
315 pr_source_repo_url = h.route_url(
317 pr_source_repo_url = h.route_url(
316 'repo_summary', repo_name=pr_source_repo.repo_name)
318 'repo_summary', repo_name=pr_source_repo.repo_name)
317
319
318 # pull request specifics
320 # pull request specifics
319 kwargs.update({
321 kwargs.update({
320 'pull_request': pull_request_obj,
322 'pull_request': pull_request_obj,
321 'pr_id': pull_request_obj.pull_request_id,
323 'pr_id': pull_request_obj.pull_request_id,
322 'pr_target_repo': pr_target_repo,
324 'pr_target_repo': pr_target_repo,
323 'pr_target_repo_url': pr_target_repo_url,
325 'pr_target_repo_url': pr_target_repo_url,
324 'pr_source_repo': pr_source_repo,
326 'pr_source_repo': pr_source_repo,
325 'pr_source_repo_url': pr_source_repo_url,
327 'pr_source_repo_url': pr_source_repo_url,
326 'pr_comment_url': pr_comment_url,
328 'pr_comment_url': pr_comment_url,
327 'pr_closing': closing_pr,
329 'pr_closing': closing_pr,
328 })
330 })
329 if send_email:
331 if send_email:
330 # pre-generate the subject for notification itself
332 # pre-generate the subject for notification itself
331 (subject,
333 (subject,
332 _h, _e, # we don't care about those
334 _h, _e, # we don't care about those
333 body_plaintext) = EmailNotificationModel().render_email(
335 body_plaintext) = EmailNotificationModel().render_email(
334 notification_type, **kwargs)
336 notification_type, **kwargs)
335
337
336 mention_recipients = set(
338 mention_recipients = set(
337 self._extract_mentions(text)).difference(recipients)
339 self._extract_mentions(text)).difference(recipients)
338
340
339 # create notification objects, and emails
341 # create notification objects, and emails
340 NotificationModel().create(
342 NotificationModel().create(
341 created_by=user,
343 created_by=user,
342 notification_subject=subject,
344 notification_subject=subject,
343 notification_body=body_plaintext,
345 notification_body=body_plaintext,
344 notification_type=notification_type,
346 notification_type=notification_type,
345 recipients=recipients,
347 recipients=recipients,
346 mention_recipients=mention_recipients,
348 mention_recipients=mention_recipients,
347 email_kwargs=kwargs,
349 email_kwargs=kwargs,
348 )
350 )
349
351
350 Session().flush()
352 Session().flush()
351 if comment.pull_request:
353 if comment.pull_request:
352 action = 'repo.pull_request.comment.create'
354 action = 'repo.pull_request.comment.create'
353 else:
355 else:
354 action = 'repo.commit.comment.create'
356 action = 'repo.commit.comment.create'
355
357
356 comment_data = comment.get_api_data()
358 comment_data = comment.get_api_data()
357 self._log_audit_action(
359 self._log_audit_action(
358 action, {'data': comment_data}, user, comment)
360 action, {'data': comment_data}, auth_user, comment)
359
361
360 msg_url = ''
362 msg_url = ''
361 channel = None
363 channel = None
362 if commit_obj:
364 if commit_obj:
363 msg_url = commit_comment_url
365 msg_url = commit_comment_url
364 repo_name = repo.repo_name
366 repo_name = repo.repo_name
365 channel = u'/repo${}$/commit/{}'.format(
367 channel = u'/repo${}$/commit/{}'.format(
366 repo_name,
368 repo_name,
367 commit_obj.raw_id
369 commit_obj.raw_id
368 )
370 )
369 elif pull_request_obj:
371 elif pull_request_obj:
370 msg_url = pr_comment_url
372 msg_url = pr_comment_url
371 repo_name = pr_target_repo.repo_name
373 repo_name = pr_target_repo.repo_name
372 channel = u'/repo${}$/pr/{}'.format(
374 channel = u'/repo${}$/pr/{}'.format(
373 repo_name,
375 repo_name,
374 pull_request_id
376 pull_request_id
375 )
377 )
376
378
377 message = '<strong>{}</strong> {} - ' \
379 message = '<strong>{}</strong> {} - ' \
378 '<a onclick="window.location=\'{}\';' \
380 '<a onclick="window.location=\'{}\';' \
379 'window.location.reload()">' \
381 'window.location.reload()">' \
380 '<strong>{}</strong></a>'
382 '<strong>{}</strong></a>'
381 message = message.format(
383 message = message.format(
382 user.username, _('made a comment'), msg_url,
384 user.username, _('made a comment'), msg_url,
383 _('Show it now'))
385 _('Show it now'))
384
386
385 channelstream.post_message(
387 channelstream.post_message(
386 channel, message, user.username,
388 channel, message, user.username,
387 registry=get_current_registry())
389 registry=get_current_registry())
388
390
389 return comment
391 return comment
390
392
391 def delete(self, comment, user):
393 def delete(self, comment, auth_user):
392 """
394 """
393 Deletes given comment
395 Deletes given comment
394 """
396 """
395 comment = self.__get_commit_comment(comment)
397 comment = self.__get_commit_comment(comment)
396 old_data = comment.get_api_data()
398 old_data = comment.get_api_data()
397 Session().delete(comment)
399 Session().delete(comment)
398
400
399 if comment.pull_request:
401 if comment.pull_request:
400 action = 'repo.pull_request.comment.delete'
402 action = 'repo.pull_request.comment.delete'
401 else:
403 else:
402 action = 'repo.commit.comment.delete'
404 action = 'repo.commit.comment.delete'
403
405
404 self._log_audit_action(
406 self._log_audit_action(
405 action, {'old_data': old_data}, user, comment)
407 action, {'old_data': old_data}, auth_user, comment)
406
408
407 return comment
409 return comment
408
410
409 def get_all_comments(self, repo_id, revision=None, pull_request=None):
411 def get_all_comments(self, repo_id, revision=None, pull_request=None):
410 q = ChangesetComment.query()\
412 q = ChangesetComment.query()\
411 .filter(ChangesetComment.repo_id == repo_id)
413 .filter(ChangesetComment.repo_id == repo_id)
412 if revision:
414 if revision:
413 q = q.filter(ChangesetComment.revision == revision)
415 q = q.filter(ChangesetComment.revision == revision)
414 elif pull_request:
416 elif pull_request:
415 pull_request = self.__get_pull_request(pull_request)
417 pull_request = self.__get_pull_request(pull_request)
416 q = q.filter(ChangesetComment.pull_request == pull_request)
418 q = q.filter(ChangesetComment.pull_request == pull_request)
417 else:
419 else:
418 raise Exception('Please specify commit or pull_request')
420 raise Exception('Please specify commit or pull_request')
419 q = q.order_by(ChangesetComment.created_on)
421 q = q.order_by(ChangesetComment.created_on)
420 return q.all()
422 return q.all()
421
423
422 def get_url(self, comment, request=None, permalink=False):
424 def get_url(self, comment, request=None, permalink=False):
423 if not request:
425 if not request:
424 request = get_current_request()
426 request = get_current_request()
425
427
426 comment = self.__get_commit_comment(comment)
428 comment = self.__get_commit_comment(comment)
427 if comment.pull_request:
429 if comment.pull_request:
428 pull_request = comment.pull_request
430 pull_request = comment.pull_request
429 if permalink:
431 if permalink:
430 return request.route_url(
432 return request.route_url(
431 'pull_requests_global',
433 'pull_requests_global',
432 pull_request_id=pull_request.pull_request_id,
434 pull_request_id=pull_request.pull_request_id,
433 _anchor='comment-%s' % comment.comment_id)
435 _anchor='comment-%s' % comment.comment_id)
434 else:
436 else:
435 return request.route_url(
437 return request.route_url(
436 'pullrequest_show',
438 'pullrequest_show',
437 repo_name=safe_str(pull_request.target_repo.repo_name),
439 repo_name=safe_str(pull_request.target_repo.repo_name),
438 pull_request_id=pull_request.pull_request_id,
440 pull_request_id=pull_request.pull_request_id,
439 _anchor='comment-%s' % comment.comment_id)
441 _anchor='comment-%s' % comment.comment_id)
440
442
441 else:
443 else:
442 repo = comment.repo
444 repo = comment.repo
443 commit_id = comment.revision
445 commit_id = comment.revision
444
446
445 if permalink:
447 if permalink:
446 return request.route_url(
448 return request.route_url(
447 'repo_commit', repo_name=safe_str(repo.repo_id),
449 'repo_commit', repo_name=safe_str(repo.repo_id),
448 commit_id=commit_id,
450 commit_id=commit_id,
449 _anchor='comment-%s' % comment.comment_id)
451 _anchor='comment-%s' % comment.comment_id)
450
452
451 else:
453 else:
452 return request.route_url(
454 return request.route_url(
453 'repo_commit', repo_name=safe_str(repo.repo_name),
455 'repo_commit', repo_name=safe_str(repo.repo_name),
454 commit_id=commit_id,
456 commit_id=commit_id,
455 _anchor='comment-%s' % comment.comment_id)
457 _anchor='comment-%s' % comment.comment_id)
456
458
457 def get_comments(self, repo_id, revision=None, pull_request=None):
459 def get_comments(self, repo_id, revision=None, pull_request=None):
458 """
460 """
459 Gets main comments based on revision or pull_request_id
461 Gets main comments based on revision or pull_request_id
460
462
461 :param repo_id:
463 :param repo_id:
462 :param revision:
464 :param revision:
463 :param pull_request:
465 :param pull_request:
464 """
466 """
465
467
466 q = ChangesetComment.query()\
468 q = ChangesetComment.query()\
467 .filter(ChangesetComment.repo_id == repo_id)\
469 .filter(ChangesetComment.repo_id == repo_id)\
468 .filter(ChangesetComment.line_no == None)\
470 .filter(ChangesetComment.line_no == None)\
469 .filter(ChangesetComment.f_path == None)
471 .filter(ChangesetComment.f_path == None)
470 if revision:
472 if revision:
471 q = q.filter(ChangesetComment.revision == revision)
473 q = q.filter(ChangesetComment.revision == revision)
472 elif pull_request:
474 elif pull_request:
473 pull_request = self.__get_pull_request(pull_request)
475 pull_request = self.__get_pull_request(pull_request)
474 q = q.filter(ChangesetComment.pull_request == pull_request)
476 q = q.filter(ChangesetComment.pull_request == pull_request)
475 else:
477 else:
476 raise Exception('Please specify commit or pull_request')
478 raise Exception('Please specify commit or pull_request')
477 q = q.order_by(ChangesetComment.created_on)
479 q = q.order_by(ChangesetComment.created_on)
478 return q.all()
480 return q.all()
479
481
480 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
482 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
481 q = self._get_inline_comments_query(repo_id, revision, pull_request)
483 q = self._get_inline_comments_query(repo_id, revision, pull_request)
482 return self._group_comments_by_path_and_line_number(q)
484 return self._group_comments_by_path_and_line_number(q)
483
485
484 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
486 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
485 version=None):
487 version=None):
486 inline_cnt = 0
488 inline_cnt = 0
487 for fname, per_line_comments in inline_comments.iteritems():
489 for fname, per_line_comments in inline_comments.iteritems():
488 for lno, comments in per_line_comments.iteritems():
490 for lno, comments in per_line_comments.iteritems():
489 for comm in comments:
491 for comm in comments:
490 if not comm.outdated_at_version(version) and skip_outdated:
492 if not comm.outdated_at_version(version) and skip_outdated:
491 inline_cnt += 1
493 inline_cnt += 1
492
494
493 return inline_cnt
495 return inline_cnt
494
496
495 def get_outdated_comments(self, repo_id, pull_request):
497 def get_outdated_comments(self, repo_id, pull_request):
496 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
498 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
497 # of a pull request.
499 # of a pull request.
498 q = self._all_inline_comments_of_pull_request(pull_request)
500 q = self._all_inline_comments_of_pull_request(pull_request)
499 q = q.filter(
501 q = q.filter(
500 ChangesetComment.display_state ==
502 ChangesetComment.display_state ==
501 ChangesetComment.COMMENT_OUTDATED
503 ChangesetComment.COMMENT_OUTDATED
502 ).order_by(ChangesetComment.comment_id.asc())
504 ).order_by(ChangesetComment.comment_id.asc())
503
505
504 return self._group_comments_by_path_and_line_number(q)
506 return self._group_comments_by_path_and_line_number(q)
505
507
506 def _get_inline_comments_query(self, repo_id, revision, pull_request):
508 def _get_inline_comments_query(self, repo_id, revision, pull_request):
507 # TODO: johbo: Split this into two methods: One for PR and one for
509 # TODO: johbo: Split this into two methods: One for PR and one for
508 # commit.
510 # commit.
509 if revision:
511 if revision:
510 q = Session().query(ChangesetComment).filter(
512 q = Session().query(ChangesetComment).filter(
511 ChangesetComment.repo_id == repo_id,
513 ChangesetComment.repo_id == repo_id,
512 ChangesetComment.line_no != null(),
514 ChangesetComment.line_no != null(),
513 ChangesetComment.f_path != null(),
515 ChangesetComment.f_path != null(),
514 ChangesetComment.revision == revision)
516 ChangesetComment.revision == revision)
515
517
516 elif pull_request:
518 elif pull_request:
517 pull_request = self.__get_pull_request(pull_request)
519 pull_request = self.__get_pull_request(pull_request)
518 if not CommentsModel.use_outdated_comments(pull_request):
520 if not CommentsModel.use_outdated_comments(pull_request):
519 q = self._visible_inline_comments_of_pull_request(pull_request)
521 q = self._visible_inline_comments_of_pull_request(pull_request)
520 else:
522 else:
521 q = self._all_inline_comments_of_pull_request(pull_request)
523 q = self._all_inline_comments_of_pull_request(pull_request)
522
524
523 else:
525 else:
524 raise Exception('Please specify commit or pull_request_id')
526 raise Exception('Please specify commit or pull_request_id')
525 q = q.order_by(ChangesetComment.comment_id.asc())
527 q = q.order_by(ChangesetComment.comment_id.asc())
526 return q
528 return q
527
529
528 def _group_comments_by_path_and_line_number(self, q):
530 def _group_comments_by_path_and_line_number(self, q):
529 comments = q.all()
531 comments = q.all()
530 paths = collections.defaultdict(lambda: collections.defaultdict(list))
532 paths = collections.defaultdict(lambda: collections.defaultdict(list))
531 for co in comments:
533 for co in comments:
532 paths[co.f_path][co.line_no].append(co)
534 paths[co.f_path][co.line_no].append(co)
533 return paths
535 return paths
534
536
535 @classmethod
537 @classmethod
536 def needed_extra_diff_context(cls):
538 def needed_extra_diff_context(cls):
537 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
539 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
538
540
539 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
541 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
540 if not CommentsModel.use_outdated_comments(pull_request):
542 if not CommentsModel.use_outdated_comments(pull_request):
541 return
543 return
542
544
543 comments = self._visible_inline_comments_of_pull_request(pull_request)
545 comments = self._visible_inline_comments_of_pull_request(pull_request)
544 comments_to_outdate = comments.all()
546 comments_to_outdate = comments.all()
545
547
546 for comment in comments_to_outdate:
548 for comment in comments_to_outdate:
547 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
549 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
548
550
549 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
551 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
550 diff_line = _parse_comment_line_number(comment.line_no)
552 diff_line = _parse_comment_line_number(comment.line_no)
551
553
552 try:
554 try:
553 old_context = old_diff_proc.get_context_of_line(
555 old_context = old_diff_proc.get_context_of_line(
554 path=comment.f_path, diff_line=diff_line)
556 path=comment.f_path, diff_line=diff_line)
555 new_context = new_diff_proc.get_context_of_line(
557 new_context = new_diff_proc.get_context_of_line(
556 path=comment.f_path, diff_line=diff_line)
558 path=comment.f_path, diff_line=diff_line)
557 except (diffs.LineNotInDiffException,
559 except (diffs.LineNotInDiffException,
558 diffs.FileNotInDiffException):
560 diffs.FileNotInDiffException):
559 comment.display_state = ChangesetComment.COMMENT_OUTDATED
561 comment.display_state = ChangesetComment.COMMENT_OUTDATED
560 return
562 return
561
563
562 if old_context == new_context:
564 if old_context == new_context:
563 return
565 return
564
566
565 if self._should_relocate_diff_line(diff_line):
567 if self._should_relocate_diff_line(diff_line):
566 new_diff_lines = new_diff_proc.find_context(
568 new_diff_lines = new_diff_proc.find_context(
567 path=comment.f_path, context=old_context,
569 path=comment.f_path, context=old_context,
568 offset=self.DIFF_CONTEXT_BEFORE)
570 offset=self.DIFF_CONTEXT_BEFORE)
569 if not new_diff_lines:
571 if not new_diff_lines:
570 comment.display_state = ChangesetComment.COMMENT_OUTDATED
572 comment.display_state = ChangesetComment.COMMENT_OUTDATED
571 else:
573 else:
572 new_diff_line = self._choose_closest_diff_line(
574 new_diff_line = self._choose_closest_diff_line(
573 diff_line, new_diff_lines)
575 diff_line, new_diff_lines)
574 comment.line_no = _diff_to_comment_line_number(new_diff_line)
576 comment.line_no = _diff_to_comment_line_number(new_diff_line)
575 else:
577 else:
576 comment.display_state = ChangesetComment.COMMENT_OUTDATED
578 comment.display_state = ChangesetComment.COMMENT_OUTDATED
577
579
578 def _should_relocate_diff_line(self, diff_line):
580 def _should_relocate_diff_line(self, diff_line):
579 """
581 """
580 Checks if relocation shall be tried for the given `diff_line`.
582 Checks if relocation shall be tried for the given `diff_line`.
581
583
582 If a comment points into the first lines, then we can have a situation
584 If a comment points into the first lines, then we can have a situation
583 that after an update another line has been added on top. In this case
585 that after an update another line has been added on top. In this case
584 we would find the context still and move the comment around. This
586 we would find the context still and move the comment around. This
585 would be wrong.
587 would be wrong.
586 """
588 """
587 should_relocate = (
589 should_relocate = (
588 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
590 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
589 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
591 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
590 return should_relocate
592 return should_relocate
591
593
592 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
594 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
593 candidate = new_diff_lines[0]
595 candidate = new_diff_lines[0]
594 best_delta = _diff_line_delta(diff_line, candidate)
596 best_delta = _diff_line_delta(diff_line, candidate)
595 for new_diff_line in new_diff_lines[1:]:
597 for new_diff_line in new_diff_lines[1:]:
596 delta = _diff_line_delta(diff_line, new_diff_line)
598 delta = _diff_line_delta(diff_line, new_diff_line)
597 if delta < best_delta:
599 if delta < best_delta:
598 candidate = new_diff_line
600 candidate = new_diff_line
599 best_delta = delta
601 best_delta = delta
600 return candidate
602 return candidate
601
603
602 def _visible_inline_comments_of_pull_request(self, pull_request):
604 def _visible_inline_comments_of_pull_request(self, pull_request):
603 comments = self._all_inline_comments_of_pull_request(pull_request)
605 comments = self._all_inline_comments_of_pull_request(pull_request)
604 comments = comments.filter(
606 comments = comments.filter(
605 coalesce(ChangesetComment.display_state, '') !=
607 coalesce(ChangesetComment.display_state, '') !=
606 ChangesetComment.COMMENT_OUTDATED)
608 ChangesetComment.COMMENT_OUTDATED)
607 return comments
609 return comments
608
610
609 def _all_inline_comments_of_pull_request(self, pull_request):
611 def _all_inline_comments_of_pull_request(self, pull_request):
610 comments = Session().query(ChangesetComment)\
612 comments = Session().query(ChangesetComment)\
611 .filter(ChangesetComment.line_no != None)\
613 .filter(ChangesetComment.line_no != None)\
612 .filter(ChangesetComment.f_path != None)\
614 .filter(ChangesetComment.f_path != None)\
613 .filter(ChangesetComment.pull_request == pull_request)
615 .filter(ChangesetComment.pull_request == pull_request)
614 return comments
616 return comments
615
617
616 def _all_general_comments_of_pull_request(self, pull_request):
618 def _all_general_comments_of_pull_request(self, pull_request):
617 comments = Session().query(ChangesetComment)\
619 comments = Session().query(ChangesetComment)\
618 .filter(ChangesetComment.line_no == None)\
620 .filter(ChangesetComment.line_no == None)\
619 .filter(ChangesetComment.f_path == None)\
621 .filter(ChangesetComment.f_path == None)\
620 .filter(ChangesetComment.pull_request == pull_request)
622 .filter(ChangesetComment.pull_request == pull_request)
621 return comments
623 return comments
622
624
623 @staticmethod
625 @staticmethod
624 def use_outdated_comments(pull_request):
626 def use_outdated_comments(pull_request):
625 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
627 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
626 settings = settings_model.get_general_settings()
628 settings = settings_model.get_general_settings()
627 return settings.get('rhodecode_use_outdated_comments', False)
629 return settings.get('rhodecode_use_outdated_comments', False)
628
630
629
631
630 def _parse_comment_line_number(line_no):
632 def _parse_comment_line_number(line_no):
631 """
633 """
632 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
634 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
633 """
635 """
634 old_line = None
636 old_line = None
635 new_line = None
637 new_line = None
636 if line_no.startswith('o'):
638 if line_no.startswith('o'):
637 old_line = int(line_no[1:])
639 old_line = int(line_no[1:])
638 elif line_no.startswith('n'):
640 elif line_no.startswith('n'):
639 new_line = int(line_no[1:])
641 new_line = int(line_no[1:])
640 else:
642 else:
641 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
643 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
642 return diffs.DiffLineNumber(old_line, new_line)
644 return diffs.DiffLineNumber(old_line, new_line)
643
645
644
646
645 def _diff_to_comment_line_number(diff_line):
647 def _diff_to_comment_line_number(diff_line):
646 if diff_line.new is not None:
648 if diff_line.new is not None:
647 return u'n{}'.format(diff_line.new)
649 return u'n{}'.format(diff_line.new)
648 elif diff_line.old is not None:
650 elif diff_line.old is not None:
649 return u'o{}'.format(diff_line.old)
651 return u'o{}'.format(diff_line.old)
650 return u''
652 return u''
651
653
652
654
653 def _diff_line_delta(a, b):
655 def _diff_line_delta(a, b):
654 if None not in (a.new, b.new):
656 if None not in (a.new, b.new):
655 return abs(a.new - b.new)
657 return abs(a.new - b.new)
656 elif None not in (a.old, b.old):
658 elif None not in (a.old, b.old):
657 return abs(a.old - b.old)
659 return abs(a.old - b.old)
658 else:
660 else:
659 raise ValueError(
661 raise ValueError(
660 "Cannot compute delta between {} and {}".format(a, b))
662 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now