##// END OF EJS Templates
channelstream: cleanup, and re-organize code for posting comments/pr updated messages....
marcink -
r4505:7d9c5b92 stable
parent child Browse files
Show More
@@ -1,1017 +1,1052 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib import channelstream
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
39
40
40 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
41
42
42
43
43 @jsonrpc_method()
44 @jsonrpc_method()
44 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
45 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
45 merge_state=Optional(False)):
46 merge_state=Optional(False)):
46 """
47 """
47 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
48
49
49 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :type apiuser: AuthUser
51 :type apiuser: AuthUser
51 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
52 the pull request was opened.
53 the pull request was opened.
53 :type repoid: str or int
54 :type repoid: str or int
54 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
55 :type pullrequestid: int
56 :type pullrequestid: int
56 :param merge_state: Optional calculate merge state for each repository.
57 :param merge_state: Optional calculate merge state for each repository.
57 This could result in longer time to fetch the data
58 This could result in longer time to fetch the data
58 :type merge_state: bool
59 :type merge_state: bool
59
60
60 Example output:
61 Example output:
61
62
62 .. code-block:: bash
63 .. code-block:: bash
63
64
64 "id": <id_given_in_input>,
65 "id": <id_given_in_input>,
65 "result":
66 "result":
66 {
67 {
67 "pull_request_id": "<pull_request_id>",
68 "pull_request_id": "<pull_request_id>",
68 "url": "<url>",
69 "url": "<url>",
69 "title": "<title>",
70 "title": "<title>",
70 "description": "<description>",
71 "description": "<description>",
71 "status" : "<status>",
72 "status" : "<status>",
72 "created_on": "<date_time_created>",
73 "created_on": "<date_time_created>",
73 "updated_on": "<date_time_updated>",
74 "updated_on": "<date_time_updated>",
74 "versions": "<number_or_versions_of_pr>",
75 "versions": "<number_or_versions_of_pr>",
75 "commit_ids": [
76 "commit_ids": [
76 ...
77 ...
77 "<commit_id>",
78 "<commit_id>",
78 "<commit_id>",
79 "<commit_id>",
79 ...
80 ...
80 ],
81 ],
81 "review_status": "<review_status>",
82 "review_status": "<review_status>",
82 "mergeable": {
83 "mergeable": {
83 "status": "<bool>",
84 "status": "<bool>",
84 "message": "<message>",
85 "message": "<message>",
85 },
86 },
86 "source": {
87 "source": {
87 "clone_url": "<clone_url>",
88 "clone_url": "<clone_url>",
88 "repository": "<repository_name>",
89 "repository": "<repository_name>",
89 "reference":
90 "reference":
90 {
91 {
91 "name": "<name>",
92 "name": "<name>",
92 "type": "<type>",
93 "type": "<type>",
93 "commit_id": "<commit_id>",
94 "commit_id": "<commit_id>",
94 }
95 }
95 },
96 },
96 "target": {
97 "target": {
97 "clone_url": "<clone_url>",
98 "clone_url": "<clone_url>",
98 "repository": "<repository_name>",
99 "repository": "<repository_name>",
99 "reference":
100 "reference":
100 {
101 {
101 "name": "<name>",
102 "name": "<name>",
102 "type": "<type>",
103 "type": "<type>",
103 "commit_id": "<commit_id>",
104 "commit_id": "<commit_id>",
104 }
105 }
105 },
106 },
106 "merge": {
107 "merge": {
107 "clone_url": "<clone_url>",
108 "clone_url": "<clone_url>",
108 "reference":
109 "reference":
109 {
110 {
110 "name": "<name>",
111 "name": "<name>",
111 "type": "<type>",
112 "type": "<type>",
112 "commit_id": "<commit_id>",
113 "commit_id": "<commit_id>",
113 }
114 }
114 },
115 },
115 "author": <user_obj>,
116 "author": <user_obj>,
116 "reviewers": [
117 "reviewers": [
117 ...
118 ...
118 {
119 {
119 "user": "<user_obj>",
120 "user": "<user_obj>",
120 "review_status": "<review_status>",
121 "review_status": "<review_status>",
121 }
122 }
122 ...
123 ...
123 ]
124 ]
124 },
125 },
125 "error": null
126 "error": null
126 """
127 """
127
128
128 pull_request = get_pull_request_or_error(pullrequestid)
129 pull_request = get_pull_request_or_error(pullrequestid)
129 if Optional.extract(repoid):
130 if Optional.extract(repoid):
130 repo = get_repo_or_error(repoid)
131 repo = get_repo_or_error(repoid)
131 else:
132 else:
132 repo = pull_request.target_repo
133 repo = pull_request.target_repo
133
134
134 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
135 raise JSONRPCError('repository `%s` or pull request `%s` '
136 raise JSONRPCError('repository `%s` or pull request `%s` '
136 'does not exist' % (repoid, pullrequestid))
137 'does not exist' % (repoid, pullrequestid))
137
138
138 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
139 # otherwise we can lock the repo on calculation of merge state while update/merge
140 # otherwise we can lock the repo on calculation of merge state while update/merge
140 # is happening.
141 # is happening.
141 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
142 merge_state = Optional.extract(merge_state, binary=True) and pr_created
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
143 data = pull_request.get_api_data(with_merge_state=merge_state)
144 data = pull_request.get_api_data(with_merge_state=merge_state)
144 return data
145 return data
145
146
146
147
147 @jsonrpc_method()
148 @jsonrpc_method()
148 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
149 merge_state=Optional(False)):
150 merge_state=Optional(False)):
150 """
151 """
151 Get all pull requests from the repository specified in `repoid`.
152 Get all pull requests from the repository specified in `repoid`.
152
153
153 :param apiuser: This is filled automatically from the |authtoken|.
154 :param apiuser: This is filled automatically from the |authtoken|.
154 :type apiuser: AuthUser
155 :type apiuser: AuthUser
155 :param repoid: Optional repository name or repository ID.
156 :param repoid: Optional repository name or repository ID.
156 :type repoid: str or int
157 :type repoid: str or int
157 :param status: Only return pull requests with the specified status.
158 :param status: Only return pull requests with the specified status.
158 Valid options are.
159 Valid options are.
159 * ``new`` (default)
160 * ``new`` (default)
160 * ``open``
161 * ``open``
161 * ``closed``
162 * ``closed``
162 :type status: str
163 :type status: str
163 :param merge_state: Optional calculate merge state for each repository.
164 :param merge_state: Optional calculate merge state for each repository.
164 This could result in longer time to fetch the data
165 This could result in longer time to fetch the data
165 :type merge_state: bool
166 :type merge_state: bool
166
167
167 Example output:
168 Example output:
168
169
169 .. code-block:: bash
170 .. code-block:: bash
170
171
171 "id": <id_given_in_input>,
172 "id": <id_given_in_input>,
172 "result":
173 "result":
173 [
174 [
174 ...
175 ...
175 {
176 {
176 "pull_request_id": "<pull_request_id>",
177 "pull_request_id": "<pull_request_id>",
177 "url": "<url>",
178 "url": "<url>",
178 "title" : "<title>",
179 "title" : "<title>",
179 "description": "<description>",
180 "description": "<description>",
180 "status": "<status>",
181 "status": "<status>",
181 "created_on": "<date_time_created>",
182 "created_on": "<date_time_created>",
182 "updated_on": "<date_time_updated>",
183 "updated_on": "<date_time_updated>",
183 "commit_ids": [
184 "commit_ids": [
184 ...
185 ...
185 "<commit_id>",
186 "<commit_id>",
186 "<commit_id>",
187 "<commit_id>",
187 ...
188 ...
188 ],
189 ],
189 "review_status": "<review_status>",
190 "review_status": "<review_status>",
190 "mergeable": {
191 "mergeable": {
191 "status": "<bool>",
192 "status": "<bool>",
192 "message: "<message>",
193 "message: "<message>",
193 },
194 },
194 "source": {
195 "source": {
195 "clone_url": "<clone_url>",
196 "clone_url": "<clone_url>",
196 "reference":
197 "reference":
197 {
198 {
198 "name": "<name>",
199 "name": "<name>",
199 "type": "<type>",
200 "type": "<type>",
200 "commit_id": "<commit_id>",
201 "commit_id": "<commit_id>",
201 }
202 }
202 },
203 },
203 "target": {
204 "target": {
204 "clone_url": "<clone_url>",
205 "clone_url": "<clone_url>",
205 "reference":
206 "reference":
206 {
207 {
207 "name": "<name>",
208 "name": "<name>",
208 "type": "<type>",
209 "type": "<type>",
209 "commit_id": "<commit_id>",
210 "commit_id": "<commit_id>",
210 }
211 }
211 },
212 },
212 "merge": {
213 "merge": {
213 "clone_url": "<clone_url>",
214 "clone_url": "<clone_url>",
214 "reference":
215 "reference":
215 {
216 {
216 "name": "<name>",
217 "name": "<name>",
217 "type": "<type>",
218 "type": "<type>",
218 "commit_id": "<commit_id>",
219 "commit_id": "<commit_id>",
219 }
220 }
220 },
221 },
221 "author": <user_obj>,
222 "author": <user_obj>,
222 "reviewers": [
223 "reviewers": [
223 ...
224 ...
224 {
225 {
225 "user": "<user_obj>",
226 "user": "<user_obj>",
226 "review_status": "<review_status>",
227 "review_status": "<review_status>",
227 }
228 }
228 ...
229 ...
229 ]
230 ]
230 }
231 }
231 ...
232 ...
232 ],
233 ],
233 "error": null
234 "error": null
234
235
235 """
236 """
236 repo = get_repo_or_error(repoid)
237 repo = get_repo_or_error(repoid)
237 if not has_superadmin_permission(apiuser):
238 if not has_superadmin_permission(apiuser):
238 _perms = (
239 _perms = (
239 'repository.admin', 'repository.write', 'repository.read',)
240 'repository.admin', 'repository.write', 'repository.read',)
240 validate_repo_permissions(apiuser, repoid, repo, _perms)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
241
242
242 status = Optional.extract(status)
243 status = Optional.extract(status)
243 merge_state = Optional.extract(merge_state, binary=True)
244 merge_state = Optional.extract(merge_state, binary=True)
244 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
245 order_by='id', order_dir='desc')
246 order_by='id', order_dir='desc')
246 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
247 return data
248 return data
248
249
249
250
250 @jsonrpc_method()
251 @jsonrpc_method()
251 def merge_pull_request(
252 def merge_pull_request(
252 request, apiuser, pullrequestid, repoid=Optional(None),
253 request, apiuser, pullrequestid, repoid=Optional(None),
253 userid=Optional(OAttr('apiuser'))):
254 userid=Optional(OAttr('apiuser'))):
254 """
255 """
255 Merge the pull request specified by `pullrequestid` into its target
256 Merge the pull request specified by `pullrequestid` into its target
256 repository.
257 repository.
257
258
258 :param apiuser: This is filled automatically from the |authtoken|.
259 :param apiuser: This is filled automatically from the |authtoken|.
259 :type apiuser: AuthUser
260 :type apiuser: AuthUser
260 :param repoid: Optional, repository name or repository ID of the
261 :param repoid: Optional, repository name or repository ID of the
261 target repository to which the |pr| is to be merged.
262 target repository to which the |pr| is to be merged.
262 :type repoid: str or int
263 :type repoid: str or int
263 :param pullrequestid: ID of the pull request which shall be merged.
264 :param pullrequestid: ID of the pull request which shall be merged.
264 :type pullrequestid: int
265 :type pullrequestid: int
265 :param userid: Merge the pull request as this user.
266 :param userid: Merge the pull request as this user.
266 :type userid: Optional(str or int)
267 :type userid: Optional(str or int)
267
268
268 Example output:
269 Example output:
269
270
270 .. code-block:: bash
271 .. code-block:: bash
271
272
272 "id": <id_given_in_input>,
273 "id": <id_given_in_input>,
273 "result": {
274 "result": {
274 "executed": "<bool>",
275 "executed": "<bool>",
275 "failure_reason": "<int>",
276 "failure_reason": "<int>",
276 "merge_status_message": "<str>",
277 "merge_status_message": "<str>",
277 "merge_commit_id": "<merge_commit_id>",
278 "merge_commit_id": "<merge_commit_id>",
278 "possible": "<bool>",
279 "possible": "<bool>",
279 "merge_ref": {
280 "merge_ref": {
280 "commit_id": "<commit_id>",
281 "commit_id": "<commit_id>",
281 "type": "<type>",
282 "type": "<type>",
282 "name": "<name>"
283 "name": "<name>"
283 }
284 }
284 },
285 },
285 "error": null
286 "error": null
286 """
287 """
287 pull_request = get_pull_request_or_error(pullrequestid)
288 pull_request = get_pull_request_or_error(pullrequestid)
288 if Optional.extract(repoid):
289 if Optional.extract(repoid):
289 repo = get_repo_or_error(repoid)
290 repo = get_repo_or_error(repoid)
290 else:
291 else:
291 repo = pull_request.target_repo
292 repo = pull_request.target_repo
292 auth_user = apiuser
293 auth_user = apiuser
293
294
294 if not isinstance(userid, Optional):
295 if not isinstance(userid, Optional):
295 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 user=apiuser, repo_name=repo.repo_name)
297 user=apiuser, repo_name=repo.repo_name)
297 if has_superadmin_permission(apiuser) or is_repo_admin:
298 if has_superadmin_permission(apiuser) or is_repo_admin:
298 apiuser = get_user_or_error(userid)
299 apiuser = get_user_or_error(userid)
299 auth_user = apiuser.AuthUser()
300 auth_user = apiuser.AuthUser()
300 else:
301 else:
301 raise JSONRPCError('userid is not the same as your user')
302 raise JSONRPCError('userid is not the same as your user')
302
303
303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 raise JSONRPCError(
305 raise JSONRPCError(
305 'Operation forbidden because pull request is in state {}, '
306 'Operation forbidden because pull request is in state {}, '
306 'only state {} is allowed.'.format(
307 'only state {} is allowed.'.format(
307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308
309
309 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 translator=request.translate)
312 translator=request.translate)
312 merge_possible = not check.failed
313 merge_possible = not check.failed
313
314
314 if not merge_possible:
315 if not merge_possible:
315 error_messages = []
316 error_messages = []
316 for err_type, error_msg in check.errors:
317 for err_type, error_msg in check.errors:
317 error_msg = request.translate(error_msg)
318 error_msg = request.translate(error_msg)
318 error_messages.append(error_msg)
319 error_messages.append(error_msg)
319
320
320 reasons = ','.join(error_messages)
321 reasons = ','.join(error_messages)
321 raise JSONRPCError(
322 raise JSONRPCError(
322 'merge not possible for following reasons: {}'.format(reasons))
323 'merge not possible for following reasons: {}'.format(reasons))
323
324
324 target_repo = pull_request.target_repo
325 target_repo = pull_request.target_repo
325 extras = vcs_operation_context(
326 extras = vcs_operation_context(
326 request.environ, repo_name=target_repo.repo_name,
327 request.environ, repo_name=target_repo.repo_name,
327 username=auth_user.username, action='push',
328 username=auth_user.username, action='push',
328 scm=target_repo.repo_type)
329 scm=target_repo.repo_type)
329 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 merge_response = PullRequestModel().merge_repo(
331 merge_response = PullRequestModel().merge_repo(
331 pull_request, apiuser, extras=extras)
332 pull_request, apiuser, extras=extras)
332 if merge_response.executed:
333 if merge_response.executed:
333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334
335
335 Session().commit()
336 Session().commit()
336
337
337 # In previous versions the merge response directly contained the merge
338 # In previous versions the merge response directly contained the merge
338 # commit id. It is now contained in the merge reference object. To be
339 # commit id. It is now contained in the merge reference object. To be
339 # backwards compatible we have to extract it again.
340 # backwards compatible we have to extract it again.
340 merge_response = merge_response.asdict()
341 merge_response = merge_response.asdict()
341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342
343
343 return merge_response
344 return merge_response
344
345
345
346
346 @jsonrpc_method()
347 @jsonrpc_method()
347 def get_pull_request_comments(
348 def get_pull_request_comments(
348 request, apiuser, pullrequestid, repoid=Optional(None)):
349 request, apiuser, pullrequestid, repoid=Optional(None)):
349 """
350 """
350 Get all comments of pull request specified with the `pullrequestid`
351 Get all comments of pull request specified with the `pullrequestid`
351
352
352 :param apiuser: This is filled automatically from the |authtoken|.
353 :param apiuser: This is filled automatically from the |authtoken|.
353 :type apiuser: AuthUser
354 :type apiuser: AuthUser
354 :param repoid: Optional repository name or repository ID.
355 :param repoid: Optional repository name or repository ID.
355 :type repoid: str or int
356 :type repoid: str or int
356 :param pullrequestid: The pull request ID.
357 :param pullrequestid: The pull request ID.
357 :type pullrequestid: int
358 :type pullrequestid: int
358
359
359 Example output:
360 Example output:
360
361
361 .. code-block:: bash
362 .. code-block:: bash
362
363
363 id : <id_given_in_input>
364 id : <id_given_in_input>
364 result : [
365 result : [
365 {
366 {
366 "comment_author": {
367 "comment_author": {
367 "active": true,
368 "active": true,
368 "full_name_or_username": "Tom Gore",
369 "full_name_or_username": "Tom Gore",
369 "username": "admin"
370 "username": "admin"
370 },
371 },
371 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_f_path": null,
373 "comment_f_path": null,
373 "comment_id": 25,
374 "comment_id": 25,
374 "comment_lineno": null,
375 "comment_lineno": null,
375 "comment_status": {
376 "comment_status": {
376 "status": "under_review",
377 "status": "under_review",
377 "status_lbl": "Under Review"
378 "status_lbl": "Under Review"
378 },
379 },
379 "comment_text": "Example text",
380 "comment_text": "Example text",
380 "comment_type": null,
381 "comment_type": null,
381 "comment_last_version: 0,
382 "comment_last_version: 0,
382 "pull_request_version": null,
383 "pull_request_version": null,
383 "comment_commit_id": None,
384 "comment_commit_id": None,
384 "comment_pull_request_id": <pull_request_id>
385 "comment_pull_request_id": <pull_request_id>
385 }
386 }
386 ],
387 ],
387 error : null
388 error : null
388 """
389 """
389
390
390 pull_request = get_pull_request_or_error(pullrequestid)
391 pull_request = get_pull_request_or_error(pullrequestid)
391 if Optional.extract(repoid):
392 if Optional.extract(repoid):
392 repo = get_repo_or_error(repoid)
393 repo = get_repo_or_error(repoid)
393 else:
394 else:
394 repo = pull_request.target_repo
395 repo = pull_request.target_repo
395
396
396 if not PullRequestModel().check_user_read(
397 if not PullRequestModel().check_user_read(
397 pull_request, apiuser, api=True):
398 pull_request, apiuser, api=True):
398 raise JSONRPCError('repository `%s` or pull request `%s` '
399 raise JSONRPCError('repository `%s` or pull request `%s` '
399 'does not exist' % (repoid, pullrequestid))
400 'does not exist' % (repoid, pullrequestid))
400
401
401 (pull_request_latest,
402 (pull_request_latest,
402 pull_request_at_ver,
403 pull_request_at_ver,
403 pull_request_display_obj,
404 pull_request_display_obj,
404 at_version) = PullRequestModel().get_pr_version(
405 at_version) = PullRequestModel().get_pr_version(
405 pull_request.pull_request_id, version=None)
406 pull_request.pull_request_id, version=None)
406
407
407 versions = pull_request_display_obj.versions()
408 versions = pull_request_display_obj.versions()
408 ver_map = {
409 ver_map = {
409 ver.pull_request_version_id: cnt
410 ver.pull_request_version_id: cnt
410 for cnt, ver in enumerate(versions, 1)
411 for cnt, ver in enumerate(versions, 1)
411 }
412 }
412
413
413 # GENERAL COMMENTS with versions #
414 # GENERAL COMMENTS with versions #
414 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = q.order_by(ChangesetComment.comment_id.asc())
416 q = q.order_by(ChangesetComment.comment_id.asc())
416 general_comments = q.all()
417 general_comments = q.all()
417
418
418 # INLINE COMMENTS with versions #
419 # INLINE COMMENTS with versions #
419 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = q.order_by(ChangesetComment.comment_id.asc())
421 q = q.order_by(ChangesetComment.comment_id.asc())
421 inline_comments = q.all()
422 inline_comments = q.all()
422
423
423 data = []
424 data = []
424 for comment in inline_comments + general_comments:
425 for comment in inline_comments + general_comments:
425 full_data = comment.get_api_data()
426 full_data = comment.get_api_data()
426 pr_version_id = None
427 pr_version_id = None
427 if comment.pull_request_version_id:
428 if comment.pull_request_version_id:
428 pr_version_id = 'v{}'.format(
429 pr_version_id = 'v{}'.format(
429 ver_map[comment.pull_request_version_id])
430 ver_map[comment.pull_request_version_id])
430
431
431 # sanitize some entries
432 # sanitize some entries
432
433
433 full_data['pull_request_version'] = pr_version_id
434 full_data['pull_request_version'] = pr_version_id
434 full_data['comment_author'] = {
435 full_data['comment_author'] = {
435 'username': full_data['comment_author'].username,
436 'username': full_data['comment_author'].username,
436 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'active': full_data['comment_author'].active,
438 'active': full_data['comment_author'].active,
438 }
439 }
439
440
440 if full_data['comment_status']:
441 if full_data['comment_status']:
441 full_data['comment_status'] = {
442 full_data['comment_status'] = {
442 'status': full_data['comment_status'][0].status,
443 'status': full_data['comment_status'][0].status,
443 'status_lbl': full_data['comment_status'][0].status_lbl,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
444 }
445 }
445 else:
446 else:
446 full_data['comment_status'] = {}
447 full_data['comment_status'] = {}
447
448
448 data.append(full_data)
449 data.append(full_data)
449 return data
450 return data
450
451
451
452
452 @jsonrpc_method()
453 @jsonrpc_method()
453 def comment_pull_request(
454 def comment_pull_request(
454 request, apiuser, pullrequestid, repoid=Optional(None),
455 request, apiuser, pullrequestid, repoid=Optional(None),
455 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 """
460 """
460 Comment on the pull request specified with the `pullrequestid`,
461 Comment on the pull request specified with the `pullrequestid`,
461 in the |repo| specified by the `repoid`, and optionally change the
462 in the |repo| specified by the `repoid`, and optionally change the
462 review status.
463 review status.
463
464
464 :param apiuser: This is filled automatically from the |authtoken|.
465 :param apiuser: This is filled automatically from the |authtoken|.
465 :type apiuser: AuthUser
466 :type apiuser: AuthUser
466 :param repoid: Optional repository name or repository ID.
467 :param repoid: Optional repository name or repository ID.
467 :type repoid: str or int
468 :type repoid: str or int
468 :param pullrequestid: The pull request ID.
469 :param pullrequestid: The pull request ID.
469 :type pullrequestid: int
470 :type pullrequestid: int
470 :param commit_id: Specify the commit_id for which to set a comment. If
471 :param commit_id: Specify the commit_id for which to set a comment. If
471 given commit_id is different than latest in the PR status
472 given commit_id is different than latest in the PR status
472 change won't be performed.
473 change won't be performed.
473 :type commit_id: str
474 :type commit_id: str
474 :param message: The text content of the comment.
475 :param message: The text content of the comment.
475 :type message: str
476 :type message: str
476 :param status: (**Optional**) Set the approval status of the pull
477 :param status: (**Optional**) Set the approval status of the pull
477 request. One of: 'not_reviewed', 'approved', 'rejected',
478 request. One of: 'not_reviewed', 'approved', 'rejected',
478 'under_review'
479 'under_review'
479 :type status: str
480 :type status: str
480 :param comment_type: Comment type, one of: 'note', 'todo'
481 :param comment_type: Comment type, one of: 'note', 'todo'
481 :type comment_type: Optional(str), default: 'note'
482 :type comment_type: Optional(str), default: 'note'
482 :param resolves_comment_id: id of comment which this one will resolve
483 :param resolves_comment_id: id of comment which this one will resolve
483 :type resolves_comment_id: Optional(int)
484 :type resolves_comment_id: Optional(int)
484 :param extra_recipients: list of user ids or usernames to add
485 :param extra_recipients: list of user ids or usernames to add
485 notifications for this comment. Acts like a CC for notification
486 notifications for this comment. Acts like a CC for notification
486 :type extra_recipients: Optional(list)
487 :type extra_recipients: Optional(list)
487 :param userid: Comment on the pull request as this user
488 :param userid: Comment on the pull request as this user
488 :type userid: Optional(str or int)
489 :type userid: Optional(str or int)
489 :param send_email: Define if this comment should also send email notification
490 :param send_email: Define if this comment should also send email notification
490 :type send_email: Optional(bool)
491 :type send_email: Optional(bool)
491
492
492 Example output:
493 Example output:
493
494
494 .. code-block:: bash
495 .. code-block:: bash
495
496
496 id : <id_given_in_input>
497 id : <id_given_in_input>
497 result : {
498 result : {
498 "pull_request_id": "<Integer>",
499 "pull_request_id": "<Integer>",
499 "comment_id": "<Integer>",
500 "comment_id": "<Integer>",
500 "status": {"given": <given_status>,
501 "status": {"given": <given_status>,
501 "was_changed": <bool status_was_actually_changed> },
502 "was_changed": <bool status_was_actually_changed> },
502 },
503 },
503 error : null
504 error : null
504 """
505 """
506 _ = request.translate
507
505 pull_request = get_pull_request_or_error(pullrequestid)
508 pull_request = get_pull_request_or_error(pullrequestid)
506 if Optional.extract(repoid):
509 if Optional.extract(repoid):
507 repo = get_repo_or_error(repoid)
510 repo = get_repo_or_error(repoid)
508 else:
511 else:
509 repo = pull_request.target_repo
512 repo = pull_request.target_repo
510
513
514 db_repo_name = repo.repo_name
511 auth_user = apiuser
515 auth_user = apiuser
512 if not isinstance(userid, Optional):
516 if not isinstance(userid, Optional):
513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
517 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
514 user=apiuser, repo_name=repo.repo_name)
518 user=apiuser, repo_name=db_repo_name)
515 if has_superadmin_permission(apiuser) or is_repo_admin:
519 if has_superadmin_permission(apiuser) or is_repo_admin:
516 apiuser = get_user_or_error(userid)
520 apiuser = get_user_or_error(userid)
517 auth_user = apiuser.AuthUser()
521 auth_user = apiuser.AuthUser()
518 else:
522 else:
519 raise JSONRPCError('userid is not the same as your user')
523 raise JSONRPCError('userid is not the same as your user')
520
524
521 if pull_request.is_closed():
525 if pull_request.is_closed():
522 raise JSONRPCError(
526 raise JSONRPCError(
523 'pull request `%s` comment failed, pull request is closed' % (
527 'pull request `%s` comment failed, pull request is closed' % (
524 pullrequestid,))
528 pullrequestid,))
525
529
526 if not PullRequestModel().check_user_read(
530 if not PullRequestModel().check_user_read(
527 pull_request, apiuser, api=True):
531 pull_request, apiuser, api=True):
528 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
532 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
529 message = Optional.extract(message)
533 message = Optional.extract(message)
530 status = Optional.extract(status)
534 status = Optional.extract(status)
531 commit_id = Optional.extract(commit_id)
535 commit_id = Optional.extract(commit_id)
532 comment_type = Optional.extract(comment_type)
536 comment_type = Optional.extract(comment_type)
533 resolves_comment_id = Optional.extract(resolves_comment_id)
537 resolves_comment_id = Optional.extract(resolves_comment_id)
534 extra_recipients = Optional.extract(extra_recipients)
538 extra_recipients = Optional.extract(extra_recipients)
535 send_email = Optional.extract(send_email, binary=True)
539 send_email = Optional.extract(send_email, binary=True)
536
540
537 if not message and not status:
541 if not message and not status:
538 raise JSONRPCError(
542 raise JSONRPCError(
539 'Both message and status parameters are missing. '
543 'Both message and status parameters are missing. '
540 'At least one is required.')
544 'At least one is required.')
541
545
542 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
546 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
543 status is not None):
547 status is not None):
544 raise JSONRPCError('Unknown comment status: `%s`' % status)
548 raise JSONRPCError('Unknown comment status: `%s`' % status)
545
549
546 if commit_id and commit_id not in pull_request.revisions:
550 if commit_id and commit_id not in pull_request.revisions:
547 raise JSONRPCError(
551 raise JSONRPCError(
548 'Invalid commit_id `%s` for this pull request.' % commit_id)
552 'Invalid commit_id `%s` for this pull request.' % commit_id)
549
553
550 allowed_to_change_status = PullRequestModel().check_user_change_status(
554 allowed_to_change_status = PullRequestModel().check_user_change_status(
551 pull_request, apiuser)
555 pull_request, apiuser)
552
556
553 # if commit_id is passed re-validated if user is allowed to change status
557 # if commit_id is passed re-validated if user is allowed to change status
554 # based on latest commit_id from the PR
558 # based on latest commit_id from the PR
555 if commit_id:
559 if commit_id:
556 commit_idx = pull_request.revisions.index(commit_id)
560 commit_idx = pull_request.revisions.index(commit_id)
557 if commit_idx != 0:
561 if commit_idx != 0:
558 allowed_to_change_status = False
562 allowed_to_change_status = False
559
563
560 if resolves_comment_id:
564 if resolves_comment_id:
561 comment = ChangesetComment.get(resolves_comment_id)
565 comment = ChangesetComment.get(resolves_comment_id)
562 if not comment:
566 if not comment:
563 raise JSONRPCError(
567 raise JSONRPCError(
564 'Invalid resolves_comment_id `%s` for this pull request.'
568 'Invalid resolves_comment_id `%s` for this pull request.'
565 % resolves_comment_id)
569 % resolves_comment_id)
566 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
570 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
567 raise JSONRPCError(
571 raise JSONRPCError(
568 'Comment `%s` is wrong type for setting status to resolved.'
572 'Comment `%s` is wrong type for setting status to resolved.'
569 % resolves_comment_id)
573 % resolves_comment_id)
570
574
571 text = message
575 text = message
572 status_label = ChangesetStatus.get_status_lbl(status)
576 status_label = ChangesetStatus.get_status_lbl(status)
573 if status and allowed_to_change_status:
577 if status and allowed_to_change_status:
574 st_message = ('Status change %(transition_icon)s %(status)s'
578 st_message = ('Status change %(transition_icon)s %(status)s'
575 % {'transition_icon': '>', 'status': status_label})
579 % {'transition_icon': '>', 'status': status_label})
576 text = message or st_message
580 text = message or st_message
577
581
578 rc_config = SettingsModel().get_all_settings()
582 rc_config = SettingsModel().get_all_settings()
579 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
583 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
580
584
581 status_change = status and allowed_to_change_status
585 status_change = status and allowed_to_change_status
582 comment = CommentsModel().create(
586 comment = CommentsModel().create(
583 text=text,
587 text=text,
584 repo=pull_request.target_repo.repo_id,
588 repo=pull_request.target_repo.repo_id,
585 user=apiuser.user_id,
589 user=apiuser.user_id,
586 pull_request=pull_request.pull_request_id,
590 pull_request=pull_request.pull_request_id,
587 f_path=None,
591 f_path=None,
588 line_no=None,
592 line_no=None,
589 status_change=(status_label if status_change else None),
593 status_change=(status_label if status_change else None),
590 status_change_type=(status if status_change else None),
594 status_change_type=(status if status_change else None),
591 closing_pr=False,
595 closing_pr=False,
592 renderer=renderer,
596 renderer=renderer,
593 comment_type=comment_type,
597 comment_type=comment_type,
594 resolves_comment_id=resolves_comment_id,
598 resolves_comment_id=resolves_comment_id,
595 auth_user=auth_user,
599 auth_user=auth_user,
596 extra_recipients=extra_recipients,
600 extra_recipients=extra_recipients,
597 send_email=send_email
601 send_email=send_email
598 )
602 )
603 is_inline = bool(comment.f_path and comment.line_no)
599
604
600 if allowed_to_change_status and status:
605 if allowed_to_change_status and status:
601 old_calculated_status = pull_request.calculated_review_status()
606 old_calculated_status = pull_request.calculated_review_status()
602 ChangesetStatusModel().set_status(
607 ChangesetStatusModel().set_status(
603 pull_request.target_repo.repo_id,
608 pull_request.target_repo.repo_id,
604 status,
609 status,
605 apiuser.user_id,
610 apiuser.user_id,
606 comment,
611 comment,
607 pull_request=pull_request.pull_request_id
612 pull_request=pull_request.pull_request_id
608 )
613 )
609 Session().flush()
614 Session().flush()
610
615
611 Session().commit()
616 Session().commit()
612
617
613 PullRequestModel().trigger_pull_request_hook(
618 PullRequestModel().trigger_pull_request_hook(
614 pull_request, apiuser, 'comment',
619 pull_request, apiuser, 'comment',
615 data={'comment': comment})
620 data={'comment': comment})
616
621
617 if allowed_to_change_status and status:
622 if allowed_to_change_status and status:
618 # we now calculate the status of pull request, and based on that
623 # we now calculate the status of pull request, and based on that
619 # calculation we set the commits status
624 # calculation we set the commits status
620 calculated_status = pull_request.calculated_review_status()
625 calculated_status = pull_request.calculated_review_status()
621 if old_calculated_status != calculated_status:
626 if old_calculated_status != calculated_status:
622 PullRequestModel().trigger_pull_request_hook(
627 PullRequestModel().trigger_pull_request_hook(
623 pull_request, apiuser, 'review_status_change',
628 pull_request, apiuser, 'review_status_change',
624 data={'status': calculated_status})
629 data={'status': calculated_status})
625
630
626 data = {
631 data = {
627 'pull_request_id': pull_request.pull_request_id,
632 'pull_request_id': pull_request.pull_request_id,
628 'comment_id': comment.comment_id if comment else None,
633 'comment_id': comment.comment_id if comment else None,
629 'status': {'given': status, 'was_changed': status_change},
634 'status': {'given': status, 'was_changed': status_change},
630 }
635 }
636
637 comment_broadcast_channel = channelstream.comment_channel(
638 db_repo_name, pull_request_obj=pull_request)
639
640 comment_data = data
641 comment_type = 'inline' if is_inline else 'general'
642 channelstream.comment_channelstream_push(
643 request, comment_broadcast_channel, apiuser,
644 _('posted a new {} comment').format(comment_type),
645 comment_data=comment_data)
646
631 return data
647 return data
632
648
633
649
634 @jsonrpc_method()
650 @jsonrpc_method()
635 def create_pull_request(
651 def create_pull_request(
636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
652 request, apiuser, source_repo, target_repo, source_ref, target_ref,
637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
653 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
638 description_renderer=Optional(''), reviewers=Optional(None)):
654 description_renderer=Optional(''), reviewers=Optional(None)):
639 """
655 """
640 Creates a new pull request.
656 Creates a new pull request.
641
657
642 Accepts refs in the following formats:
658 Accepts refs in the following formats:
643
659
644 * branch:<branch_name>:<sha>
660 * branch:<branch_name>:<sha>
645 * branch:<branch_name>
661 * branch:<branch_name>
646 * bookmark:<bookmark_name>:<sha> (Mercurial only)
662 * bookmark:<bookmark_name>:<sha> (Mercurial only)
647 * bookmark:<bookmark_name> (Mercurial only)
663 * bookmark:<bookmark_name> (Mercurial only)
648
664
649 :param apiuser: This is filled automatically from the |authtoken|.
665 :param apiuser: This is filled automatically from the |authtoken|.
650 :type apiuser: AuthUser
666 :type apiuser: AuthUser
651 :param source_repo: Set the source repository name.
667 :param source_repo: Set the source repository name.
652 :type source_repo: str
668 :type source_repo: str
653 :param target_repo: Set the target repository name.
669 :param target_repo: Set the target repository name.
654 :type target_repo: str
670 :type target_repo: str
655 :param source_ref: Set the source ref name.
671 :param source_ref: Set the source ref name.
656 :type source_ref: str
672 :type source_ref: str
657 :param target_ref: Set the target ref name.
673 :param target_ref: Set the target ref name.
658 :type target_ref: str
674 :type target_ref: str
659 :param owner: user_id or username
675 :param owner: user_id or username
660 :type owner: Optional(str)
676 :type owner: Optional(str)
661 :param title: Optionally Set the pull request title, it's generated otherwise
677 :param title: Optionally Set the pull request title, it's generated otherwise
662 :type title: str
678 :type title: str
663 :param description: Set the pull request description.
679 :param description: Set the pull request description.
664 :type description: Optional(str)
680 :type description: Optional(str)
665 :type description_renderer: Optional(str)
681 :type description_renderer: Optional(str)
666 :param description_renderer: Set pull request renderer for the description.
682 :param description_renderer: Set pull request renderer for the description.
667 It should be 'rst', 'markdown' or 'plain'. If not give default
683 It should be 'rst', 'markdown' or 'plain'. If not give default
668 system renderer will be used
684 system renderer will be used
669 :param reviewers: Set the new pull request reviewers list.
685 :param reviewers: Set the new pull request reviewers list.
670 Reviewer defined by review rules will be added automatically to the
686 Reviewer defined by review rules will be added automatically to the
671 defined list.
687 defined list.
672 :type reviewers: Optional(list)
688 :type reviewers: Optional(list)
673 Accepts username strings or objects of the format:
689 Accepts username strings or objects of the format:
674
690
675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
691 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
676 """
692 """
677
693
678 source_db_repo = get_repo_or_error(source_repo)
694 source_db_repo = get_repo_or_error(source_repo)
679 target_db_repo = get_repo_or_error(target_repo)
695 target_db_repo = get_repo_or_error(target_repo)
680 if not has_superadmin_permission(apiuser):
696 if not has_superadmin_permission(apiuser):
681 _perms = ('repository.admin', 'repository.write', 'repository.read',)
697 _perms = ('repository.admin', 'repository.write', 'repository.read',)
682 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
698 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
683
699
684 owner = validate_set_owner_permissions(apiuser, owner)
700 owner = validate_set_owner_permissions(apiuser, owner)
685
701
686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
702 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
703 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
688
704
689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
705 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
706 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
691
707
692 reviewer_objects = Optional.extract(reviewers) or []
708 reviewer_objects = Optional.extract(reviewers) or []
693
709
694 # serialize and validate passed in given reviewers
710 # serialize and validate passed in given reviewers
695 if reviewer_objects:
711 if reviewer_objects:
696 schema = ReviewerListSchema()
712 schema = ReviewerListSchema()
697 try:
713 try:
698 reviewer_objects = schema.deserialize(reviewer_objects)
714 reviewer_objects = schema.deserialize(reviewer_objects)
699 except Invalid as err:
715 except Invalid as err:
700 raise JSONRPCValidationError(colander_exc=err)
716 raise JSONRPCValidationError(colander_exc=err)
701
717
702 # validate users
718 # validate users
703 for reviewer_object in reviewer_objects:
719 for reviewer_object in reviewer_objects:
704 user = get_user_or_error(reviewer_object['username'])
720 user = get_user_or_error(reviewer_object['username'])
705 reviewer_object['user_id'] = user.user_id
721 reviewer_object['user_id'] = user.user_id
706
722
707 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
723 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
708 PullRequestModel().get_reviewer_functions()
724 PullRequestModel().get_reviewer_functions()
709
725
710 # recalculate reviewers logic, to make sure we can validate this
726 # recalculate reviewers logic, to make sure we can validate this
711 default_reviewers_data = get_default_reviewers_data(
727 default_reviewers_data = get_default_reviewers_data(
712 owner, source_db_repo,
728 owner, source_db_repo,
713 source_commit, target_db_repo, target_commit)
729 source_commit, target_db_repo, target_commit)
714
730
715 # now MERGE our given with the calculated
731 # now MERGE our given with the calculated
716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
732 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
717
733
718 try:
734 try:
719 reviewers = validate_default_reviewers(
735 reviewers = validate_default_reviewers(
720 reviewer_objects, default_reviewers_data)
736 reviewer_objects, default_reviewers_data)
721 except ValueError as e:
737 except ValueError as e:
722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
738 raise JSONRPCError('Reviewers Validation: {}'.format(e))
723
739
724 title = Optional.extract(title)
740 title = Optional.extract(title)
725 if not title:
741 if not title:
726 title_source_ref = source_ref.split(':', 2)[1]
742 title_source_ref = source_ref.split(':', 2)[1]
727 title = PullRequestModel().generate_pullrequest_title(
743 title = PullRequestModel().generate_pullrequest_title(
728 source=source_repo,
744 source=source_repo,
729 source_ref=title_source_ref,
745 source_ref=title_source_ref,
730 target=target_repo
746 target=target_repo
731 )
747 )
732
748
733 diff_info = default_reviewers_data['diff_info']
749 diff_info = default_reviewers_data['diff_info']
734 common_ancestor_id = diff_info['ancestor']
750 common_ancestor_id = diff_info['ancestor']
735 commits = diff_info['commits']
751 commits = diff_info['commits']
736
752
737 if not common_ancestor_id:
753 if not common_ancestor_id:
738 raise JSONRPCError('no common ancestor found')
754 raise JSONRPCError('no common ancestor found')
739
755
740 if not commits:
756 if not commits:
741 raise JSONRPCError('no commits found')
757 raise JSONRPCError('no commits found')
742
758
743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
759 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
744 revisions = [commit.raw_id for commit in reversed(commits)]
760 revisions = [commit.raw_id for commit in reversed(commits)]
745
761
746 # recalculate target ref based on ancestor
762 # recalculate target ref based on ancestor
747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
763 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
764 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
749
765
750 # fetch renderer, if set fallback to plain in case of PR
766 # fetch renderer, if set fallback to plain in case of PR
751 rc_config = SettingsModel().get_all_settings()
767 rc_config = SettingsModel().get_all_settings()
752 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
768 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
753 description = Optional.extract(description)
769 description = Optional.extract(description)
754 description_renderer = Optional.extract(description_renderer) or default_system_renderer
770 description_renderer = Optional.extract(description_renderer) or default_system_renderer
755
771
756 pull_request = PullRequestModel().create(
772 pull_request = PullRequestModel().create(
757 created_by=owner.user_id,
773 created_by=owner.user_id,
758 source_repo=source_repo,
774 source_repo=source_repo,
759 source_ref=full_source_ref,
775 source_ref=full_source_ref,
760 target_repo=target_repo,
776 target_repo=target_repo,
761 target_ref=full_target_ref,
777 target_ref=full_target_ref,
762 common_ancestor_id=common_ancestor_id,
778 common_ancestor_id=common_ancestor_id,
763 revisions=revisions,
779 revisions=revisions,
764 reviewers=reviewers,
780 reviewers=reviewers,
765 title=title,
781 title=title,
766 description=description,
782 description=description,
767 description_renderer=description_renderer,
783 description_renderer=description_renderer,
768 reviewer_data=default_reviewers_data,
784 reviewer_data=default_reviewers_data,
769 auth_user=apiuser
785 auth_user=apiuser
770 )
786 )
771
787
772 Session().commit()
788 Session().commit()
773 data = {
789 data = {
774 'msg': 'Created new pull request `{}`'.format(title),
790 'msg': 'Created new pull request `{}`'.format(title),
775 'pull_request_id': pull_request.pull_request_id,
791 'pull_request_id': pull_request.pull_request_id,
776 }
792 }
777 return data
793 return data
778
794
779
795
780 @jsonrpc_method()
796 @jsonrpc_method()
781 def update_pull_request(
797 def update_pull_request(
782 request, apiuser, pullrequestid, repoid=Optional(None),
798 request, apiuser, pullrequestid, repoid=Optional(None),
783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
799 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
784 reviewers=Optional(None), update_commits=Optional(None)):
800 reviewers=Optional(None), update_commits=Optional(None)):
785 """
801 """
786 Updates a pull request.
802 Updates a pull request.
787
803
788 :param apiuser: This is filled automatically from the |authtoken|.
804 :param apiuser: This is filled automatically from the |authtoken|.
789 :type apiuser: AuthUser
805 :type apiuser: AuthUser
790 :param repoid: Optional repository name or repository ID.
806 :param repoid: Optional repository name or repository ID.
791 :type repoid: str or int
807 :type repoid: str or int
792 :param pullrequestid: The pull request ID.
808 :param pullrequestid: The pull request ID.
793 :type pullrequestid: int
809 :type pullrequestid: int
794 :param title: Set the pull request title.
810 :param title: Set the pull request title.
795 :type title: str
811 :type title: str
796 :param description: Update pull request description.
812 :param description: Update pull request description.
797 :type description: Optional(str)
813 :type description: Optional(str)
798 :type description_renderer: Optional(str)
814 :type description_renderer: Optional(str)
799 :param description_renderer: Update pull request renderer for the description.
815 :param description_renderer: Update pull request renderer for the description.
800 It should be 'rst', 'markdown' or 'plain'
816 It should be 'rst', 'markdown' or 'plain'
801 :param reviewers: Update pull request reviewers list with new value.
817 :param reviewers: Update pull request reviewers list with new value.
802 :type reviewers: Optional(list)
818 :type reviewers: Optional(list)
803 Accepts username strings or objects of the format:
819 Accepts username strings or objects of the format:
804
820
805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
821 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
806
822
807 :param update_commits: Trigger update of commits for this pull request
823 :param update_commits: Trigger update of commits for this pull request
808 :type: update_commits: Optional(bool)
824 :type: update_commits: Optional(bool)
809
825
810 Example output:
826 Example output:
811
827
812 .. code-block:: bash
828 .. code-block:: bash
813
829
814 id : <id_given_in_input>
830 id : <id_given_in_input>
815 result : {
831 result : {
816 "msg": "Updated pull request `63`",
832 "msg": "Updated pull request `63`",
817 "pull_request": <pull_request_object>,
833 "pull_request": <pull_request_object>,
818 "updated_reviewers": {
834 "updated_reviewers": {
819 "added": [
835 "added": [
820 "username"
836 "username"
821 ],
837 ],
822 "removed": []
838 "removed": []
823 },
839 },
824 "updated_commits": {
840 "updated_commits": {
825 "added": [
841 "added": [
826 "<sha1_hash>"
842 "<sha1_hash>"
827 ],
843 ],
828 "common": [
844 "common": [
829 "<sha1_hash>",
845 "<sha1_hash>",
830 "<sha1_hash>",
846 "<sha1_hash>",
831 ],
847 ],
832 "removed": []
848 "removed": []
833 }
849 }
834 }
850 }
835 error : null
851 error : null
836 """
852 """
837
853
838 pull_request = get_pull_request_or_error(pullrequestid)
854 pull_request = get_pull_request_or_error(pullrequestid)
839 if Optional.extract(repoid):
855 if Optional.extract(repoid):
840 repo = get_repo_or_error(repoid)
856 repo = get_repo_or_error(repoid)
841 else:
857 else:
842 repo = pull_request.target_repo
858 repo = pull_request.target_repo
843
859
844 if not PullRequestModel().check_user_update(
860 if not PullRequestModel().check_user_update(
845 pull_request, apiuser, api=True):
861 pull_request, apiuser, api=True):
846 raise JSONRPCError(
862 raise JSONRPCError(
847 'pull request `%s` update failed, no permission to update.' % (
863 'pull request `%s` update failed, no permission to update.' % (
848 pullrequestid,))
864 pullrequestid,))
849 if pull_request.is_closed():
865 if pull_request.is_closed():
850 raise JSONRPCError(
866 raise JSONRPCError(
851 'pull request `%s` update failed, pull request is closed' % (
867 'pull request `%s` update failed, pull request is closed' % (
852 pullrequestid,))
868 pullrequestid,))
853
869
854 reviewer_objects = Optional.extract(reviewers) or []
870 reviewer_objects = Optional.extract(reviewers) or []
855
871
856 if reviewer_objects:
872 if reviewer_objects:
857 schema = ReviewerListSchema()
873 schema = ReviewerListSchema()
858 try:
874 try:
859 reviewer_objects = schema.deserialize(reviewer_objects)
875 reviewer_objects = schema.deserialize(reviewer_objects)
860 except Invalid as err:
876 except Invalid as err:
861 raise JSONRPCValidationError(colander_exc=err)
877 raise JSONRPCValidationError(colander_exc=err)
862
878
863 # validate users
879 # validate users
864 for reviewer_object in reviewer_objects:
880 for reviewer_object in reviewer_objects:
865 user = get_user_or_error(reviewer_object['username'])
881 user = get_user_or_error(reviewer_object['username'])
866 reviewer_object['user_id'] = user.user_id
882 reviewer_object['user_id'] = user.user_id
867
883
868 get_default_reviewers_data, get_validated_reviewers, validate_observers = \
884 get_default_reviewers_data, get_validated_reviewers, validate_observers = \
869 PullRequestModel().get_reviewer_functions()
885 PullRequestModel().get_reviewer_functions()
870
886
871 # re-use stored rules
887 # re-use stored rules
872 reviewer_rules = pull_request.reviewer_data
888 reviewer_rules = pull_request.reviewer_data
873 try:
889 try:
874 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
890 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
875 except ValueError as e:
891 except ValueError as e:
876 raise JSONRPCError('Reviewers Validation: {}'.format(e))
892 raise JSONRPCError('Reviewers Validation: {}'.format(e))
877 else:
893 else:
878 reviewers = []
894 reviewers = []
879
895
880 title = Optional.extract(title)
896 title = Optional.extract(title)
881 description = Optional.extract(description)
897 description = Optional.extract(description)
882 description_renderer = Optional.extract(description_renderer)
898 description_renderer = Optional.extract(description_renderer)
883
899
900 # Update title/description
901 title_changed = False
884 if title or description:
902 if title or description:
885 PullRequestModel().edit(
903 PullRequestModel().edit(
886 pull_request,
904 pull_request,
887 title or pull_request.title,
905 title or pull_request.title,
888 description or pull_request.description,
906 description or pull_request.description,
889 description_renderer or pull_request.description_renderer,
907 description_renderer or pull_request.description_renderer,
890 apiuser)
908 apiuser)
891 Session().commit()
909 Session().commit()
910 title_changed = True
892
911
893 commit_changes = {"added": [], "common": [], "removed": []}
912 commit_changes = {"added": [], "common": [], "removed": []}
913
914 # Update commits
915 commits_changed = False
894 if str2bool(Optional.extract(update_commits)):
916 if str2bool(Optional.extract(update_commits)):
895
917
896 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
918 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
897 raise JSONRPCError(
919 raise JSONRPCError(
898 'Operation forbidden because pull request is in state {}, '
920 'Operation forbidden because pull request is in state {}, '
899 'only state {} is allowed.'.format(
921 'only state {} is allowed.'.format(
900 pull_request.pull_request_state, PullRequest.STATE_CREATED))
922 pull_request.pull_request_state, PullRequest.STATE_CREATED))
901
923
902 with pull_request.set_state(PullRequest.STATE_UPDATING):
924 with pull_request.set_state(PullRequest.STATE_UPDATING):
903 if PullRequestModel().has_valid_update_type(pull_request):
925 if PullRequestModel().has_valid_update_type(pull_request):
904 db_user = apiuser.get_instance()
926 db_user = apiuser.get_instance()
905 update_response = PullRequestModel().update_commits(
927 update_response = PullRequestModel().update_commits(
906 pull_request, db_user)
928 pull_request, db_user)
907 commit_changes = update_response.changes or commit_changes
929 commit_changes = update_response.changes or commit_changes
908 Session().commit()
930 Session().commit()
931 commits_changed = True
909
932
933 # Update reviewers
934 reviewers_changed = False
910 reviewers_changes = {"added": [], "removed": []}
935 reviewers_changes = {"added": [], "removed": []}
911 if reviewers:
936 if reviewers:
912 old_calculated_status = pull_request.calculated_review_status()
937 old_calculated_status = pull_request.calculated_review_status()
913 added_reviewers, removed_reviewers = \
938 added_reviewers, removed_reviewers = \
914 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
939 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
915
940
916 reviewers_changes['added'] = sorted(
941 reviewers_changes['added'] = sorted(
917 [get_user_or_error(n).username for n in added_reviewers])
942 [get_user_or_error(n).username for n in added_reviewers])
918 reviewers_changes['removed'] = sorted(
943 reviewers_changes['removed'] = sorted(
919 [get_user_or_error(n).username for n in removed_reviewers])
944 [get_user_or_error(n).username for n in removed_reviewers])
920 Session().commit()
945 Session().commit()
921
946
922 # trigger status changed if change in reviewers changes the status
947 # trigger status changed if change in reviewers changes the status
923 calculated_status = pull_request.calculated_review_status()
948 calculated_status = pull_request.calculated_review_status()
924 if old_calculated_status != calculated_status:
949 if old_calculated_status != calculated_status:
925 PullRequestModel().trigger_pull_request_hook(
950 PullRequestModel().trigger_pull_request_hook(
926 pull_request, apiuser, 'review_status_change',
951 pull_request, apiuser, 'review_status_change',
927 data={'status': calculated_status})
952 data={'status': calculated_status})
953 reviewers_changed = True
954
955 observers_changed = False
956
957 # push changed to channelstream
958 if commits_changed or reviewers_changed or observers_changed:
959 pr_broadcast_channel = channelstream.pr_channel(pull_request)
960 msg = 'Pull request was updated.'
961 channelstream.pr_update_channelstream_push(
962 request, pr_broadcast_channel, apiuser, msg)
928
963
929 data = {
964 data = {
930 'msg': 'Updated pull request `{}`'.format(
965 'msg': 'Updated pull request `{}`'.format(
931 pull_request.pull_request_id),
966 pull_request.pull_request_id),
932 'pull_request': pull_request.get_api_data(),
967 'pull_request': pull_request.get_api_data(),
933 'updated_commits': commit_changes,
968 'updated_commits': commit_changes,
934 'updated_reviewers': reviewers_changes
969 'updated_reviewers': reviewers_changes
935 }
970 }
936
971
937 return data
972 return data
938
973
939
974
940 @jsonrpc_method()
975 @jsonrpc_method()
941 def close_pull_request(
976 def close_pull_request(
942 request, apiuser, pullrequestid, repoid=Optional(None),
977 request, apiuser, pullrequestid, repoid=Optional(None),
943 userid=Optional(OAttr('apiuser')), message=Optional('')):
978 userid=Optional(OAttr('apiuser')), message=Optional('')):
944 """
979 """
945 Close the pull request specified by `pullrequestid`.
980 Close the pull request specified by `pullrequestid`.
946
981
947 :param apiuser: This is filled automatically from the |authtoken|.
982 :param apiuser: This is filled automatically from the |authtoken|.
948 :type apiuser: AuthUser
983 :type apiuser: AuthUser
949 :param repoid: Repository name or repository ID to which the pull
984 :param repoid: Repository name or repository ID to which the pull
950 request belongs.
985 request belongs.
951 :type repoid: str or int
986 :type repoid: str or int
952 :param pullrequestid: ID of the pull request to be closed.
987 :param pullrequestid: ID of the pull request to be closed.
953 :type pullrequestid: int
988 :type pullrequestid: int
954 :param userid: Close the pull request as this user.
989 :param userid: Close the pull request as this user.
955 :type userid: Optional(str or int)
990 :type userid: Optional(str or int)
956 :param message: Optional message to close the Pull Request with. If not
991 :param message: Optional message to close the Pull Request with. If not
957 specified it will be generated automatically.
992 specified it will be generated automatically.
958 :type message: Optional(str)
993 :type message: Optional(str)
959
994
960 Example output:
995 Example output:
961
996
962 .. code-block:: bash
997 .. code-block:: bash
963
998
964 "id": <id_given_in_input>,
999 "id": <id_given_in_input>,
965 "result": {
1000 "result": {
966 "pull_request_id": "<int>",
1001 "pull_request_id": "<int>",
967 "close_status": "<str:status_lbl>,
1002 "close_status": "<str:status_lbl>,
968 "closed": "<bool>"
1003 "closed": "<bool>"
969 },
1004 },
970 "error": null
1005 "error": null
971
1006
972 """
1007 """
973 _ = request.translate
1008 _ = request.translate
974
1009
975 pull_request = get_pull_request_or_error(pullrequestid)
1010 pull_request = get_pull_request_or_error(pullrequestid)
976 if Optional.extract(repoid):
1011 if Optional.extract(repoid):
977 repo = get_repo_or_error(repoid)
1012 repo = get_repo_or_error(repoid)
978 else:
1013 else:
979 repo = pull_request.target_repo
1014 repo = pull_request.target_repo
980
1015
981 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1016 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
982 user=apiuser, repo_name=repo.repo_name)
1017 user=apiuser, repo_name=repo.repo_name)
983 if not isinstance(userid, Optional):
1018 if not isinstance(userid, Optional):
984 if has_superadmin_permission(apiuser) or is_repo_admin:
1019 if has_superadmin_permission(apiuser) or is_repo_admin:
985 apiuser = get_user_or_error(userid)
1020 apiuser = get_user_or_error(userid)
986 else:
1021 else:
987 raise JSONRPCError('userid is not the same as your user')
1022 raise JSONRPCError('userid is not the same as your user')
988
1023
989 if pull_request.is_closed():
1024 if pull_request.is_closed():
990 raise JSONRPCError(
1025 raise JSONRPCError(
991 'pull request `%s` is already closed' % (pullrequestid,))
1026 'pull request `%s` is already closed' % (pullrequestid,))
992
1027
993 # only owner or admin or person with write permissions
1028 # only owner or admin or person with write permissions
994 allowed_to_close = PullRequestModel().check_user_update(
1029 allowed_to_close = PullRequestModel().check_user_update(
995 pull_request, apiuser, api=True)
1030 pull_request, apiuser, api=True)
996
1031
997 if not allowed_to_close:
1032 if not allowed_to_close:
998 raise JSONRPCError(
1033 raise JSONRPCError(
999 'pull request `%s` close failed, no permission to close.' % (
1034 'pull request `%s` close failed, no permission to close.' % (
1000 pullrequestid,))
1035 pullrequestid,))
1001
1036
1002 # message we're using to close the PR, else it's automatically generated
1037 # message we're using to close the PR, else it's automatically generated
1003 message = Optional.extract(message)
1038 message = Optional.extract(message)
1004
1039
1005 # finally close the PR, with proper message comment
1040 # finally close the PR, with proper message comment
1006 comment, status = PullRequestModel().close_pull_request_with_comment(
1041 comment, status = PullRequestModel().close_pull_request_with_comment(
1007 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1042 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1008 status_lbl = ChangesetStatus.get_status_lbl(status)
1043 status_lbl = ChangesetStatus.get_status_lbl(status)
1009
1044
1010 Session().commit()
1045 Session().commit()
1011
1046
1012 data = {
1047 data = {
1013 'pull_request_id': pull_request.pull_request_id,
1048 'pull_request_id': pull_request.pull_request_id,
1014 'close_status': status_lbl,
1049 'close_status': status_lbl,
1015 'closed': True,
1050 'closed': True,
1016 }
1051 }
1017 return data
1052 return data
@@ -1,2507 +1,2523 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger, rc_cache
32 from rhodecode.lib import audit_logger, rc_cache, channelstream
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 HasRepoPermissionAnyApi)
36 HasRepoPermissionAnyApi)
37 from rhodecode.lib.celerylib.utils import get_task_id
37 from rhodecode.lib.celerylib.utils import get_task_id
38 from rhodecode.lib.utils2 import (
38 from rhodecode.lib.utils2 import (
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
40 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.exceptions import (
41 from rhodecode.lib.exceptions import (
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
43 from rhodecode.lib.vcs import RepositoryError
43 from rhodecode.lib.vcs import RepositoryError
44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (
47 from rhodecode.model.db import (
48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
49 ChangesetComment)
49 ChangesetComment)
50 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.pull_request import PullRequestModel
51 from rhodecode.model.pull_request import PullRequestModel
52 from rhodecode.model.repo import RepoModel
52 from rhodecode.model.repo import RepoModel
53 from rhodecode.model.scm import ScmModel, RepoList
53 from rhodecode.model.scm import ScmModel, RepoList
54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
55 from rhodecode.model import validation_schema
55 from rhodecode.model import validation_schema
56 from rhodecode.model.validation_schema.schemas import repo_schema
56 from rhodecode.model.validation_schema.schemas import repo_schema
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 @jsonrpc_method()
61 @jsonrpc_method()
62 def get_repo(request, apiuser, repoid, cache=Optional(True)):
62 def get_repo(request, apiuser, repoid, cache=Optional(True)):
63 """
63 """
64 Gets an existing repository by its name or repository_id.
64 Gets an existing repository by its name or repository_id.
65
65
66 The members section so the output returns users groups or users
66 The members section so the output returns users groups or users
67 associated with that repository.
67 associated with that repository.
68
68
69 This command can only be run using an |authtoken| with admin rights,
69 This command can only be run using an |authtoken| with admin rights,
70 or users with at least read rights to the |repo|.
70 or users with at least read rights to the |repo|.
71
71
72 :param apiuser: This is filled automatically from the |authtoken|.
72 :param apiuser: This is filled automatically from the |authtoken|.
73 :type apiuser: AuthUser
73 :type apiuser: AuthUser
74 :param repoid: The repository name or repository id.
74 :param repoid: The repository name or repository id.
75 :type repoid: str or int
75 :type repoid: str or int
76 :param cache: use the cached value for last changeset
76 :param cache: use the cached value for last changeset
77 :type: cache: Optional(bool)
77 :type: cache: Optional(bool)
78
78
79 Example output:
79 Example output:
80
80
81 .. code-block:: bash
81 .. code-block:: bash
82
82
83 {
83 {
84 "error": null,
84 "error": null,
85 "id": <repo_id>,
85 "id": <repo_id>,
86 "result": {
86 "result": {
87 "clone_uri": null,
87 "clone_uri": null,
88 "created_on": "timestamp",
88 "created_on": "timestamp",
89 "description": "repo description",
89 "description": "repo description",
90 "enable_downloads": false,
90 "enable_downloads": false,
91 "enable_locking": false,
91 "enable_locking": false,
92 "enable_statistics": false,
92 "enable_statistics": false,
93 "followers": [
93 "followers": [
94 {
94 {
95 "active": true,
95 "active": true,
96 "admin": false,
96 "admin": false,
97 "api_key": "****************************************",
97 "api_key": "****************************************",
98 "api_keys": [
98 "api_keys": [
99 "****************************************"
99 "****************************************"
100 ],
100 ],
101 "email": "user@example.com",
101 "email": "user@example.com",
102 "emails": [
102 "emails": [
103 "user@example.com"
103 "user@example.com"
104 ],
104 ],
105 "extern_name": "rhodecode",
105 "extern_name": "rhodecode",
106 "extern_type": "rhodecode",
106 "extern_type": "rhodecode",
107 "firstname": "username",
107 "firstname": "username",
108 "ip_addresses": [],
108 "ip_addresses": [],
109 "language": null,
109 "language": null,
110 "last_login": "2015-09-16T17:16:35.854",
110 "last_login": "2015-09-16T17:16:35.854",
111 "lastname": "surname",
111 "lastname": "surname",
112 "user_id": <user_id>,
112 "user_id": <user_id>,
113 "username": "name"
113 "username": "name"
114 }
114 }
115 ],
115 ],
116 "fork_of": "parent-repo",
116 "fork_of": "parent-repo",
117 "landing_rev": [
117 "landing_rev": [
118 "rev",
118 "rev",
119 "tip"
119 "tip"
120 ],
120 ],
121 "last_changeset": {
121 "last_changeset": {
122 "author": "User <user@example.com>",
122 "author": "User <user@example.com>",
123 "branch": "default",
123 "branch": "default",
124 "date": "timestamp",
124 "date": "timestamp",
125 "message": "last commit message",
125 "message": "last commit message",
126 "parents": [
126 "parents": [
127 {
127 {
128 "raw_id": "commit-id"
128 "raw_id": "commit-id"
129 }
129 }
130 ],
130 ],
131 "raw_id": "commit-id",
131 "raw_id": "commit-id",
132 "revision": <revision number>,
132 "revision": <revision number>,
133 "short_id": "short id"
133 "short_id": "short id"
134 },
134 },
135 "lock_reason": null,
135 "lock_reason": null,
136 "locked_by": null,
136 "locked_by": null,
137 "locked_date": null,
137 "locked_date": null,
138 "owner": "owner-name",
138 "owner": "owner-name",
139 "permissions": [
139 "permissions": [
140 {
140 {
141 "name": "super-admin-name",
141 "name": "super-admin-name",
142 "origin": "super-admin",
142 "origin": "super-admin",
143 "permission": "repository.admin",
143 "permission": "repository.admin",
144 "type": "user"
144 "type": "user"
145 },
145 },
146 {
146 {
147 "name": "owner-name",
147 "name": "owner-name",
148 "origin": "owner",
148 "origin": "owner",
149 "permission": "repository.admin",
149 "permission": "repository.admin",
150 "type": "user"
150 "type": "user"
151 },
151 },
152 {
152 {
153 "name": "user-group-name",
153 "name": "user-group-name",
154 "origin": "permission",
154 "origin": "permission",
155 "permission": "repository.write",
155 "permission": "repository.write",
156 "type": "user_group"
156 "type": "user_group"
157 }
157 }
158 ],
158 ],
159 "private": true,
159 "private": true,
160 "repo_id": 676,
160 "repo_id": 676,
161 "repo_name": "user-group/repo-name",
161 "repo_name": "user-group/repo-name",
162 "repo_type": "hg"
162 "repo_type": "hg"
163 }
163 }
164 }
164 }
165 """
165 """
166
166
167 repo = get_repo_or_error(repoid)
167 repo = get_repo_or_error(repoid)
168 cache = Optional.extract(cache)
168 cache = Optional.extract(cache)
169
169
170 include_secrets = False
170 include_secrets = False
171 if has_superadmin_permission(apiuser):
171 if has_superadmin_permission(apiuser):
172 include_secrets = True
172 include_secrets = True
173 else:
173 else:
174 # check if we have at least read permission for this repo !
174 # check if we have at least read permission for this repo !
175 _perms = (
175 _perms = (
176 'repository.admin', 'repository.write', 'repository.read',)
176 'repository.admin', 'repository.write', 'repository.read',)
177 validate_repo_permissions(apiuser, repoid, repo, _perms)
177 validate_repo_permissions(apiuser, repoid, repo, _perms)
178
178
179 permissions = []
179 permissions = []
180 for _user in repo.permissions():
180 for _user in repo.permissions():
181 user_data = {
181 user_data = {
182 'name': _user.username,
182 'name': _user.username,
183 'permission': _user.permission,
183 'permission': _user.permission,
184 'origin': get_origin(_user),
184 'origin': get_origin(_user),
185 'type': "user",
185 'type': "user",
186 }
186 }
187 permissions.append(user_data)
187 permissions.append(user_data)
188
188
189 for _user_group in repo.permission_user_groups():
189 for _user_group in repo.permission_user_groups():
190 user_group_data = {
190 user_group_data = {
191 'name': _user_group.users_group_name,
191 'name': _user_group.users_group_name,
192 'permission': _user_group.permission,
192 'permission': _user_group.permission,
193 'origin': get_origin(_user_group),
193 'origin': get_origin(_user_group),
194 'type': "user_group",
194 'type': "user_group",
195 }
195 }
196 permissions.append(user_group_data)
196 permissions.append(user_group_data)
197
197
198 following_users = [
198 following_users = [
199 user.user.get_api_data(include_secrets=include_secrets)
199 user.user.get_api_data(include_secrets=include_secrets)
200 for user in repo.followers]
200 for user in repo.followers]
201
201
202 if not cache:
202 if not cache:
203 repo.update_commit_cache()
203 repo.update_commit_cache()
204 data = repo.get_api_data(include_secrets=include_secrets)
204 data = repo.get_api_data(include_secrets=include_secrets)
205 data['permissions'] = permissions
205 data['permissions'] = permissions
206 data['followers'] = following_users
206 data['followers'] = following_users
207 return data
207 return data
208
208
209
209
210 @jsonrpc_method()
210 @jsonrpc_method()
211 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
211 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
212 """
212 """
213 Lists all existing repositories.
213 Lists all existing repositories.
214
214
215 This command can only be run using an |authtoken| with admin rights,
215 This command can only be run using an |authtoken| with admin rights,
216 or users with at least read rights to |repos|.
216 or users with at least read rights to |repos|.
217
217
218 :param apiuser: This is filled automatically from the |authtoken|.
218 :param apiuser: This is filled automatically from the |authtoken|.
219 :type apiuser: AuthUser
219 :type apiuser: AuthUser
220 :param root: specify root repository group to fetch repositories.
220 :param root: specify root repository group to fetch repositories.
221 filters the returned repositories to be members of given root group.
221 filters the returned repositories to be members of given root group.
222 :type root: Optional(None)
222 :type root: Optional(None)
223 :param traverse: traverse given root into subrepositories. With this flag
223 :param traverse: traverse given root into subrepositories. With this flag
224 set to False, it will only return top-level repositories from `root`.
224 set to False, it will only return top-level repositories from `root`.
225 if root is empty it will return just top-level repositories.
225 if root is empty it will return just top-level repositories.
226 :type traverse: Optional(True)
226 :type traverse: Optional(True)
227
227
228
228
229 Example output:
229 Example output:
230
230
231 .. code-block:: bash
231 .. code-block:: bash
232
232
233 id : <id_given_in_input>
233 id : <id_given_in_input>
234 result: [
234 result: [
235 {
235 {
236 "repo_id" : "<repo_id>",
236 "repo_id" : "<repo_id>",
237 "repo_name" : "<reponame>"
237 "repo_name" : "<reponame>"
238 "repo_type" : "<repo_type>",
238 "repo_type" : "<repo_type>",
239 "clone_uri" : "<clone_uri>",
239 "clone_uri" : "<clone_uri>",
240 "private": : "<bool>",
240 "private": : "<bool>",
241 "created_on" : "<datetimecreated>",
241 "created_on" : "<datetimecreated>",
242 "description" : "<description>",
242 "description" : "<description>",
243 "landing_rev": "<landing_rev>",
243 "landing_rev": "<landing_rev>",
244 "owner": "<repo_owner>",
244 "owner": "<repo_owner>",
245 "fork_of": "<name_of_fork_parent>",
245 "fork_of": "<name_of_fork_parent>",
246 "enable_downloads": "<bool>",
246 "enable_downloads": "<bool>",
247 "enable_locking": "<bool>",
247 "enable_locking": "<bool>",
248 "enable_statistics": "<bool>",
248 "enable_statistics": "<bool>",
249 },
249 },
250 ...
250 ...
251 ]
251 ]
252 error: null
252 error: null
253 """
253 """
254
254
255 include_secrets = has_superadmin_permission(apiuser)
255 include_secrets = has_superadmin_permission(apiuser)
256 _perms = ('repository.read', 'repository.write', 'repository.admin',)
256 _perms = ('repository.read', 'repository.write', 'repository.admin',)
257 extras = {'user': apiuser}
257 extras = {'user': apiuser}
258
258
259 root = Optional.extract(root)
259 root = Optional.extract(root)
260 traverse = Optional.extract(traverse, binary=True)
260 traverse = Optional.extract(traverse, binary=True)
261
261
262 if root:
262 if root:
263 # verify parent existance, if it's empty return an error
263 # verify parent existance, if it's empty return an error
264 parent = RepoGroup.get_by_group_name(root)
264 parent = RepoGroup.get_by_group_name(root)
265 if not parent:
265 if not parent:
266 raise JSONRPCError(
266 raise JSONRPCError(
267 'Root repository group `{}` does not exist'.format(root))
267 'Root repository group `{}` does not exist'.format(root))
268
268
269 if traverse:
269 if traverse:
270 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
270 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
271 else:
271 else:
272 repos = RepoModel().get_repos_for_root(root=parent)
272 repos = RepoModel().get_repos_for_root(root=parent)
273 else:
273 else:
274 if traverse:
274 if traverse:
275 repos = RepoModel().get_all()
275 repos = RepoModel().get_all()
276 else:
276 else:
277 # return just top-level
277 # return just top-level
278 repos = RepoModel().get_repos_for_root(root=None)
278 repos = RepoModel().get_repos_for_root(root=None)
279
279
280 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
280 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
281 return [repo.get_api_data(include_secrets=include_secrets)
281 return [repo.get_api_data(include_secrets=include_secrets)
282 for repo in repo_list]
282 for repo in repo_list]
283
283
284
284
285 @jsonrpc_method()
285 @jsonrpc_method()
286 def get_repo_changeset(request, apiuser, repoid, revision,
286 def get_repo_changeset(request, apiuser, repoid, revision,
287 details=Optional('basic')):
287 details=Optional('basic')):
288 """
288 """
289 Returns information about a changeset.
289 Returns information about a changeset.
290
290
291 Additionally parameters define the amount of details returned by
291 Additionally parameters define the amount of details returned by
292 this function.
292 this function.
293
293
294 This command can only be run using an |authtoken| with admin rights,
294 This command can only be run using an |authtoken| with admin rights,
295 or users with at least read rights to the |repo|.
295 or users with at least read rights to the |repo|.
296
296
297 :param apiuser: This is filled automatically from the |authtoken|.
297 :param apiuser: This is filled automatically from the |authtoken|.
298 :type apiuser: AuthUser
298 :type apiuser: AuthUser
299 :param repoid: The repository name or repository id
299 :param repoid: The repository name or repository id
300 :type repoid: str or int
300 :type repoid: str or int
301 :param revision: revision for which listing should be done
301 :param revision: revision for which listing should be done
302 :type revision: str
302 :type revision: str
303 :param details: details can be 'basic|extended|full' full gives diff
303 :param details: details can be 'basic|extended|full' full gives diff
304 info details like the diff itself, and number of changed files etc.
304 info details like the diff itself, and number of changed files etc.
305 :type details: Optional(str)
305 :type details: Optional(str)
306
306
307 """
307 """
308 repo = get_repo_or_error(repoid)
308 repo = get_repo_or_error(repoid)
309 if not has_superadmin_permission(apiuser):
309 if not has_superadmin_permission(apiuser):
310 _perms = ('repository.admin', 'repository.write', 'repository.read',)
310 _perms = ('repository.admin', 'repository.write', 'repository.read',)
311 validate_repo_permissions(apiuser, repoid, repo, _perms)
311 validate_repo_permissions(apiuser, repoid, repo, _perms)
312
312
313 changes_details = Optional.extract(details)
313 changes_details = Optional.extract(details)
314 _changes_details_types = ['basic', 'extended', 'full']
314 _changes_details_types = ['basic', 'extended', 'full']
315 if changes_details not in _changes_details_types:
315 if changes_details not in _changes_details_types:
316 raise JSONRPCError(
316 raise JSONRPCError(
317 'ret_type must be one of %s' % (
317 'ret_type must be one of %s' % (
318 ','.join(_changes_details_types)))
318 ','.join(_changes_details_types)))
319
319
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
321 'status', '_commit', '_file_paths']
321 'status', '_commit', '_file_paths']
322
322
323 try:
323 try:
324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
325 except TypeError as e:
325 except TypeError as e:
326 raise JSONRPCError(safe_str(e))
326 raise JSONRPCError(safe_str(e))
327 _cs_json = cs.__json__()
327 _cs_json = cs.__json__()
328 _cs_json['diff'] = build_commit_data(cs, changes_details)
328 _cs_json['diff'] = build_commit_data(cs, changes_details)
329 if changes_details == 'full':
329 if changes_details == 'full':
330 _cs_json['refs'] = cs._get_refs()
330 _cs_json['refs'] = cs._get_refs()
331 return _cs_json
331 return _cs_json
332
332
333
333
334 @jsonrpc_method()
334 @jsonrpc_method()
335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
336 details=Optional('basic')):
336 details=Optional('basic')):
337 """
337 """
338 Returns a set of commits limited by the number starting
338 Returns a set of commits limited by the number starting
339 from the `start_rev` option.
339 from the `start_rev` option.
340
340
341 Additional parameters define the amount of details returned by this
341 Additional parameters define the amount of details returned by this
342 function.
342 function.
343
343
344 This command can only be run using an |authtoken| with admin rights,
344 This command can only be run using an |authtoken| with admin rights,
345 or users with at least read rights to |repos|.
345 or users with at least read rights to |repos|.
346
346
347 :param apiuser: This is filled automatically from the |authtoken|.
347 :param apiuser: This is filled automatically from the |authtoken|.
348 :type apiuser: AuthUser
348 :type apiuser: AuthUser
349 :param repoid: The repository name or repository ID.
349 :param repoid: The repository name or repository ID.
350 :type repoid: str or int
350 :type repoid: str or int
351 :param start_rev: The starting revision from where to get changesets.
351 :param start_rev: The starting revision from where to get changesets.
352 :type start_rev: str
352 :type start_rev: str
353 :param limit: Limit the number of commits to this amount
353 :param limit: Limit the number of commits to this amount
354 :type limit: str or int
354 :type limit: str or int
355 :param details: Set the level of detail returned. Valid option are:
355 :param details: Set the level of detail returned. Valid option are:
356 ``basic``, ``extended`` and ``full``.
356 ``basic``, ``extended`` and ``full``.
357 :type details: Optional(str)
357 :type details: Optional(str)
358
358
359 .. note::
359 .. note::
360
360
361 Setting the parameter `details` to the value ``full`` is extensive
361 Setting the parameter `details` to the value ``full`` is extensive
362 and returns details like the diff itself, and the number
362 and returns details like the diff itself, and the number
363 of changed files.
363 of changed files.
364
364
365 """
365 """
366 repo = get_repo_or_error(repoid)
366 repo = get_repo_or_error(repoid)
367 if not has_superadmin_permission(apiuser):
367 if not has_superadmin_permission(apiuser):
368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
369 validate_repo_permissions(apiuser, repoid, repo, _perms)
369 validate_repo_permissions(apiuser, repoid, repo, _perms)
370
370
371 changes_details = Optional.extract(details)
371 changes_details = Optional.extract(details)
372 _changes_details_types = ['basic', 'extended', 'full']
372 _changes_details_types = ['basic', 'extended', 'full']
373 if changes_details not in _changes_details_types:
373 if changes_details not in _changes_details_types:
374 raise JSONRPCError(
374 raise JSONRPCError(
375 'ret_type must be one of %s' % (
375 'ret_type must be one of %s' % (
376 ','.join(_changes_details_types)))
376 ','.join(_changes_details_types)))
377
377
378 limit = int(limit)
378 limit = int(limit)
379 pre_load = ['author', 'branch', 'date', 'message', 'parents',
379 pre_load = ['author', 'branch', 'date', 'message', 'parents',
380 'status', '_commit', '_file_paths']
380 'status', '_commit', '_file_paths']
381
381
382 vcs_repo = repo.scm_instance()
382 vcs_repo = repo.scm_instance()
383 # SVN needs a special case to distinguish its index and commit id
383 # SVN needs a special case to distinguish its index and commit id
384 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
384 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
385 start_rev = vcs_repo.commit_ids[0]
385 start_rev = vcs_repo.commit_ids[0]
386
386
387 try:
387 try:
388 commits = vcs_repo.get_commits(
388 commits = vcs_repo.get_commits(
389 start_id=start_rev, pre_load=pre_load, translate_tags=False)
389 start_id=start_rev, pre_load=pre_load, translate_tags=False)
390 except TypeError as e:
390 except TypeError as e:
391 raise JSONRPCError(safe_str(e))
391 raise JSONRPCError(safe_str(e))
392 except Exception:
392 except Exception:
393 log.exception('Fetching of commits failed')
393 log.exception('Fetching of commits failed')
394 raise JSONRPCError('Error occurred during commit fetching')
394 raise JSONRPCError('Error occurred during commit fetching')
395
395
396 ret = []
396 ret = []
397 for cnt, commit in enumerate(commits):
397 for cnt, commit in enumerate(commits):
398 if cnt >= limit != -1:
398 if cnt >= limit != -1:
399 break
399 break
400 _cs_json = commit.__json__()
400 _cs_json = commit.__json__()
401 _cs_json['diff'] = build_commit_data(commit, changes_details)
401 _cs_json['diff'] = build_commit_data(commit, changes_details)
402 if changes_details == 'full':
402 if changes_details == 'full':
403 _cs_json['refs'] = {
403 _cs_json['refs'] = {
404 'branches': [commit.branch],
404 'branches': [commit.branch],
405 'bookmarks': getattr(commit, 'bookmarks', []),
405 'bookmarks': getattr(commit, 'bookmarks', []),
406 'tags': commit.tags
406 'tags': commit.tags
407 }
407 }
408 ret.append(_cs_json)
408 ret.append(_cs_json)
409 return ret
409 return ret
410
410
411
411
412 @jsonrpc_method()
412 @jsonrpc_method()
413 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
413 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
414 ret_type=Optional('all'), details=Optional('basic'),
414 ret_type=Optional('all'), details=Optional('basic'),
415 max_file_bytes=Optional(None)):
415 max_file_bytes=Optional(None)):
416 """
416 """
417 Returns a list of nodes and children in a flat list for a given
417 Returns a list of nodes and children in a flat list for a given
418 path at given revision.
418 path at given revision.
419
419
420 It's possible to specify ret_type to show only `files` or `dirs`.
420 It's possible to specify ret_type to show only `files` or `dirs`.
421
421
422 This command can only be run using an |authtoken| with admin rights,
422 This command can only be run using an |authtoken| with admin rights,
423 or users with at least read rights to |repos|.
423 or users with at least read rights to |repos|.
424
424
425 :param apiuser: This is filled automatically from the |authtoken|.
425 :param apiuser: This is filled automatically from the |authtoken|.
426 :type apiuser: AuthUser
426 :type apiuser: AuthUser
427 :param repoid: The repository name or repository ID.
427 :param repoid: The repository name or repository ID.
428 :type repoid: str or int
428 :type repoid: str or int
429 :param revision: The revision for which listing should be done.
429 :param revision: The revision for which listing should be done.
430 :type revision: str
430 :type revision: str
431 :param root_path: The path from which to start displaying.
431 :param root_path: The path from which to start displaying.
432 :type root_path: str
432 :type root_path: str
433 :param ret_type: Set the return type. Valid options are
433 :param ret_type: Set the return type. Valid options are
434 ``all`` (default), ``files`` and ``dirs``.
434 ``all`` (default), ``files`` and ``dirs``.
435 :type ret_type: Optional(str)
435 :type ret_type: Optional(str)
436 :param details: Returns extended information about nodes, such as
436 :param details: Returns extended information about nodes, such as
437 md5, binary, and or content.
437 md5, binary, and or content.
438 The valid options are ``basic`` and ``full``.
438 The valid options are ``basic`` and ``full``.
439 :type details: Optional(str)
439 :type details: Optional(str)
440 :param max_file_bytes: Only return file content under this file size bytes
440 :param max_file_bytes: Only return file content under this file size bytes
441 :type details: Optional(int)
441 :type details: Optional(int)
442
442
443 Example output:
443 Example output:
444
444
445 .. code-block:: bash
445 .. code-block:: bash
446
446
447 id : <id_given_in_input>
447 id : <id_given_in_input>
448 result: [
448 result: [
449 {
449 {
450 "binary": false,
450 "binary": false,
451 "content": "File line",
451 "content": "File line",
452 "extension": "md",
452 "extension": "md",
453 "lines": 2,
453 "lines": 2,
454 "md5": "059fa5d29b19c0657e384749480f6422",
454 "md5": "059fa5d29b19c0657e384749480f6422",
455 "mimetype": "text/x-minidsrc",
455 "mimetype": "text/x-minidsrc",
456 "name": "file.md",
456 "name": "file.md",
457 "size": 580,
457 "size": 580,
458 "type": "file"
458 "type": "file"
459 },
459 },
460 ...
460 ...
461 ]
461 ]
462 error: null
462 error: null
463 """
463 """
464
464
465 repo = get_repo_or_error(repoid)
465 repo = get_repo_or_error(repoid)
466 if not has_superadmin_permission(apiuser):
466 if not has_superadmin_permission(apiuser):
467 _perms = ('repository.admin', 'repository.write', 'repository.read',)
467 _perms = ('repository.admin', 'repository.write', 'repository.read',)
468 validate_repo_permissions(apiuser, repoid, repo, _perms)
468 validate_repo_permissions(apiuser, repoid, repo, _perms)
469
469
470 ret_type = Optional.extract(ret_type)
470 ret_type = Optional.extract(ret_type)
471 details = Optional.extract(details)
471 details = Optional.extract(details)
472 _extended_types = ['basic', 'full']
472 _extended_types = ['basic', 'full']
473 if details not in _extended_types:
473 if details not in _extended_types:
474 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
474 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
475 extended_info = False
475 extended_info = False
476 content = False
476 content = False
477 if details == 'basic':
477 if details == 'basic':
478 extended_info = True
478 extended_info = True
479
479
480 if details == 'full':
480 if details == 'full':
481 extended_info = content = True
481 extended_info = content = True
482
482
483 _map = {}
483 _map = {}
484 try:
484 try:
485 # check if repo is not empty by any chance, skip quicker if it is.
485 # check if repo is not empty by any chance, skip quicker if it is.
486 _scm = repo.scm_instance()
486 _scm = repo.scm_instance()
487 if _scm.is_empty():
487 if _scm.is_empty():
488 return []
488 return []
489
489
490 _d, _f = ScmModel().get_nodes(
490 _d, _f = ScmModel().get_nodes(
491 repo, revision, root_path, flat=False,
491 repo, revision, root_path, flat=False,
492 extended_info=extended_info, content=content,
492 extended_info=extended_info, content=content,
493 max_file_bytes=max_file_bytes)
493 max_file_bytes=max_file_bytes)
494 _map = {
494 _map = {
495 'all': _d + _f,
495 'all': _d + _f,
496 'files': _f,
496 'files': _f,
497 'dirs': _d,
497 'dirs': _d,
498 }
498 }
499 return _map[ret_type]
499 return _map[ret_type]
500 except KeyError:
500 except KeyError:
501 raise JSONRPCError(
501 raise JSONRPCError(
502 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
502 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
503 except Exception:
503 except Exception:
504 log.exception("Exception occurred while trying to get repo nodes")
504 log.exception("Exception occurred while trying to get repo nodes")
505 raise JSONRPCError(
505 raise JSONRPCError(
506 'failed to get repo: `%s` nodes' % repo.repo_name
506 'failed to get repo: `%s` nodes' % repo.repo_name
507 )
507 )
508
508
509
509
510 @jsonrpc_method()
510 @jsonrpc_method()
511 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
511 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
512 max_file_bytes=Optional(None), details=Optional('basic'),
512 max_file_bytes=Optional(None), details=Optional('basic'),
513 cache=Optional(True)):
513 cache=Optional(True)):
514 """
514 """
515 Returns a single file from repository at given revision.
515 Returns a single file from repository at given revision.
516
516
517 This command can only be run using an |authtoken| with admin rights,
517 This command can only be run using an |authtoken| with admin rights,
518 or users with at least read rights to |repos|.
518 or users with at least read rights to |repos|.
519
519
520 :param apiuser: This is filled automatically from the |authtoken|.
520 :param apiuser: This is filled automatically from the |authtoken|.
521 :type apiuser: AuthUser
521 :type apiuser: AuthUser
522 :param repoid: The repository name or repository ID.
522 :param repoid: The repository name or repository ID.
523 :type repoid: str or int
523 :type repoid: str or int
524 :param commit_id: The revision for which listing should be done.
524 :param commit_id: The revision for which listing should be done.
525 :type commit_id: str
525 :type commit_id: str
526 :param file_path: The path from which to start displaying.
526 :param file_path: The path from which to start displaying.
527 :type file_path: str
527 :type file_path: str
528 :param details: Returns different set of information about nodes.
528 :param details: Returns different set of information about nodes.
529 The valid options are ``minimal`` ``basic`` and ``full``.
529 The valid options are ``minimal`` ``basic`` and ``full``.
530 :type details: Optional(str)
530 :type details: Optional(str)
531 :param max_file_bytes: Only return file content under this file size bytes
531 :param max_file_bytes: Only return file content under this file size bytes
532 :type max_file_bytes: Optional(int)
532 :type max_file_bytes: Optional(int)
533 :param cache: Use internal caches for fetching files. If disabled fetching
533 :param cache: Use internal caches for fetching files. If disabled fetching
534 files is slower but more memory efficient
534 files is slower but more memory efficient
535 :type cache: Optional(bool)
535 :type cache: Optional(bool)
536
536
537 Example output:
537 Example output:
538
538
539 .. code-block:: bash
539 .. code-block:: bash
540
540
541 id : <id_given_in_input>
541 id : <id_given_in_input>
542 result: {
542 result: {
543 "binary": false,
543 "binary": false,
544 "extension": "py",
544 "extension": "py",
545 "lines": 35,
545 "lines": 35,
546 "content": "....",
546 "content": "....",
547 "md5": "76318336366b0f17ee249e11b0c99c41",
547 "md5": "76318336366b0f17ee249e11b0c99c41",
548 "mimetype": "text/x-python",
548 "mimetype": "text/x-python",
549 "name": "python.py",
549 "name": "python.py",
550 "size": 817,
550 "size": 817,
551 "type": "file",
551 "type": "file",
552 }
552 }
553 error: null
553 error: null
554 """
554 """
555
555
556 repo = get_repo_or_error(repoid)
556 repo = get_repo_or_error(repoid)
557 if not has_superadmin_permission(apiuser):
557 if not has_superadmin_permission(apiuser):
558 _perms = ('repository.admin', 'repository.write', 'repository.read',)
558 _perms = ('repository.admin', 'repository.write', 'repository.read',)
559 validate_repo_permissions(apiuser, repoid, repo, _perms)
559 validate_repo_permissions(apiuser, repoid, repo, _perms)
560
560
561 cache = Optional.extract(cache, binary=True)
561 cache = Optional.extract(cache, binary=True)
562 details = Optional.extract(details)
562 details = Optional.extract(details)
563 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
563 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
564 if details not in _extended_types:
564 if details not in _extended_types:
565 raise JSONRPCError(
565 raise JSONRPCError(
566 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
566 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
567 extended_info = False
567 extended_info = False
568 content = False
568 content = False
569
569
570 if details == 'minimal':
570 if details == 'minimal':
571 extended_info = False
571 extended_info = False
572
572
573 elif details == 'basic':
573 elif details == 'basic':
574 extended_info = True
574 extended_info = True
575
575
576 elif details == 'full':
576 elif details == 'full':
577 extended_info = content = True
577 extended_info = content = True
578
578
579 file_path = safe_unicode(file_path)
579 file_path = safe_unicode(file_path)
580 try:
580 try:
581 # check if repo is not empty by any chance, skip quicker if it is.
581 # check if repo is not empty by any chance, skip quicker if it is.
582 _scm = repo.scm_instance()
582 _scm = repo.scm_instance()
583 if _scm.is_empty():
583 if _scm.is_empty():
584 return None
584 return None
585
585
586 node = ScmModel().get_node(
586 node = ScmModel().get_node(
587 repo, commit_id, file_path, extended_info=extended_info,
587 repo, commit_id, file_path, extended_info=extended_info,
588 content=content, max_file_bytes=max_file_bytes, cache=cache)
588 content=content, max_file_bytes=max_file_bytes, cache=cache)
589 except NodeDoesNotExistError:
589 except NodeDoesNotExistError:
590 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
590 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
591 repo.repo_name, file_path, commit_id))
591 repo.repo_name, file_path, commit_id))
592 except Exception:
592 except Exception:
593 log.exception(u"Exception occurred while trying to get repo %s file",
593 log.exception(u"Exception occurred while trying to get repo %s file",
594 repo.repo_name)
594 repo.repo_name)
595 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
595 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
596 repo.repo_name, file_path))
596 repo.repo_name, file_path))
597
597
598 return node
598 return node
599
599
600
600
601 @jsonrpc_method()
601 @jsonrpc_method()
602 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
602 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
603 """
603 """
604 Returns a list of tree nodes for path at given revision. This api is built
604 Returns a list of tree nodes for path at given revision. This api is built
605 strictly for usage in full text search building, and shouldn't be consumed
605 strictly for usage in full text search building, and shouldn't be consumed
606
606
607 This command can only be run using an |authtoken| with admin rights,
607 This command can only be run using an |authtoken| with admin rights,
608 or users with at least read rights to |repos|.
608 or users with at least read rights to |repos|.
609
609
610 """
610 """
611
611
612 repo = get_repo_or_error(repoid)
612 repo = get_repo_or_error(repoid)
613 if not has_superadmin_permission(apiuser):
613 if not has_superadmin_permission(apiuser):
614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
615 validate_repo_permissions(apiuser, repoid, repo, _perms)
615 validate_repo_permissions(apiuser, repoid, repo, _perms)
616
616
617 repo_id = repo.repo_id
617 repo_id = repo.repo_id
618 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
618 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
619 cache_on = cache_seconds > 0
619 cache_on = cache_seconds > 0
620
620
621 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
621 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
622 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
622 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
623
623
624 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
624 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
625 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
625 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
626
626
627 try:
627 try:
628 # check if repo is not empty by any chance, skip quicker if it is.
628 # check if repo is not empty by any chance, skip quicker if it is.
629 _scm = repo.scm_instance()
629 _scm = repo.scm_instance()
630 if _scm.is_empty():
630 if _scm.is_empty():
631 return []
631 return []
632 except RepositoryError:
632 except RepositoryError:
633 log.exception("Exception occurred while trying to get repo nodes")
633 log.exception("Exception occurred while trying to get repo nodes")
634 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
634 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
635
635
636 try:
636 try:
637 # we need to resolve commit_id to a FULL sha for cache to work correctly.
637 # we need to resolve commit_id to a FULL sha for cache to work correctly.
638 # sending 'master' is a pointer that needs to be translated to current commit.
638 # sending 'master' is a pointer that needs to be translated to current commit.
639 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
639 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
640 log.debug(
640 log.debug(
641 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
641 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
642 'with caching: %s[TTL: %ss]' % (
642 'with caching: %s[TTL: %ss]' % (
643 repo_id, commit_id, cache_on, cache_seconds or 0))
643 repo_id, commit_id, cache_on, cache_seconds or 0))
644
644
645 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
645 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
646 return tree_files
646 return tree_files
647
647
648 except Exception:
648 except Exception:
649 log.exception("Exception occurred while trying to get repo nodes")
649 log.exception("Exception occurred while trying to get repo nodes")
650 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
650 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
651
651
652
652
653 @jsonrpc_method()
653 @jsonrpc_method()
654 def get_repo_refs(request, apiuser, repoid):
654 def get_repo_refs(request, apiuser, repoid):
655 """
655 """
656 Returns a dictionary of current references. It returns
656 Returns a dictionary of current references. It returns
657 bookmarks, branches, closed_branches, and tags for given repository
657 bookmarks, branches, closed_branches, and tags for given repository
658
658
659 It's possible to specify ret_type to show only `files` or `dirs`.
659 It's possible to specify ret_type to show only `files` or `dirs`.
660
660
661 This command can only be run using an |authtoken| with admin rights,
661 This command can only be run using an |authtoken| with admin rights,
662 or users with at least read rights to |repos|.
662 or users with at least read rights to |repos|.
663
663
664 :param apiuser: This is filled automatically from the |authtoken|.
664 :param apiuser: This is filled automatically from the |authtoken|.
665 :type apiuser: AuthUser
665 :type apiuser: AuthUser
666 :param repoid: The repository name or repository ID.
666 :param repoid: The repository name or repository ID.
667 :type repoid: str or int
667 :type repoid: str or int
668
668
669 Example output:
669 Example output:
670
670
671 .. code-block:: bash
671 .. code-block:: bash
672
672
673 id : <id_given_in_input>
673 id : <id_given_in_input>
674 "result": {
674 "result": {
675 "bookmarks": {
675 "bookmarks": {
676 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
676 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
677 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
677 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
678 },
678 },
679 "branches": {
679 "branches": {
680 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
680 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
681 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
681 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
682 },
682 },
683 "branches_closed": {},
683 "branches_closed": {},
684 "tags": {
684 "tags": {
685 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
685 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
686 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
686 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
687 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
687 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
688 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
688 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
689 }
689 }
690 }
690 }
691 error: null
691 error: null
692 """
692 """
693
693
694 repo = get_repo_or_error(repoid)
694 repo = get_repo_or_error(repoid)
695 if not has_superadmin_permission(apiuser):
695 if not has_superadmin_permission(apiuser):
696 _perms = ('repository.admin', 'repository.write', 'repository.read',)
696 _perms = ('repository.admin', 'repository.write', 'repository.read',)
697 validate_repo_permissions(apiuser, repoid, repo, _perms)
697 validate_repo_permissions(apiuser, repoid, repo, _perms)
698
698
699 try:
699 try:
700 # check if repo is not empty by any chance, skip quicker if it is.
700 # check if repo is not empty by any chance, skip quicker if it is.
701 vcs_instance = repo.scm_instance()
701 vcs_instance = repo.scm_instance()
702 refs = vcs_instance.refs()
702 refs = vcs_instance.refs()
703 return refs
703 return refs
704 except Exception:
704 except Exception:
705 log.exception("Exception occurred while trying to get repo refs")
705 log.exception("Exception occurred while trying to get repo refs")
706 raise JSONRPCError(
706 raise JSONRPCError(
707 'failed to get repo: `%s` references' % repo.repo_name
707 'failed to get repo: `%s` references' % repo.repo_name
708 )
708 )
709
709
710
710
711 @jsonrpc_method()
711 @jsonrpc_method()
712 def create_repo(
712 def create_repo(
713 request, apiuser, repo_name, repo_type,
713 request, apiuser, repo_name, repo_type,
714 owner=Optional(OAttr('apiuser')),
714 owner=Optional(OAttr('apiuser')),
715 description=Optional(''),
715 description=Optional(''),
716 private=Optional(False),
716 private=Optional(False),
717 clone_uri=Optional(None),
717 clone_uri=Optional(None),
718 push_uri=Optional(None),
718 push_uri=Optional(None),
719 landing_rev=Optional(None),
719 landing_rev=Optional(None),
720 enable_statistics=Optional(False),
720 enable_statistics=Optional(False),
721 enable_locking=Optional(False),
721 enable_locking=Optional(False),
722 enable_downloads=Optional(False),
722 enable_downloads=Optional(False),
723 copy_permissions=Optional(False)):
723 copy_permissions=Optional(False)):
724 """
724 """
725 Creates a repository.
725 Creates a repository.
726
726
727 * If the repository name contains "/", repository will be created inside
727 * If the repository name contains "/", repository will be created inside
728 a repository group or nested repository groups
728 a repository group or nested repository groups
729
729
730 For example "foo/bar/repo1" will create |repo| called "repo1" inside
730 For example "foo/bar/repo1" will create |repo| called "repo1" inside
731 group "foo/bar". You have to have permissions to access and write to
731 group "foo/bar". You have to have permissions to access and write to
732 the last repository group ("bar" in this example)
732 the last repository group ("bar" in this example)
733
733
734 This command can only be run using an |authtoken| with at least
734 This command can only be run using an |authtoken| with at least
735 permissions to create repositories, or write permissions to
735 permissions to create repositories, or write permissions to
736 parent repository groups.
736 parent repository groups.
737
737
738 :param apiuser: This is filled automatically from the |authtoken|.
738 :param apiuser: This is filled automatically from the |authtoken|.
739 :type apiuser: AuthUser
739 :type apiuser: AuthUser
740 :param repo_name: Set the repository name.
740 :param repo_name: Set the repository name.
741 :type repo_name: str
741 :type repo_name: str
742 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
742 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
743 :type repo_type: str
743 :type repo_type: str
744 :param owner: user_id or username
744 :param owner: user_id or username
745 :type owner: Optional(str)
745 :type owner: Optional(str)
746 :param description: Set the repository description.
746 :param description: Set the repository description.
747 :type description: Optional(str)
747 :type description: Optional(str)
748 :param private: set repository as private
748 :param private: set repository as private
749 :type private: bool
749 :type private: bool
750 :param clone_uri: set clone_uri
750 :param clone_uri: set clone_uri
751 :type clone_uri: str
751 :type clone_uri: str
752 :param push_uri: set push_uri
752 :param push_uri: set push_uri
753 :type push_uri: str
753 :type push_uri: str
754 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
754 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
755 :type landing_rev: str
755 :type landing_rev: str
756 :param enable_locking:
756 :param enable_locking:
757 :type enable_locking: bool
757 :type enable_locking: bool
758 :param enable_downloads:
758 :param enable_downloads:
759 :type enable_downloads: bool
759 :type enable_downloads: bool
760 :param enable_statistics:
760 :param enable_statistics:
761 :type enable_statistics: bool
761 :type enable_statistics: bool
762 :param copy_permissions: Copy permission from group in which the
762 :param copy_permissions: Copy permission from group in which the
763 repository is being created.
763 repository is being created.
764 :type copy_permissions: bool
764 :type copy_permissions: bool
765
765
766
766
767 Example output:
767 Example output:
768
768
769 .. code-block:: bash
769 .. code-block:: bash
770
770
771 id : <id_given_in_input>
771 id : <id_given_in_input>
772 result: {
772 result: {
773 "msg": "Created new repository `<reponame>`",
773 "msg": "Created new repository `<reponame>`",
774 "success": true,
774 "success": true,
775 "task": "<celery task id or None if done sync>"
775 "task": "<celery task id or None if done sync>"
776 }
776 }
777 error: null
777 error: null
778
778
779
779
780 Example error output:
780 Example error output:
781
781
782 .. code-block:: bash
782 .. code-block:: bash
783
783
784 id : <id_given_in_input>
784 id : <id_given_in_input>
785 result : null
785 result : null
786 error : {
786 error : {
787 'failed to create repository `<repo_name>`'
787 'failed to create repository `<repo_name>`'
788 }
788 }
789
789
790 """
790 """
791
791
792 owner = validate_set_owner_permissions(apiuser, owner)
792 owner = validate_set_owner_permissions(apiuser, owner)
793
793
794 description = Optional.extract(description)
794 description = Optional.extract(description)
795 copy_permissions = Optional.extract(copy_permissions)
795 copy_permissions = Optional.extract(copy_permissions)
796 clone_uri = Optional.extract(clone_uri)
796 clone_uri = Optional.extract(clone_uri)
797 push_uri = Optional.extract(push_uri)
797 push_uri = Optional.extract(push_uri)
798
798
799 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
799 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
800 if isinstance(private, Optional):
800 if isinstance(private, Optional):
801 private = defs.get('repo_private') or Optional.extract(private)
801 private = defs.get('repo_private') or Optional.extract(private)
802 if isinstance(repo_type, Optional):
802 if isinstance(repo_type, Optional):
803 repo_type = defs.get('repo_type')
803 repo_type = defs.get('repo_type')
804 if isinstance(enable_statistics, Optional):
804 if isinstance(enable_statistics, Optional):
805 enable_statistics = defs.get('repo_enable_statistics')
805 enable_statistics = defs.get('repo_enable_statistics')
806 if isinstance(enable_locking, Optional):
806 if isinstance(enable_locking, Optional):
807 enable_locking = defs.get('repo_enable_locking')
807 enable_locking = defs.get('repo_enable_locking')
808 if isinstance(enable_downloads, Optional):
808 if isinstance(enable_downloads, Optional):
809 enable_downloads = defs.get('repo_enable_downloads')
809 enable_downloads = defs.get('repo_enable_downloads')
810
810
811 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
811 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
812 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
812 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
813 ref_choices = list(set(ref_choices + [landing_ref]))
813 ref_choices = list(set(ref_choices + [landing_ref]))
814
814
815 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
815 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
816
816
817 schema = repo_schema.RepoSchema().bind(
817 schema = repo_schema.RepoSchema().bind(
818 repo_type_options=rhodecode.BACKENDS.keys(),
818 repo_type_options=rhodecode.BACKENDS.keys(),
819 repo_ref_options=ref_choices,
819 repo_ref_options=ref_choices,
820 repo_type=repo_type,
820 repo_type=repo_type,
821 # user caller
821 # user caller
822 user=apiuser)
822 user=apiuser)
823
823
824 try:
824 try:
825 schema_data = schema.deserialize(dict(
825 schema_data = schema.deserialize(dict(
826 repo_name=repo_name,
826 repo_name=repo_name,
827 repo_type=repo_type,
827 repo_type=repo_type,
828 repo_owner=owner.username,
828 repo_owner=owner.username,
829 repo_description=description,
829 repo_description=description,
830 repo_landing_commit_ref=landing_commit_ref,
830 repo_landing_commit_ref=landing_commit_ref,
831 repo_clone_uri=clone_uri,
831 repo_clone_uri=clone_uri,
832 repo_push_uri=push_uri,
832 repo_push_uri=push_uri,
833 repo_private=private,
833 repo_private=private,
834 repo_copy_permissions=copy_permissions,
834 repo_copy_permissions=copy_permissions,
835 repo_enable_statistics=enable_statistics,
835 repo_enable_statistics=enable_statistics,
836 repo_enable_downloads=enable_downloads,
836 repo_enable_downloads=enable_downloads,
837 repo_enable_locking=enable_locking))
837 repo_enable_locking=enable_locking))
838 except validation_schema.Invalid as err:
838 except validation_schema.Invalid as err:
839 raise JSONRPCValidationError(colander_exc=err)
839 raise JSONRPCValidationError(colander_exc=err)
840
840
841 try:
841 try:
842 data = {
842 data = {
843 'owner': owner,
843 'owner': owner,
844 'repo_name': schema_data['repo_group']['repo_name_without_group'],
844 'repo_name': schema_data['repo_group']['repo_name_without_group'],
845 'repo_name_full': schema_data['repo_name'],
845 'repo_name_full': schema_data['repo_name'],
846 'repo_group': schema_data['repo_group']['repo_group_id'],
846 'repo_group': schema_data['repo_group']['repo_group_id'],
847 'repo_type': schema_data['repo_type'],
847 'repo_type': schema_data['repo_type'],
848 'repo_description': schema_data['repo_description'],
848 'repo_description': schema_data['repo_description'],
849 'repo_private': schema_data['repo_private'],
849 'repo_private': schema_data['repo_private'],
850 'clone_uri': schema_data['repo_clone_uri'],
850 'clone_uri': schema_data['repo_clone_uri'],
851 'push_uri': schema_data['repo_push_uri'],
851 'push_uri': schema_data['repo_push_uri'],
852 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
852 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
853 'enable_statistics': schema_data['repo_enable_statistics'],
853 'enable_statistics': schema_data['repo_enable_statistics'],
854 'enable_locking': schema_data['repo_enable_locking'],
854 'enable_locking': schema_data['repo_enable_locking'],
855 'enable_downloads': schema_data['repo_enable_downloads'],
855 'enable_downloads': schema_data['repo_enable_downloads'],
856 'repo_copy_permissions': schema_data['repo_copy_permissions'],
856 'repo_copy_permissions': schema_data['repo_copy_permissions'],
857 }
857 }
858
858
859 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
859 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
860 task_id = get_task_id(task)
860 task_id = get_task_id(task)
861 # no commit, it's done in RepoModel, or async via celery
861 # no commit, it's done in RepoModel, or async via celery
862 return {
862 return {
863 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
863 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
864 'success': True, # cannot return the repo data here since fork
864 'success': True, # cannot return the repo data here since fork
865 # can be done async
865 # can be done async
866 'task': task_id
866 'task': task_id
867 }
867 }
868 except Exception:
868 except Exception:
869 log.exception(
869 log.exception(
870 u"Exception while trying to create the repository %s",
870 u"Exception while trying to create the repository %s",
871 schema_data['repo_name'])
871 schema_data['repo_name'])
872 raise JSONRPCError(
872 raise JSONRPCError(
873 'failed to create repository `%s`' % (schema_data['repo_name'],))
873 'failed to create repository `%s`' % (schema_data['repo_name'],))
874
874
875
875
876 @jsonrpc_method()
876 @jsonrpc_method()
877 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
877 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
878 description=Optional('')):
878 description=Optional('')):
879 """
879 """
880 Adds an extra field to a repository.
880 Adds an extra field to a repository.
881
881
882 This command can only be run using an |authtoken| with at least
882 This command can only be run using an |authtoken| with at least
883 write permissions to the |repo|.
883 write permissions to the |repo|.
884
884
885 :param apiuser: This is filled automatically from the |authtoken|.
885 :param apiuser: This is filled automatically from the |authtoken|.
886 :type apiuser: AuthUser
886 :type apiuser: AuthUser
887 :param repoid: Set the repository name or repository id.
887 :param repoid: Set the repository name or repository id.
888 :type repoid: str or int
888 :type repoid: str or int
889 :param key: Create a unique field key for this repository.
889 :param key: Create a unique field key for this repository.
890 :type key: str
890 :type key: str
891 :param label:
891 :param label:
892 :type label: Optional(str)
892 :type label: Optional(str)
893 :param description:
893 :param description:
894 :type description: Optional(str)
894 :type description: Optional(str)
895 """
895 """
896 repo = get_repo_or_error(repoid)
896 repo = get_repo_or_error(repoid)
897 if not has_superadmin_permission(apiuser):
897 if not has_superadmin_permission(apiuser):
898 _perms = ('repository.admin',)
898 _perms = ('repository.admin',)
899 validate_repo_permissions(apiuser, repoid, repo, _perms)
899 validate_repo_permissions(apiuser, repoid, repo, _perms)
900
900
901 label = Optional.extract(label) or key
901 label = Optional.extract(label) or key
902 description = Optional.extract(description)
902 description = Optional.extract(description)
903
903
904 field = RepositoryField.get_by_key_name(key, repo)
904 field = RepositoryField.get_by_key_name(key, repo)
905 if field:
905 if field:
906 raise JSONRPCError('Field with key '
906 raise JSONRPCError('Field with key '
907 '`%s` exists for repo `%s`' % (key, repoid))
907 '`%s` exists for repo `%s`' % (key, repoid))
908
908
909 try:
909 try:
910 RepoModel().add_repo_field(repo, key, field_label=label,
910 RepoModel().add_repo_field(repo, key, field_label=label,
911 field_desc=description)
911 field_desc=description)
912 Session().commit()
912 Session().commit()
913 return {
913 return {
914 'msg': "Added new repository field `%s`" % (key,),
914 'msg': "Added new repository field `%s`" % (key,),
915 'success': True,
915 'success': True,
916 }
916 }
917 except Exception:
917 except Exception:
918 log.exception("Exception occurred while trying to add field to repo")
918 log.exception("Exception occurred while trying to add field to repo")
919 raise JSONRPCError(
919 raise JSONRPCError(
920 'failed to create new field for repository `%s`' % (repoid,))
920 'failed to create new field for repository `%s`' % (repoid,))
921
921
922
922
923 @jsonrpc_method()
923 @jsonrpc_method()
924 def remove_field_from_repo(request, apiuser, repoid, key):
924 def remove_field_from_repo(request, apiuser, repoid, key):
925 """
925 """
926 Removes an extra field from a repository.
926 Removes an extra field from a repository.
927
927
928 This command can only be run using an |authtoken| with at least
928 This command can only be run using an |authtoken| with at least
929 write permissions to the |repo|.
929 write permissions to the |repo|.
930
930
931 :param apiuser: This is filled automatically from the |authtoken|.
931 :param apiuser: This is filled automatically from the |authtoken|.
932 :type apiuser: AuthUser
932 :type apiuser: AuthUser
933 :param repoid: Set the repository name or repository ID.
933 :param repoid: Set the repository name or repository ID.
934 :type repoid: str or int
934 :type repoid: str or int
935 :param key: Set the unique field key for this repository.
935 :param key: Set the unique field key for this repository.
936 :type key: str
936 :type key: str
937 """
937 """
938
938
939 repo = get_repo_or_error(repoid)
939 repo = get_repo_or_error(repoid)
940 if not has_superadmin_permission(apiuser):
940 if not has_superadmin_permission(apiuser):
941 _perms = ('repository.admin',)
941 _perms = ('repository.admin',)
942 validate_repo_permissions(apiuser, repoid, repo, _perms)
942 validate_repo_permissions(apiuser, repoid, repo, _perms)
943
943
944 field = RepositoryField.get_by_key_name(key, repo)
944 field = RepositoryField.get_by_key_name(key, repo)
945 if not field:
945 if not field:
946 raise JSONRPCError('Field with key `%s` does not '
946 raise JSONRPCError('Field with key `%s` does not '
947 'exists for repo `%s`' % (key, repoid))
947 'exists for repo `%s`' % (key, repoid))
948
948
949 try:
949 try:
950 RepoModel().delete_repo_field(repo, field_key=key)
950 RepoModel().delete_repo_field(repo, field_key=key)
951 Session().commit()
951 Session().commit()
952 return {
952 return {
953 'msg': "Deleted repository field `%s`" % (key,),
953 'msg': "Deleted repository field `%s`" % (key,),
954 'success': True,
954 'success': True,
955 }
955 }
956 except Exception:
956 except Exception:
957 log.exception(
957 log.exception(
958 "Exception occurred while trying to delete field from repo")
958 "Exception occurred while trying to delete field from repo")
959 raise JSONRPCError(
959 raise JSONRPCError(
960 'failed to delete field for repository `%s`' % (repoid,))
960 'failed to delete field for repository `%s`' % (repoid,))
961
961
962
962
963 @jsonrpc_method()
963 @jsonrpc_method()
964 def update_repo(
964 def update_repo(
965 request, apiuser, repoid, repo_name=Optional(None),
965 request, apiuser, repoid, repo_name=Optional(None),
966 owner=Optional(OAttr('apiuser')), description=Optional(''),
966 owner=Optional(OAttr('apiuser')), description=Optional(''),
967 private=Optional(False),
967 private=Optional(False),
968 clone_uri=Optional(None), push_uri=Optional(None),
968 clone_uri=Optional(None), push_uri=Optional(None),
969 landing_rev=Optional(None), fork_of=Optional(None),
969 landing_rev=Optional(None), fork_of=Optional(None),
970 enable_statistics=Optional(False),
970 enable_statistics=Optional(False),
971 enable_locking=Optional(False),
971 enable_locking=Optional(False),
972 enable_downloads=Optional(False), fields=Optional('')):
972 enable_downloads=Optional(False), fields=Optional('')):
973 """
973 """
974 Updates a repository with the given information.
974 Updates a repository with the given information.
975
975
976 This command can only be run using an |authtoken| with at least
976 This command can only be run using an |authtoken| with at least
977 admin permissions to the |repo|.
977 admin permissions to the |repo|.
978
978
979 * If the repository name contains "/", repository will be updated
979 * If the repository name contains "/", repository will be updated
980 accordingly with a repository group or nested repository groups
980 accordingly with a repository group or nested repository groups
981
981
982 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
982 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
983 called "repo-test" and place it inside group "foo/bar".
983 called "repo-test" and place it inside group "foo/bar".
984 You have to have permissions to access and write to the last repository
984 You have to have permissions to access and write to the last repository
985 group ("bar" in this example)
985 group ("bar" in this example)
986
986
987 :param apiuser: This is filled automatically from the |authtoken|.
987 :param apiuser: This is filled automatically from the |authtoken|.
988 :type apiuser: AuthUser
988 :type apiuser: AuthUser
989 :param repoid: repository name or repository ID.
989 :param repoid: repository name or repository ID.
990 :type repoid: str or int
990 :type repoid: str or int
991 :param repo_name: Update the |repo| name, including the
991 :param repo_name: Update the |repo| name, including the
992 repository group it's in.
992 repository group it's in.
993 :type repo_name: str
993 :type repo_name: str
994 :param owner: Set the |repo| owner.
994 :param owner: Set the |repo| owner.
995 :type owner: str
995 :type owner: str
996 :param fork_of: Set the |repo| as fork of another |repo|.
996 :param fork_of: Set the |repo| as fork of another |repo|.
997 :type fork_of: str
997 :type fork_of: str
998 :param description: Update the |repo| description.
998 :param description: Update the |repo| description.
999 :type description: str
999 :type description: str
1000 :param private: Set the |repo| as private. (True | False)
1000 :param private: Set the |repo| as private. (True | False)
1001 :type private: bool
1001 :type private: bool
1002 :param clone_uri: Update the |repo| clone URI.
1002 :param clone_uri: Update the |repo| clone URI.
1003 :type clone_uri: str
1003 :type clone_uri: str
1004 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1004 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1005 :type landing_rev: str
1005 :type landing_rev: str
1006 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1006 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1007 :type enable_statistics: bool
1007 :type enable_statistics: bool
1008 :param enable_locking: Enable |repo| locking.
1008 :param enable_locking: Enable |repo| locking.
1009 :type enable_locking: bool
1009 :type enable_locking: bool
1010 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1010 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1011 :type enable_downloads: bool
1011 :type enable_downloads: bool
1012 :param fields: Add extra fields to the |repo|. Use the following
1012 :param fields: Add extra fields to the |repo|. Use the following
1013 example format: ``field_key=field_val,field_key2=fieldval2``.
1013 example format: ``field_key=field_val,field_key2=fieldval2``.
1014 Escape ', ' with \,
1014 Escape ', ' with \,
1015 :type fields: str
1015 :type fields: str
1016 """
1016 """
1017
1017
1018 repo = get_repo_or_error(repoid)
1018 repo = get_repo_or_error(repoid)
1019
1019
1020 include_secrets = False
1020 include_secrets = False
1021 if not has_superadmin_permission(apiuser):
1021 if not has_superadmin_permission(apiuser):
1022 _perms = ('repository.admin',)
1022 _perms = ('repository.admin',)
1023 validate_repo_permissions(apiuser, repoid, repo, _perms)
1023 validate_repo_permissions(apiuser, repoid, repo, _perms)
1024 else:
1024 else:
1025 include_secrets = True
1025 include_secrets = True
1026
1026
1027 updates = dict(
1027 updates = dict(
1028 repo_name=repo_name
1028 repo_name=repo_name
1029 if not isinstance(repo_name, Optional) else repo.repo_name,
1029 if not isinstance(repo_name, Optional) else repo.repo_name,
1030
1030
1031 fork_id=fork_of
1031 fork_id=fork_of
1032 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1032 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1033
1033
1034 user=owner
1034 user=owner
1035 if not isinstance(owner, Optional) else repo.user.username,
1035 if not isinstance(owner, Optional) else repo.user.username,
1036
1036
1037 repo_description=description
1037 repo_description=description
1038 if not isinstance(description, Optional) else repo.description,
1038 if not isinstance(description, Optional) else repo.description,
1039
1039
1040 repo_private=private
1040 repo_private=private
1041 if not isinstance(private, Optional) else repo.private,
1041 if not isinstance(private, Optional) else repo.private,
1042
1042
1043 clone_uri=clone_uri
1043 clone_uri=clone_uri
1044 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1044 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1045
1045
1046 push_uri=push_uri
1046 push_uri=push_uri
1047 if not isinstance(push_uri, Optional) else repo.push_uri,
1047 if not isinstance(push_uri, Optional) else repo.push_uri,
1048
1048
1049 repo_landing_rev=landing_rev
1049 repo_landing_rev=landing_rev
1050 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1050 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1051
1051
1052 repo_enable_statistics=enable_statistics
1052 repo_enable_statistics=enable_statistics
1053 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1053 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1054
1054
1055 repo_enable_locking=enable_locking
1055 repo_enable_locking=enable_locking
1056 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1056 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1057
1057
1058 repo_enable_downloads=enable_downloads
1058 repo_enable_downloads=enable_downloads
1059 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1059 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1060
1060
1061 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1061 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1062 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1062 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1063 request.translate, repo=repo)
1063 request.translate, repo=repo)
1064 ref_choices = list(set(ref_choices + [landing_ref]))
1064 ref_choices = list(set(ref_choices + [landing_ref]))
1065
1065
1066 old_values = repo.get_api_data()
1066 old_values = repo.get_api_data()
1067 repo_type = repo.repo_type
1067 repo_type = repo.repo_type
1068 schema = repo_schema.RepoSchema().bind(
1068 schema = repo_schema.RepoSchema().bind(
1069 repo_type_options=rhodecode.BACKENDS.keys(),
1069 repo_type_options=rhodecode.BACKENDS.keys(),
1070 repo_ref_options=ref_choices,
1070 repo_ref_options=ref_choices,
1071 repo_type=repo_type,
1071 repo_type=repo_type,
1072 # user caller
1072 # user caller
1073 user=apiuser,
1073 user=apiuser,
1074 old_values=old_values)
1074 old_values=old_values)
1075 try:
1075 try:
1076 schema_data = schema.deserialize(dict(
1076 schema_data = schema.deserialize(dict(
1077 # we save old value, users cannot change type
1077 # we save old value, users cannot change type
1078 repo_type=repo_type,
1078 repo_type=repo_type,
1079
1079
1080 repo_name=updates['repo_name'],
1080 repo_name=updates['repo_name'],
1081 repo_owner=updates['user'],
1081 repo_owner=updates['user'],
1082 repo_description=updates['repo_description'],
1082 repo_description=updates['repo_description'],
1083 repo_clone_uri=updates['clone_uri'],
1083 repo_clone_uri=updates['clone_uri'],
1084 repo_push_uri=updates['push_uri'],
1084 repo_push_uri=updates['push_uri'],
1085 repo_fork_of=updates['fork_id'],
1085 repo_fork_of=updates['fork_id'],
1086 repo_private=updates['repo_private'],
1086 repo_private=updates['repo_private'],
1087 repo_landing_commit_ref=updates['repo_landing_rev'],
1087 repo_landing_commit_ref=updates['repo_landing_rev'],
1088 repo_enable_statistics=updates['repo_enable_statistics'],
1088 repo_enable_statistics=updates['repo_enable_statistics'],
1089 repo_enable_downloads=updates['repo_enable_downloads'],
1089 repo_enable_downloads=updates['repo_enable_downloads'],
1090 repo_enable_locking=updates['repo_enable_locking']))
1090 repo_enable_locking=updates['repo_enable_locking']))
1091 except validation_schema.Invalid as err:
1091 except validation_schema.Invalid as err:
1092 raise JSONRPCValidationError(colander_exc=err)
1092 raise JSONRPCValidationError(colander_exc=err)
1093
1093
1094 # save validated data back into the updates dict
1094 # save validated data back into the updates dict
1095 validated_updates = dict(
1095 validated_updates = dict(
1096 repo_name=schema_data['repo_group']['repo_name_without_group'],
1096 repo_name=schema_data['repo_group']['repo_name_without_group'],
1097 repo_group=schema_data['repo_group']['repo_group_id'],
1097 repo_group=schema_data['repo_group']['repo_group_id'],
1098
1098
1099 user=schema_data['repo_owner'],
1099 user=schema_data['repo_owner'],
1100 repo_description=schema_data['repo_description'],
1100 repo_description=schema_data['repo_description'],
1101 repo_private=schema_data['repo_private'],
1101 repo_private=schema_data['repo_private'],
1102 clone_uri=schema_data['repo_clone_uri'],
1102 clone_uri=schema_data['repo_clone_uri'],
1103 push_uri=schema_data['repo_push_uri'],
1103 push_uri=schema_data['repo_push_uri'],
1104 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1104 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1105 repo_enable_statistics=schema_data['repo_enable_statistics'],
1105 repo_enable_statistics=schema_data['repo_enable_statistics'],
1106 repo_enable_locking=schema_data['repo_enable_locking'],
1106 repo_enable_locking=schema_data['repo_enable_locking'],
1107 repo_enable_downloads=schema_data['repo_enable_downloads'],
1107 repo_enable_downloads=schema_data['repo_enable_downloads'],
1108 )
1108 )
1109
1109
1110 if schema_data['repo_fork_of']:
1110 if schema_data['repo_fork_of']:
1111 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1111 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1112 validated_updates['fork_id'] = fork_repo.repo_id
1112 validated_updates['fork_id'] = fork_repo.repo_id
1113
1113
1114 # extra fields
1114 # extra fields
1115 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1115 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1116 if fields:
1116 if fields:
1117 validated_updates.update(fields)
1117 validated_updates.update(fields)
1118
1118
1119 try:
1119 try:
1120 RepoModel().update(repo, **validated_updates)
1120 RepoModel().update(repo, **validated_updates)
1121 audit_logger.store_api(
1121 audit_logger.store_api(
1122 'repo.edit', action_data={'old_data': old_values},
1122 'repo.edit', action_data={'old_data': old_values},
1123 user=apiuser, repo=repo)
1123 user=apiuser, repo=repo)
1124 Session().commit()
1124 Session().commit()
1125 return {
1125 return {
1126 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1126 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1127 'repository': repo.get_api_data(include_secrets=include_secrets)
1127 'repository': repo.get_api_data(include_secrets=include_secrets)
1128 }
1128 }
1129 except Exception:
1129 except Exception:
1130 log.exception(
1130 log.exception(
1131 u"Exception while trying to update the repository %s",
1131 u"Exception while trying to update the repository %s",
1132 repoid)
1132 repoid)
1133 raise JSONRPCError('failed to update repo `%s`' % repoid)
1133 raise JSONRPCError('failed to update repo `%s`' % repoid)
1134
1134
1135
1135
1136 @jsonrpc_method()
1136 @jsonrpc_method()
1137 def fork_repo(request, apiuser, repoid, fork_name,
1137 def fork_repo(request, apiuser, repoid, fork_name,
1138 owner=Optional(OAttr('apiuser')),
1138 owner=Optional(OAttr('apiuser')),
1139 description=Optional(''),
1139 description=Optional(''),
1140 private=Optional(False),
1140 private=Optional(False),
1141 clone_uri=Optional(None),
1141 clone_uri=Optional(None),
1142 landing_rev=Optional(None),
1142 landing_rev=Optional(None),
1143 copy_permissions=Optional(False)):
1143 copy_permissions=Optional(False)):
1144 """
1144 """
1145 Creates a fork of the specified |repo|.
1145 Creates a fork of the specified |repo|.
1146
1146
1147 * If the fork_name contains "/", fork will be created inside
1147 * If the fork_name contains "/", fork will be created inside
1148 a repository group or nested repository groups
1148 a repository group or nested repository groups
1149
1149
1150 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1150 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1151 inside group "foo/bar". You have to have permissions to access and
1151 inside group "foo/bar". You have to have permissions to access and
1152 write to the last repository group ("bar" in this example)
1152 write to the last repository group ("bar" in this example)
1153
1153
1154 This command can only be run using an |authtoken| with minimum
1154 This command can only be run using an |authtoken| with minimum
1155 read permissions of the forked repo, create fork permissions for an user.
1155 read permissions of the forked repo, create fork permissions for an user.
1156
1156
1157 :param apiuser: This is filled automatically from the |authtoken|.
1157 :param apiuser: This is filled automatically from the |authtoken|.
1158 :type apiuser: AuthUser
1158 :type apiuser: AuthUser
1159 :param repoid: Set repository name or repository ID.
1159 :param repoid: Set repository name or repository ID.
1160 :type repoid: str or int
1160 :type repoid: str or int
1161 :param fork_name: Set the fork name, including it's repository group membership.
1161 :param fork_name: Set the fork name, including it's repository group membership.
1162 :type fork_name: str
1162 :type fork_name: str
1163 :param owner: Set the fork owner.
1163 :param owner: Set the fork owner.
1164 :type owner: str
1164 :type owner: str
1165 :param description: Set the fork description.
1165 :param description: Set the fork description.
1166 :type description: str
1166 :type description: str
1167 :param copy_permissions: Copy permissions from parent |repo|. The
1167 :param copy_permissions: Copy permissions from parent |repo|. The
1168 default is False.
1168 default is False.
1169 :type copy_permissions: bool
1169 :type copy_permissions: bool
1170 :param private: Make the fork private. The default is False.
1170 :param private: Make the fork private. The default is False.
1171 :type private: bool
1171 :type private: bool
1172 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1172 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1173
1173
1174 Example output:
1174 Example output:
1175
1175
1176 .. code-block:: bash
1176 .. code-block:: bash
1177
1177
1178 id : <id_for_response>
1178 id : <id_for_response>
1179 api_key : "<api_key>"
1179 api_key : "<api_key>"
1180 args: {
1180 args: {
1181 "repoid" : "<reponame or repo_id>",
1181 "repoid" : "<reponame or repo_id>",
1182 "fork_name": "<forkname>",
1182 "fork_name": "<forkname>",
1183 "owner": "<username or user_id = Optional(=apiuser)>",
1183 "owner": "<username or user_id = Optional(=apiuser)>",
1184 "description": "<description>",
1184 "description": "<description>",
1185 "copy_permissions": "<bool>",
1185 "copy_permissions": "<bool>",
1186 "private": "<bool>",
1186 "private": "<bool>",
1187 "landing_rev": "<landing_rev>"
1187 "landing_rev": "<landing_rev>"
1188 }
1188 }
1189
1189
1190 Example error output:
1190 Example error output:
1191
1191
1192 .. code-block:: bash
1192 .. code-block:: bash
1193
1193
1194 id : <id_given_in_input>
1194 id : <id_given_in_input>
1195 result: {
1195 result: {
1196 "msg": "Created fork of `<reponame>` as `<forkname>`",
1196 "msg": "Created fork of `<reponame>` as `<forkname>`",
1197 "success": true,
1197 "success": true,
1198 "task": "<celery task id or None if done sync>"
1198 "task": "<celery task id or None if done sync>"
1199 }
1199 }
1200 error: null
1200 error: null
1201
1201
1202 """
1202 """
1203
1203
1204 repo = get_repo_or_error(repoid)
1204 repo = get_repo_or_error(repoid)
1205 repo_name = repo.repo_name
1205 repo_name = repo.repo_name
1206
1206
1207 if not has_superadmin_permission(apiuser):
1207 if not has_superadmin_permission(apiuser):
1208 # check if we have at least read permission for
1208 # check if we have at least read permission for
1209 # this repo that we fork !
1209 # this repo that we fork !
1210 _perms = ('repository.admin', 'repository.write', 'repository.read')
1210 _perms = ('repository.admin', 'repository.write', 'repository.read')
1211 validate_repo_permissions(apiuser, repoid, repo, _perms)
1211 validate_repo_permissions(apiuser, repoid, repo, _perms)
1212
1212
1213 # check if the regular user has at least fork permissions as well
1213 # check if the regular user has at least fork permissions as well
1214 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1214 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1215 raise JSONRPCForbidden()
1215 raise JSONRPCForbidden()
1216
1216
1217 # check if user can set owner parameter
1217 # check if user can set owner parameter
1218 owner = validate_set_owner_permissions(apiuser, owner)
1218 owner = validate_set_owner_permissions(apiuser, owner)
1219
1219
1220 description = Optional.extract(description)
1220 description = Optional.extract(description)
1221 copy_permissions = Optional.extract(copy_permissions)
1221 copy_permissions = Optional.extract(copy_permissions)
1222 clone_uri = Optional.extract(clone_uri)
1222 clone_uri = Optional.extract(clone_uri)
1223
1223
1224 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1224 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1225 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1225 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1226 ref_choices = list(set(ref_choices + [landing_ref]))
1226 ref_choices = list(set(ref_choices + [landing_ref]))
1227 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1227 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1228
1228
1229 private = Optional.extract(private)
1229 private = Optional.extract(private)
1230
1230
1231 schema = repo_schema.RepoSchema().bind(
1231 schema = repo_schema.RepoSchema().bind(
1232 repo_type_options=rhodecode.BACKENDS.keys(),
1232 repo_type_options=rhodecode.BACKENDS.keys(),
1233 repo_ref_options=ref_choices,
1233 repo_ref_options=ref_choices,
1234 repo_type=repo.repo_type,
1234 repo_type=repo.repo_type,
1235 # user caller
1235 # user caller
1236 user=apiuser)
1236 user=apiuser)
1237
1237
1238 try:
1238 try:
1239 schema_data = schema.deserialize(dict(
1239 schema_data = schema.deserialize(dict(
1240 repo_name=fork_name,
1240 repo_name=fork_name,
1241 repo_type=repo.repo_type,
1241 repo_type=repo.repo_type,
1242 repo_owner=owner.username,
1242 repo_owner=owner.username,
1243 repo_description=description,
1243 repo_description=description,
1244 repo_landing_commit_ref=landing_commit_ref,
1244 repo_landing_commit_ref=landing_commit_ref,
1245 repo_clone_uri=clone_uri,
1245 repo_clone_uri=clone_uri,
1246 repo_private=private,
1246 repo_private=private,
1247 repo_copy_permissions=copy_permissions))
1247 repo_copy_permissions=copy_permissions))
1248 except validation_schema.Invalid as err:
1248 except validation_schema.Invalid as err:
1249 raise JSONRPCValidationError(colander_exc=err)
1249 raise JSONRPCValidationError(colander_exc=err)
1250
1250
1251 try:
1251 try:
1252 data = {
1252 data = {
1253 'fork_parent_id': repo.repo_id,
1253 'fork_parent_id': repo.repo_id,
1254
1254
1255 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1255 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1256 'repo_name_full': schema_data['repo_name'],
1256 'repo_name_full': schema_data['repo_name'],
1257 'repo_group': schema_data['repo_group']['repo_group_id'],
1257 'repo_group': schema_data['repo_group']['repo_group_id'],
1258 'repo_type': schema_data['repo_type'],
1258 'repo_type': schema_data['repo_type'],
1259 'description': schema_data['repo_description'],
1259 'description': schema_data['repo_description'],
1260 'private': schema_data['repo_private'],
1260 'private': schema_data['repo_private'],
1261 'copy_permissions': schema_data['repo_copy_permissions'],
1261 'copy_permissions': schema_data['repo_copy_permissions'],
1262 'landing_rev': schema_data['repo_landing_commit_ref'],
1262 'landing_rev': schema_data['repo_landing_commit_ref'],
1263 }
1263 }
1264
1264
1265 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1265 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1266 # no commit, it's done in RepoModel, or async via celery
1266 # no commit, it's done in RepoModel, or async via celery
1267 task_id = get_task_id(task)
1267 task_id = get_task_id(task)
1268
1268
1269 return {
1269 return {
1270 'msg': 'Created fork of `%s` as `%s`' % (
1270 'msg': 'Created fork of `%s` as `%s`' % (
1271 repo.repo_name, schema_data['repo_name']),
1271 repo.repo_name, schema_data['repo_name']),
1272 'success': True, # cannot return the repo data here since fork
1272 'success': True, # cannot return the repo data here since fork
1273 # can be done async
1273 # can be done async
1274 'task': task_id
1274 'task': task_id
1275 }
1275 }
1276 except Exception:
1276 except Exception:
1277 log.exception(
1277 log.exception(
1278 u"Exception while trying to create fork %s",
1278 u"Exception while trying to create fork %s",
1279 schema_data['repo_name'])
1279 schema_data['repo_name'])
1280 raise JSONRPCError(
1280 raise JSONRPCError(
1281 'failed to fork repository `%s` as `%s`' % (
1281 'failed to fork repository `%s` as `%s`' % (
1282 repo_name, schema_data['repo_name']))
1282 repo_name, schema_data['repo_name']))
1283
1283
1284
1284
1285 @jsonrpc_method()
1285 @jsonrpc_method()
1286 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1286 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1287 """
1287 """
1288 Deletes a repository.
1288 Deletes a repository.
1289
1289
1290 * When the `forks` parameter is set it's possible to detach or delete
1290 * When the `forks` parameter is set it's possible to detach or delete
1291 forks of deleted repository.
1291 forks of deleted repository.
1292
1292
1293 This command can only be run using an |authtoken| with admin
1293 This command can only be run using an |authtoken| with admin
1294 permissions on the |repo|.
1294 permissions on the |repo|.
1295
1295
1296 :param apiuser: This is filled automatically from the |authtoken|.
1296 :param apiuser: This is filled automatically from the |authtoken|.
1297 :type apiuser: AuthUser
1297 :type apiuser: AuthUser
1298 :param repoid: Set the repository name or repository ID.
1298 :param repoid: Set the repository name or repository ID.
1299 :type repoid: str or int
1299 :type repoid: str or int
1300 :param forks: Set to `detach` or `delete` forks from the |repo|.
1300 :param forks: Set to `detach` or `delete` forks from the |repo|.
1301 :type forks: Optional(str)
1301 :type forks: Optional(str)
1302
1302
1303 Example error output:
1303 Example error output:
1304
1304
1305 .. code-block:: bash
1305 .. code-block:: bash
1306
1306
1307 id : <id_given_in_input>
1307 id : <id_given_in_input>
1308 result: {
1308 result: {
1309 "msg": "Deleted repository `<reponame>`",
1309 "msg": "Deleted repository `<reponame>`",
1310 "success": true
1310 "success": true
1311 }
1311 }
1312 error: null
1312 error: null
1313 """
1313 """
1314
1314
1315 repo = get_repo_or_error(repoid)
1315 repo = get_repo_or_error(repoid)
1316 repo_name = repo.repo_name
1316 repo_name = repo.repo_name
1317 if not has_superadmin_permission(apiuser):
1317 if not has_superadmin_permission(apiuser):
1318 _perms = ('repository.admin',)
1318 _perms = ('repository.admin',)
1319 validate_repo_permissions(apiuser, repoid, repo, _perms)
1319 validate_repo_permissions(apiuser, repoid, repo, _perms)
1320
1320
1321 try:
1321 try:
1322 handle_forks = Optional.extract(forks)
1322 handle_forks = Optional.extract(forks)
1323 _forks_msg = ''
1323 _forks_msg = ''
1324 _forks = [f for f in repo.forks]
1324 _forks = [f for f in repo.forks]
1325 if handle_forks == 'detach':
1325 if handle_forks == 'detach':
1326 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1326 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1327 elif handle_forks == 'delete':
1327 elif handle_forks == 'delete':
1328 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1328 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1329 elif _forks:
1329 elif _forks:
1330 raise JSONRPCError(
1330 raise JSONRPCError(
1331 'Cannot delete `%s` it still contains attached forks' %
1331 'Cannot delete `%s` it still contains attached forks' %
1332 (repo.repo_name,)
1332 (repo.repo_name,)
1333 )
1333 )
1334 old_data = repo.get_api_data()
1334 old_data = repo.get_api_data()
1335 RepoModel().delete(repo, forks=forks)
1335 RepoModel().delete(repo, forks=forks)
1336
1336
1337 repo = audit_logger.RepoWrap(repo_id=None,
1337 repo = audit_logger.RepoWrap(repo_id=None,
1338 repo_name=repo.repo_name)
1338 repo_name=repo.repo_name)
1339
1339
1340 audit_logger.store_api(
1340 audit_logger.store_api(
1341 'repo.delete', action_data={'old_data': old_data},
1341 'repo.delete', action_data={'old_data': old_data},
1342 user=apiuser, repo=repo)
1342 user=apiuser, repo=repo)
1343
1343
1344 ScmModel().mark_for_invalidation(repo_name, delete=True)
1344 ScmModel().mark_for_invalidation(repo_name, delete=True)
1345 Session().commit()
1345 Session().commit()
1346 return {
1346 return {
1347 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1347 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1348 'success': True
1348 'success': True
1349 }
1349 }
1350 except Exception:
1350 except Exception:
1351 log.exception("Exception occurred while trying to delete repo")
1351 log.exception("Exception occurred while trying to delete repo")
1352 raise JSONRPCError(
1352 raise JSONRPCError(
1353 'failed to delete repository `%s`' % (repo_name,)
1353 'failed to delete repository `%s`' % (repo_name,)
1354 )
1354 )
1355
1355
1356
1356
1357 #TODO: marcink, change name ?
1357 #TODO: marcink, change name ?
1358 @jsonrpc_method()
1358 @jsonrpc_method()
1359 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1359 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1360 """
1360 """
1361 Invalidates the cache for the specified repository.
1361 Invalidates the cache for the specified repository.
1362
1362
1363 This command can only be run using an |authtoken| with admin rights to
1363 This command can only be run using an |authtoken| with admin rights to
1364 the specified repository.
1364 the specified repository.
1365
1365
1366 This command takes the following options:
1366 This command takes the following options:
1367
1367
1368 :param apiuser: This is filled automatically from |authtoken|.
1368 :param apiuser: This is filled automatically from |authtoken|.
1369 :type apiuser: AuthUser
1369 :type apiuser: AuthUser
1370 :param repoid: Sets the repository name or repository ID.
1370 :param repoid: Sets the repository name or repository ID.
1371 :type repoid: str or int
1371 :type repoid: str or int
1372 :param delete_keys: This deletes the invalidated keys instead of
1372 :param delete_keys: This deletes the invalidated keys instead of
1373 just flagging them.
1373 just flagging them.
1374 :type delete_keys: Optional(``True`` | ``False``)
1374 :type delete_keys: Optional(``True`` | ``False``)
1375
1375
1376 Example output:
1376 Example output:
1377
1377
1378 .. code-block:: bash
1378 .. code-block:: bash
1379
1379
1380 id : <id_given_in_input>
1380 id : <id_given_in_input>
1381 result : {
1381 result : {
1382 'msg': Cache for repository `<repository name>` was invalidated,
1382 'msg': Cache for repository `<repository name>` was invalidated,
1383 'repository': <repository name>
1383 'repository': <repository name>
1384 }
1384 }
1385 error : null
1385 error : null
1386
1386
1387 Example error output:
1387 Example error output:
1388
1388
1389 .. code-block:: bash
1389 .. code-block:: bash
1390
1390
1391 id : <id_given_in_input>
1391 id : <id_given_in_input>
1392 result : null
1392 result : null
1393 error : {
1393 error : {
1394 'Error occurred during cache invalidation action'
1394 'Error occurred during cache invalidation action'
1395 }
1395 }
1396
1396
1397 """
1397 """
1398
1398
1399 repo = get_repo_or_error(repoid)
1399 repo = get_repo_or_error(repoid)
1400 if not has_superadmin_permission(apiuser):
1400 if not has_superadmin_permission(apiuser):
1401 _perms = ('repository.admin', 'repository.write',)
1401 _perms = ('repository.admin', 'repository.write',)
1402 validate_repo_permissions(apiuser, repoid, repo, _perms)
1402 validate_repo_permissions(apiuser, repoid, repo, _perms)
1403
1403
1404 delete = Optional.extract(delete_keys)
1404 delete = Optional.extract(delete_keys)
1405 try:
1405 try:
1406 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1406 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1407 return {
1407 return {
1408 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1408 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1409 'repository': repo.repo_name
1409 'repository': repo.repo_name
1410 }
1410 }
1411 except Exception:
1411 except Exception:
1412 log.exception(
1412 log.exception(
1413 "Exception occurred while trying to invalidate repo cache")
1413 "Exception occurred while trying to invalidate repo cache")
1414 raise JSONRPCError(
1414 raise JSONRPCError(
1415 'Error occurred during cache invalidation action'
1415 'Error occurred during cache invalidation action'
1416 )
1416 )
1417
1417
1418
1418
1419 #TODO: marcink, change name ?
1419 #TODO: marcink, change name ?
1420 @jsonrpc_method()
1420 @jsonrpc_method()
1421 def lock(request, apiuser, repoid, locked=Optional(None),
1421 def lock(request, apiuser, repoid, locked=Optional(None),
1422 userid=Optional(OAttr('apiuser'))):
1422 userid=Optional(OAttr('apiuser'))):
1423 """
1423 """
1424 Sets the lock state of the specified |repo| by the given user.
1424 Sets the lock state of the specified |repo| by the given user.
1425 From more information, see :ref:`repo-locking`.
1425 From more information, see :ref:`repo-locking`.
1426
1426
1427 * If the ``userid`` option is not set, the repository is locked to the
1427 * If the ``userid`` option is not set, the repository is locked to the
1428 user who called the method.
1428 user who called the method.
1429 * If the ``locked`` parameter is not set, the current lock state of the
1429 * If the ``locked`` parameter is not set, the current lock state of the
1430 repository is displayed.
1430 repository is displayed.
1431
1431
1432 This command can only be run using an |authtoken| with admin rights to
1432 This command can only be run using an |authtoken| with admin rights to
1433 the specified repository.
1433 the specified repository.
1434
1434
1435 This command takes the following options:
1435 This command takes the following options:
1436
1436
1437 :param apiuser: This is filled automatically from the |authtoken|.
1437 :param apiuser: This is filled automatically from the |authtoken|.
1438 :type apiuser: AuthUser
1438 :type apiuser: AuthUser
1439 :param repoid: Sets the repository name or repository ID.
1439 :param repoid: Sets the repository name or repository ID.
1440 :type repoid: str or int
1440 :type repoid: str or int
1441 :param locked: Sets the lock state.
1441 :param locked: Sets the lock state.
1442 :type locked: Optional(``True`` | ``False``)
1442 :type locked: Optional(``True`` | ``False``)
1443 :param userid: Set the repository lock to this user.
1443 :param userid: Set the repository lock to this user.
1444 :type userid: Optional(str or int)
1444 :type userid: Optional(str or int)
1445
1445
1446 Example error output:
1446 Example error output:
1447
1447
1448 .. code-block:: bash
1448 .. code-block:: bash
1449
1449
1450 id : <id_given_in_input>
1450 id : <id_given_in_input>
1451 result : {
1451 result : {
1452 'repo': '<reponame>',
1452 'repo': '<reponame>',
1453 'locked': <bool: lock state>,
1453 'locked': <bool: lock state>,
1454 'locked_since': <int: lock timestamp>,
1454 'locked_since': <int: lock timestamp>,
1455 'locked_by': <username of person who made the lock>,
1455 'locked_by': <username of person who made the lock>,
1456 'lock_reason': <str: reason for locking>,
1456 'lock_reason': <str: reason for locking>,
1457 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1457 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1458 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1458 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1459 or
1459 or
1460 'msg': 'Repo `<repository name>` not locked.'
1460 'msg': 'Repo `<repository name>` not locked.'
1461 or
1461 or
1462 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1462 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1463 }
1463 }
1464 error : null
1464 error : null
1465
1465
1466 Example error output:
1466 Example error output:
1467
1467
1468 .. code-block:: bash
1468 .. code-block:: bash
1469
1469
1470 id : <id_given_in_input>
1470 id : <id_given_in_input>
1471 result : null
1471 result : null
1472 error : {
1472 error : {
1473 'Error occurred locking repository `<reponame>`'
1473 'Error occurred locking repository `<reponame>`'
1474 }
1474 }
1475 """
1475 """
1476
1476
1477 repo = get_repo_or_error(repoid)
1477 repo = get_repo_or_error(repoid)
1478 if not has_superadmin_permission(apiuser):
1478 if not has_superadmin_permission(apiuser):
1479 # check if we have at least write permission for this repo !
1479 # check if we have at least write permission for this repo !
1480 _perms = ('repository.admin', 'repository.write',)
1480 _perms = ('repository.admin', 'repository.write',)
1481 validate_repo_permissions(apiuser, repoid, repo, _perms)
1481 validate_repo_permissions(apiuser, repoid, repo, _perms)
1482
1482
1483 # make sure normal user does not pass someone else userid,
1483 # make sure normal user does not pass someone else userid,
1484 # he is not allowed to do that
1484 # he is not allowed to do that
1485 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1485 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1486 raise JSONRPCError('userid is not the same as your user')
1486 raise JSONRPCError('userid is not the same as your user')
1487
1487
1488 if isinstance(userid, Optional):
1488 if isinstance(userid, Optional):
1489 userid = apiuser.user_id
1489 userid = apiuser.user_id
1490
1490
1491 user = get_user_or_error(userid)
1491 user = get_user_or_error(userid)
1492
1492
1493 if isinstance(locked, Optional):
1493 if isinstance(locked, Optional):
1494 lockobj = repo.locked
1494 lockobj = repo.locked
1495
1495
1496 if lockobj[0] is None:
1496 if lockobj[0] is None:
1497 _d = {
1497 _d = {
1498 'repo': repo.repo_name,
1498 'repo': repo.repo_name,
1499 'locked': False,
1499 'locked': False,
1500 'locked_since': None,
1500 'locked_since': None,
1501 'locked_by': None,
1501 'locked_by': None,
1502 'lock_reason': None,
1502 'lock_reason': None,
1503 'lock_state_changed': False,
1503 'lock_state_changed': False,
1504 'msg': 'Repo `%s` not locked.' % repo.repo_name
1504 'msg': 'Repo `%s` not locked.' % repo.repo_name
1505 }
1505 }
1506 return _d
1506 return _d
1507 else:
1507 else:
1508 _user_id, _time, _reason = lockobj
1508 _user_id, _time, _reason = lockobj
1509 lock_user = get_user_or_error(userid)
1509 lock_user = get_user_or_error(userid)
1510 _d = {
1510 _d = {
1511 'repo': repo.repo_name,
1511 'repo': repo.repo_name,
1512 'locked': True,
1512 'locked': True,
1513 'locked_since': _time,
1513 'locked_since': _time,
1514 'locked_by': lock_user.username,
1514 'locked_by': lock_user.username,
1515 'lock_reason': _reason,
1515 'lock_reason': _reason,
1516 'lock_state_changed': False,
1516 'lock_state_changed': False,
1517 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1517 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1518 % (repo.repo_name, lock_user.username,
1518 % (repo.repo_name, lock_user.username,
1519 json.dumps(time_to_datetime(_time))))
1519 json.dumps(time_to_datetime(_time))))
1520 }
1520 }
1521 return _d
1521 return _d
1522
1522
1523 # force locked state through a flag
1523 # force locked state through a flag
1524 else:
1524 else:
1525 locked = str2bool(locked)
1525 locked = str2bool(locked)
1526 lock_reason = Repository.LOCK_API
1526 lock_reason = Repository.LOCK_API
1527 try:
1527 try:
1528 if locked:
1528 if locked:
1529 lock_time = time.time()
1529 lock_time = time.time()
1530 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1530 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1531 else:
1531 else:
1532 lock_time = None
1532 lock_time = None
1533 Repository.unlock(repo)
1533 Repository.unlock(repo)
1534 _d = {
1534 _d = {
1535 'repo': repo.repo_name,
1535 'repo': repo.repo_name,
1536 'locked': locked,
1536 'locked': locked,
1537 'locked_since': lock_time,
1537 'locked_since': lock_time,
1538 'locked_by': user.username,
1538 'locked_by': user.username,
1539 'lock_reason': lock_reason,
1539 'lock_reason': lock_reason,
1540 'lock_state_changed': True,
1540 'lock_state_changed': True,
1541 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1541 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1542 % (user.username, repo.repo_name, locked))
1542 % (user.username, repo.repo_name, locked))
1543 }
1543 }
1544 return _d
1544 return _d
1545 except Exception:
1545 except Exception:
1546 log.exception(
1546 log.exception(
1547 "Exception occurred while trying to lock repository")
1547 "Exception occurred while trying to lock repository")
1548 raise JSONRPCError(
1548 raise JSONRPCError(
1549 'Error occurred locking repository `%s`' % repo.repo_name
1549 'Error occurred locking repository `%s`' % repo.repo_name
1550 )
1550 )
1551
1551
1552
1552
1553 @jsonrpc_method()
1553 @jsonrpc_method()
1554 def comment_commit(
1554 def comment_commit(
1555 request, apiuser, repoid, commit_id, message, status=Optional(None),
1555 request, apiuser, repoid, commit_id, message, status=Optional(None),
1556 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1556 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1557 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1557 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1558 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1558 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1559 """
1559 """
1560 Set a commit comment, and optionally change the status of the commit.
1560 Set a commit comment, and optionally change the status of the commit.
1561
1561
1562 :param apiuser: This is filled automatically from the |authtoken|.
1562 :param apiuser: This is filled automatically from the |authtoken|.
1563 :type apiuser: AuthUser
1563 :type apiuser: AuthUser
1564 :param repoid: Set the repository name or repository ID.
1564 :param repoid: Set the repository name or repository ID.
1565 :type repoid: str or int
1565 :type repoid: str or int
1566 :param commit_id: Specify the commit_id for which to set a comment.
1566 :param commit_id: Specify the commit_id for which to set a comment.
1567 :type commit_id: str
1567 :type commit_id: str
1568 :param message: The comment text.
1568 :param message: The comment text.
1569 :type message: str
1569 :type message: str
1570 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1570 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1571 'approved', 'rejected', 'under_review'
1571 'approved', 'rejected', 'under_review'
1572 :type status: str
1572 :type status: str
1573 :param comment_type: Comment type, one of: 'note', 'todo'
1573 :param comment_type: Comment type, one of: 'note', 'todo'
1574 :type comment_type: Optional(str), default: 'note'
1574 :type comment_type: Optional(str), default: 'note'
1575 :param resolves_comment_id: id of comment which this one will resolve
1575 :param resolves_comment_id: id of comment which this one will resolve
1576 :type resolves_comment_id: Optional(int)
1576 :type resolves_comment_id: Optional(int)
1577 :param extra_recipients: list of user ids or usernames to add
1577 :param extra_recipients: list of user ids or usernames to add
1578 notifications for this comment. Acts like a CC for notification
1578 notifications for this comment. Acts like a CC for notification
1579 :type extra_recipients: Optional(list)
1579 :type extra_recipients: Optional(list)
1580 :param userid: Set the user name of the comment creator.
1580 :param userid: Set the user name of the comment creator.
1581 :type userid: Optional(str or int)
1581 :type userid: Optional(str or int)
1582 :param send_email: Define if this comment should also send email notification
1582 :param send_email: Define if this comment should also send email notification
1583 :type send_email: Optional(bool)
1583 :type send_email: Optional(bool)
1584
1584
1585 Example error output:
1585 Example error output:
1586
1586
1587 .. code-block:: bash
1587 .. code-block:: bash
1588
1588
1589 {
1589 {
1590 "id" : <id_given_in_input>,
1590 "id" : <id_given_in_input>,
1591 "result" : {
1591 "result" : {
1592 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1592 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1593 "status_change": null or <status>,
1593 "status_change": null or <status>,
1594 "success": true
1594 "success": true
1595 },
1595 },
1596 "error" : null
1596 "error" : null
1597 }
1597 }
1598
1598
1599 """
1599 """
1600 _ = request.translate
1601
1600 repo = get_repo_or_error(repoid)
1602 repo = get_repo_or_error(repoid)
1601 if not has_superadmin_permission(apiuser):
1603 if not has_superadmin_permission(apiuser):
1602 _perms = ('repository.read', 'repository.write', 'repository.admin')
1604 _perms = ('repository.read', 'repository.write', 'repository.admin')
1603 validate_repo_permissions(apiuser, repoid, repo, _perms)
1605 validate_repo_permissions(apiuser, repoid, repo, _perms)
1606 db_repo_name = repo.repo_name
1604
1607
1605 try:
1608 try:
1606 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1609 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1607 commit_id = commit.raw_id
1610 commit_id = commit.raw_id
1608 except Exception as e:
1611 except Exception as e:
1609 log.exception('Failed to fetch commit')
1612 log.exception('Failed to fetch commit')
1610 raise JSONRPCError(safe_str(e))
1613 raise JSONRPCError(safe_str(e))
1611
1614
1612 if isinstance(userid, Optional):
1615 if isinstance(userid, Optional):
1613 userid = apiuser.user_id
1616 userid = apiuser.user_id
1614
1617
1615 user = get_user_or_error(userid)
1618 user = get_user_or_error(userid)
1616 status = Optional.extract(status)
1619 status = Optional.extract(status)
1617 comment_type = Optional.extract(comment_type)
1620 comment_type = Optional.extract(comment_type)
1618 resolves_comment_id = Optional.extract(resolves_comment_id)
1621 resolves_comment_id = Optional.extract(resolves_comment_id)
1619 extra_recipients = Optional.extract(extra_recipients)
1622 extra_recipients = Optional.extract(extra_recipients)
1620 send_email = Optional.extract(send_email, binary=True)
1623 send_email = Optional.extract(send_email, binary=True)
1621
1624
1622 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1625 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1623 if status and status not in allowed_statuses:
1626 if status and status not in allowed_statuses:
1624 raise JSONRPCError('Bad status, must be on '
1627 raise JSONRPCError('Bad status, must be on '
1625 'of %s got %s' % (allowed_statuses, status,))
1628 'of %s got %s' % (allowed_statuses, status,))
1626
1629
1627 if resolves_comment_id:
1630 if resolves_comment_id:
1628 comment = ChangesetComment.get(resolves_comment_id)
1631 comment = ChangesetComment.get(resolves_comment_id)
1629 if not comment:
1632 if not comment:
1630 raise JSONRPCError(
1633 raise JSONRPCError(
1631 'Invalid resolves_comment_id `%s` for this commit.'
1634 'Invalid resolves_comment_id `%s` for this commit.'
1632 % resolves_comment_id)
1635 % resolves_comment_id)
1633 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1636 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1634 raise JSONRPCError(
1637 raise JSONRPCError(
1635 'Comment `%s` is wrong type for setting status to resolved.'
1638 'Comment `%s` is wrong type for setting status to resolved.'
1636 % resolves_comment_id)
1639 % resolves_comment_id)
1637
1640
1638 try:
1641 try:
1639 rc_config = SettingsModel().get_all_settings()
1642 rc_config = SettingsModel().get_all_settings()
1640 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1643 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1641 status_change_label = ChangesetStatus.get_status_lbl(status)
1644 status_change_label = ChangesetStatus.get_status_lbl(status)
1642 comment = CommentsModel().create(
1645 comment = CommentsModel().create(
1643 message, repo, user, commit_id=commit_id,
1646 message, repo, user, commit_id=commit_id,
1644 status_change=status_change_label,
1647 status_change=status_change_label,
1645 status_change_type=status,
1648 status_change_type=status,
1646 renderer=renderer,
1649 renderer=renderer,
1647 comment_type=comment_type,
1650 comment_type=comment_type,
1648 resolves_comment_id=resolves_comment_id,
1651 resolves_comment_id=resolves_comment_id,
1649 auth_user=apiuser,
1652 auth_user=apiuser,
1650 extra_recipients=extra_recipients,
1653 extra_recipients=extra_recipients,
1651 send_email=send_email
1654 send_email=send_email
1652 )
1655 )
1656 is_inline = bool(comment.f_path and comment.line_no)
1657
1653 if status:
1658 if status:
1654 # also do a status change
1659 # also do a status change
1655 try:
1660 try:
1656 ChangesetStatusModel().set_status(
1661 ChangesetStatusModel().set_status(
1657 repo, status, user, comment, revision=commit_id,
1662 repo, status, user, comment, revision=commit_id,
1658 dont_allow_on_closed_pull_request=True
1663 dont_allow_on_closed_pull_request=True
1659 )
1664 )
1660 except StatusChangeOnClosedPullRequestError:
1665 except StatusChangeOnClosedPullRequestError:
1661 log.exception(
1666 log.exception(
1662 "Exception occurred while trying to change repo commit status")
1667 "Exception occurred while trying to change repo commit status")
1663 msg = ('Changing status on a commit associated with '
1668 msg = ('Changing status on a commit associated with '
1664 'a closed pull request is not allowed')
1669 'a closed pull request is not allowed')
1665 raise JSONRPCError(msg)
1670 raise JSONRPCError(msg)
1666
1671
1667 CommentsModel().trigger_commit_comment_hook(
1672 CommentsModel().trigger_commit_comment_hook(
1668 repo, apiuser, 'create',
1673 repo, apiuser, 'create',
1669 data={'comment': comment, 'commit': commit})
1674 data={'comment': comment, 'commit': commit})
1670
1675
1671 Session().commit()
1676 Session().commit()
1677
1678 comment_broadcast_channel = channelstream.comment_channel(
1679 db_repo_name, commit_obj=commit)
1680
1681 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1682 comment_type = 'inline' if is_inline else 'general'
1683 channelstream.comment_channelstream_push(
1684 request, comment_broadcast_channel, apiuser,
1685 _('posted a new {} comment').format(comment_type),
1686 comment_data=comment_data)
1687
1672 return {
1688 return {
1673 'msg': (
1689 'msg': (
1674 'Commented on commit `%s` for repository `%s`' % (
1690 'Commented on commit `%s` for repository `%s`' % (
1675 comment.revision, repo.repo_name)),
1691 comment.revision, repo.repo_name)),
1676 'status_change': status,
1692 'status_change': status,
1677 'success': True,
1693 'success': True,
1678 }
1694 }
1679 except JSONRPCError:
1695 except JSONRPCError:
1680 # catch any inside errors, and re-raise them to prevent from
1696 # catch any inside errors, and re-raise them to prevent from
1681 # below global catch to silence them
1697 # below global catch to silence them
1682 raise
1698 raise
1683 except Exception:
1699 except Exception:
1684 log.exception("Exception occurred while trying to comment on commit")
1700 log.exception("Exception occurred while trying to comment on commit")
1685 raise JSONRPCError(
1701 raise JSONRPCError(
1686 'failed to set comment on repository `%s`' % (repo.repo_name,)
1702 'failed to set comment on repository `%s`' % (repo.repo_name,)
1687 )
1703 )
1688
1704
1689
1705
1690 @jsonrpc_method()
1706 @jsonrpc_method()
1691 def get_repo_comments(request, apiuser, repoid,
1707 def get_repo_comments(request, apiuser, repoid,
1692 commit_id=Optional(None), comment_type=Optional(None),
1708 commit_id=Optional(None), comment_type=Optional(None),
1693 userid=Optional(None)):
1709 userid=Optional(None)):
1694 """
1710 """
1695 Get all comments for a repository
1711 Get all comments for a repository
1696
1712
1697 :param apiuser: This is filled automatically from the |authtoken|.
1713 :param apiuser: This is filled automatically from the |authtoken|.
1698 :type apiuser: AuthUser
1714 :type apiuser: AuthUser
1699 :param repoid: Set the repository name or repository ID.
1715 :param repoid: Set the repository name or repository ID.
1700 :type repoid: str or int
1716 :type repoid: str or int
1701 :param commit_id: Optionally filter the comments by the commit_id
1717 :param commit_id: Optionally filter the comments by the commit_id
1702 :type commit_id: Optional(str), default: None
1718 :type commit_id: Optional(str), default: None
1703 :param comment_type: Optionally filter the comments by the comment_type
1719 :param comment_type: Optionally filter the comments by the comment_type
1704 one of: 'note', 'todo'
1720 one of: 'note', 'todo'
1705 :type comment_type: Optional(str), default: None
1721 :type comment_type: Optional(str), default: None
1706 :param userid: Optionally filter the comments by the author of comment
1722 :param userid: Optionally filter the comments by the author of comment
1707 :type userid: Optional(str or int), Default: None
1723 :type userid: Optional(str or int), Default: None
1708
1724
1709 Example error output:
1725 Example error output:
1710
1726
1711 .. code-block:: bash
1727 .. code-block:: bash
1712
1728
1713 {
1729 {
1714 "id" : <id_given_in_input>,
1730 "id" : <id_given_in_input>,
1715 "result" : [
1731 "result" : [
1716 {
1732 {
1717 "comment_author": <USER_DETAILS>,
1733 "comment_author": <USER_DETAILS>,
1718 "comment_created_on": "2017-02-01T14:38:16.309",
1734 "comment_created_on": "2017-02-01T14:38:16.309",
1719 "comment_f_path": "file.txt",
1735 "comment_f_path": "file.txt",
1720 "comment_id": 282,
1736 "comment_id": 282,
1721 "comment_lineno": "n1",
1737 "comment_lineno": "n1",
1722 "comment_resolved_by": null,
1738 "comment_resolved_by": null,
1723 "comment_status": [],
1739 "comment_status": [],
1724 "comment_text": "This file needs a header",
1740 "comment_text": "This file needs a header",
1725 "comment_type": "todo",
1741 "comment_type": "todo",
1726 "comment_last_version: 0
1742 "comment_last_version: 0
1727 }
1743 }
1728 ],
1744 ],
1729 "error" : null
1745 "error" : null
1730 }
1746 }
1731
1747
1732 """
1748 """
1733 repo = get_repo_or_error(repoid)
1749 repo = get_repo_or_error(repoid)
1734 if not has_superadmin_permission(apiuser):
1750 if not has_superadmin_permission(apiuser):
1735 _perms = ('repository.read', 'repository.write', 'repository.admin')
1751 _perms = ('repository.read', 'repository.write', 'repository.admin')
1736 validate_repo_permissions(apiuser, repoid, repo, _perms)
1752 validate_repo_permissions(apiuser, repoid, repo, _perms)
1737
1753
1738 commit_id = Optional.extract(commit_id)
1754 commit_id = Optional.extract(commit_id)
1739
1755
1740 userid = Optional.extract(userid)
1756 userid = Optional.extract(userid)
1741 if userid:
1757 if userid:
1742 user = get_user_or_error(userid)
1758 user = get_user_or_error(userid)
1743 else:
1759 else:
1744 user = None
1760 user = None
1745
1761
1746 comment_type = Optional.extract(comment_type)
1762 comment_type = Optional.extract(comment_type)
1747 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1763 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1748 raise JSONRPCError(
1764 raise JSONRPCError(
1749 'comment_type must be one of `{}` got {}'.format(
1765 'comment_type must be one of `{}` got {}'.format(
1750 ChangesetComment.COMMENT_TYPES, comment_type)
1766 ChangesetComment.COMMENT_TYPES, comment_type)
1751 )
1767 )
1752
1768
1753 comments = CommentsModel().get_repository_comments(
1769 comments = CommentsModel().get_repository_comments(
1754 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1770 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1755 return comments
1771 return comments
1756
1772
1757
1773
1758 @jsonrpc_method()
1774 @jsonrpc_method()
1759 def get_comment(request, apiuser, comment_id):
1775 def get_comment(request, apiuser, comment_id):
1760 """
1776 """
1761 Get single comment from repository or pull_request
1777 Get single comment from repository or pull_request
1762
1778
1763 :param apiuser: This is filled automatically from the |authtoken|.
1779 :param apiuser: This is filled automatically from the |authtoken|.
1764 :type apiuser: AuthUser
1780 :type apiuser: AuthUser
1765 :param comment_id: comment id found in the URL of comment
1781 :param comment_id: comment id found in the URL of comment
1766 :type comment_id: str or int
1782 :type comment_id: str or int
1767
1783
1768 Example error output:
1784 Example error output:
1769
1785
1770 .. code-block:: bash
1786 .. code-block:: bash
1771
1787
1772 {
1788 {
1773 "id" : <id_given_in_input>,
1789 "id" : <id_given_in_input>,
1774 "result" : {
1790 "result" : {
1775 "comment_author": <USER_DETAILS>,
1791 "comment_author": <USER_DETAILS>,
1776 "comment_created_on": "2017-02-01T14:38:16.309",
1792 "comment_created_on": "2017-02-01T14:38:16.309",
1777 "comment_f_path": "file.txt",
1793 "comment_f_path": "file.txt",
1778 "comment_id": 282,
1794 "comment_id": 282,
1779 "comment_lineno": "n1",
1795 "comment_lineno": "n1",
1780 "comment_resolved_by": null,
1796 "comment_resolved_by": null,
1781 "comment_status": [],
1797 "comment_status": [],
1782 "comment_text": "This file needs a header",
1798 "comment_text": "This file needs a header",
1783 "comment_type": "todo",
1799 "comment_type": "todo",
1784 "comment_last_version: 0
1800 "comment_last_version: 0
1785 },
1801 },
1786 "error" : null
1802 "error" : null
1787 }
1803 }
1788
1804
1789 """
1805 """
1790
1806
1791 comment = ChangesetComment.get(comment_id)
1807 comment = ChangesetComment.get(comment_id)
1792 if not comment:
1808 if not comment:
1793 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1809 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1794
1810
1795 perms = ('repository.read', 'repository.write', 'repository.admin')
1811 perms = ('repository.read', 'repository.write', 'repository.admin')
1796 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1812 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1797 (user=apiuser, repo_name=comment.repo.repo_name)
1813 (user=apiuser, repo_name=comment.repo.repo_name)
1798
1814
1799 if not has_comment_perm:
1815 if not has_comment_perm:
1800 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1816 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1801
1817
1802 return comment
1818 return comment
1803
1819
1804
1820
1805 @jsonrpc_method()
1821 @jsonrpc_method()
1806 def edit_comment(request, apiuser, message, comment_id, version,
1822 def edit_comment(request, apiuser, message, comment_id, version,
1807 userid=Optional(OAttr('apiuser'))):
1823 userid=Optional(OAttr('apiuser'))):
1808 """
1824 """
1809 Edit comment on the pull request or commit,
1825 Edit comment on the pull request or commit,
1810 specified by the `comment_id` and version. Initially version should be 0
1826 specified by the `comment_id` and version. Initially version should be 0
1811
1827
1812 :param apiuser: This is filled automatically from the |authtoken|.
1828 :param apiuser: This is filled automatically from the |authtoken|.
1813 :type apiuser: AuthUser
1829 :type apiuser: AuthUser
1814 :param comment_id: Specify the comment_id for editing
1830 :param comment_id: Specify the comment_id for editing
1815 :type comment_id: int
1831 :type comment_id: int
1816 :param version: version of the comment that will be created, starts from 0
1832 :param version: version of the comment that will be created, starts from 0
1817 :type version: int
1833 :type version: int
1818 :param message: The text content of the comment.
1834 :param message: The text content of the comment.
1819 :type message: str
1835 :type message: str
1820 :param userid: Comment on the pull request as this user
1836 :param userid: Comment on the pull request as this user
1821 :type userid: Optional(str or int)
1837 :type userid: Optional(str or int)
1822
1838
1823 Example output:
1839 Example output:
1824
1840
1825 .. code-block:: bash
1841 .. code-block:: bash
1826
1842
1827 id : <id_given_in_input>
1843 id : <id_given_in_input>
1828 result : {
1844 result : {
1829 "comment": "<comment data>",
1845 "comment": "<comment data>",
1830 "version": "<Integer>",
1846 "version": "<Integer>",
1831 },
1847 },
1832 error : null
1848 error : null
1833 """
1849 """
1834
1850
1835 auth_user = apiuser
1851 auth_user = apiuser
1836 comment = ChangesetComment.get(comment_id)
1852 comment = ChangesetComment.get(comment_id)
1837 if not comment:
1853 if not comment:
1838 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1854 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1839
1855
1840 is_super_admin = has_superadmin_permission(apiuser)
1856 is_super_admin = has_superadmin_permission(apiuser)
1841 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1857 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1842 (user=apiuser, repo_name=comment.repo.repo_name)
1858 (user=apiuser, repo_name=comment.repo.repo_name)
1843
1859
1844 if not isinstance(userid, Optional):
1860 if not isinstance(userid, Optional):
1845 if is_super_admin or is_repo_admin:
1861 if is_super_admin or is_repo_admin:
1846 apiuser = get_user_or_error(userid)
1862 apiuser = get_user_or_error(userid)
1847 auth_user = apiuser.AuthUser()
1863 auth_user = apiuser.AuthUser()
1848 else:
1864 else:
1849 raise JSONRPCError('userid is not the same as your user')
1865 raise JSONRPCError('userid is not the same as your user')
1850
1866
1851 comment_author = comment.author.user_id == auth_user.user_id
1867 comment_author = comment.author.user_id == auth_user.user_id
1852 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1868 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1853 raise JSONRPCError("you don't have access to edit this comment")
1869 raise JSONRPCError("you don't have access to edit this comment")
1854
1870
1855 try:
1871 try:
1856 comment_history = CommentsModel().edit(
1872 comment_history = CommentsModel().edit(
1857 comment_id=comment_id,
1873 comment_id=comment_id,
1858 text=message,
1874 text=message,
1859 auth_user=auth_user,
1875 auth_user=auth_user,
1860 version=version,
1876 version=version,
1861 )
1877 )
1862 Session().commit()
1878 Session().commit()
1863 except CommentVersionMismatch:
1879 except CommentVersionMismatch:
1864 raise JSONRPCError(
1880 raise JSONRPCError(
1865 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1881 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1866 )
1882 )
1867 if not comment_history and not message:
1883 if not comment_history and not message:
1868 raise JSONRPCError(
1884 raise JSONRPCError(
1869 "comment ({}) can't be changed with empty string".format(comment_id)
1885 "comment ({}) can't be changed with empty string".format(comment_id)
1870 )
1886 )
1871
1887
1872 if comment.pull_request:
1888 if comment.pull_request:
1873 pull_request = comment.pull_request
1889 pull_request = comment.pull_request
1874 PullRequestModel().trigger_pull_request_hook(
1890 PullRequestModel().trigger_pull_request_hook(
1875 pull_request, apiuser, 'comment_edit',
1891 pull_request, apiuser, 'comment_edit',
1876 data={'comment': comment})
1892 data={'comment': comment})
1877 else:
1893 else:
1878 db_repo = comment.repo
1894 db_repo = comment.repo
1879 commit_id = comment.revision
1895 commit_id = comment.revision
1880 commit = db_repo.get_commit(commit_id)
1896 commit = db_repo.get_commit(commit_id)
1881 CommentsModel().trigger_commit_comment_hook(
1897 CommentsModel().trigger_commit_comment_hook(
1882 db_repo, apiuser, 'edit',
1898 db_repo, apiuser, 'edit',
1883 data={'comment': comment, 'commit': commit})
1899 data={'comment': comment, 'commit': commit})
1884
1900
1885 data = {
1901 data = {
1886 'comment': comment,
1902 'comment': comment,
1887 'version': comment_history.version if comment_history else None,
1903 'version': comment_history.version if comment_history else None,
1888 }
1904 }
1889 return data
1905 return data
1890
1906
1891
1907
1892 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1908 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1893 # @jsonrpc_method()
1909 # @jsonrpc_method()
1894 # def delete_comment(request, apiuser, comment_id):
1910 # def delete_comment(request, apiuser, comment_id):
1895 # auth_user = apiuser
1911 # auth_user = apiuser
1896 #
1912 #
1897 # comment = ChangesetComment.get(comment_id)
1913 # comment = ChangesetComment.get(comment_id)
1898 # if not comment:
1914 # if not comment:
1899 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1915 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1900 #
1916 #
1901 # is_super_admin = has_superadmin_permission(apiuser)
1917 # is_super_admin = has_superadmin_permission(apiuser)
1902 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1918 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1903 # (user=apiuser, repo_name=comment.repo.repo_name)
1919 # (user=apiuser, repo_name=comment.repo.repo_name)
1904 #
1920 #
1905 # comment_author = comment.author.user_id == auth_user.user_id
1921 # comment_author = comment.author.user_id == auth_user.user_id
1906 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1922 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1907 # raise JSONRPCError("you don't have access to edit this comment")
1923 # raise JSONRPCError("you don't have access to edit this comment")
1908
1924
1909 @jsonrpc_method()
1925 @jsonrpc_method()
1910 def grant_user_permission(request, apiuser, repoid, userid, perm):
1926 def grant_user_permission(request, apiuser, repoid, userid, perm):
1911 """
1927 """
1912 Grant permissions for the specified user on the given repository,
1928 Grant permissions for the specified user on the given repository,
1913 or update existing permissions if found.
1929 or update existing permissions if found.
1914
1930
1915 This command can only be run using an |authtoken| with admin
1931 This command can only be run using an |authtoken| with admin
1916 permissions on the |repo|.
1932 permissions on the |repo|.
1917
1933
1918 :param apiuser: This is filled automatically from the |authtoken|.
1934 :param apiuser: This is filled automatically from the |authtoken|.
1919 :type apiuser: AuthUser
1935 :type apiuser: AuthUser
1920 :param repoid: Set the repository name or repository ID.
1936 :param repoid: Set the repository name or repository ID.
1921 :type repoid: str or int
1937 :type repoid: str or int
1922 :param userid: Set the user name.
1938 :param userid: Set the user name.
1923 :type userid: str
1939 :type userid: str
1924 :param perm: Set the user permissions, using the following format
1940 :param perm: Set the user permissions, using the following format
1925 ``(repository.(none|read|write|admin))``
1941 ``(repository.(none|read|write|admin))``
1926 :type perm: str
1942 :type perm: str
1927
1943
1928 Example output:
1944 Example output:
1929
1945
1930 .. code-block:: bash
1946 .. code-block:: bash
1931
1947
1932 id : <id_given_in_input>
1948 id : <id_given_in_input>
1933 result: {
1949 result: {
1934 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1950 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1935 "success": true
1951 "success": true
1936 }
1952 }
1937 error: null
1953 error: null
1938 """
1954 """
1939
1955
1940 repo = get_repo_or_error(repoid)
1956 repo = get_repo_or_error(repoid)
1941 user = get_user_or_error(userid)
1957 user = get_user_or_error(userid)
1942 perm = get_perm_or_error(perm)
1958 perm = get_perm_or_error(perm)
1943 if not has_superadmin_permission(apiuser):
1959 if not has_superadmin_permission(apiuser):
1944 _perms = ('repository.admin',)
1960 _perms = ('repository.admin',)
1945 validate_repo_permissions(apiuser, repoid, repo, _perms)
1961 validate_repo_permissions(apiuser, repoid, repo, _perms)
1946
1962
1947 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1963 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1948 try:
1964 try:
1949 changes = RepoModel().update_permissions(
1965 changes = RepoModel().update_permissions(
1950 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1966 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1951
1967
1952 action_data = {
1968 action_data = {
1953 'added': changes['added'],
1969 'added': changes['added'],
1954 'updated': changes['updated'],
1970 'updated': changes['updated'],
1955 'deleted': changes['deleted'],
1971 'deleted': changes['deleted'],
1956 }
1972 }
1957 audit_logger.store_api(
1973 audit_logger.store_api(
1958 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1974 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1959 Session().commit()
1975 Session().commit()
1960 PermissionModel().flush_user_permission_caches(changes)
1976 PermissionModel().flush_user_permission_caches(changes)
1961
1977
1962 return {
1978 return {
1963 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1979 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1964 perm.permission_name, user.username, repo.repo_name
1980 perm.permission_name, user.username, repo.repo_name
1965 ),
1981 ),
1966 'success': True
1982 'success': True
1967 }
1983 }
1968 except Exception:
1984 except Exception:
1969 log.exception("Exception occurred while trying edit permissions for repo")
1985 log.exception("Exception occurred while trying edit permissions for repo")
1970 raise JSONRPCError(
1986 raise JSONRPCError(
1971 'failed to edit permission for user: `%s` in repo: `%s`' % (
1987 'failed to edit permission for user: `%s` in repo: `%s`' % (
1972 userid, repoid
1988 userid, repoid
1973 )
1989 )
1974 )
1990 )
1975
1991
1976
1992
1977 @jsonrpc_method()
1993 @jsonrpc_method()
1978 def revoke_user_permission(request, apiuser, repoid, userid):
1994 def revoke_user_permission(request, apiuser, repoid, userid):
1979 """
1995 """
1980 Revoke permission for a user on the specified repository.
1996 Revoke permission for a user on the specified repository.
1981
1997
1982 This command can only be run using an |authtoken| with admin
1998 This command can only be run using an |authtoken| with admin
1983 permissions on the |repo|.
1999 permissions on the |repo|.
1984
2000
1985 :param apiuser: This is filled automatically from the |authtoken|.
2001 :param apiuser: This is filled automatically from the |authtoken|.
1986 :type apiuser: AuthUser
2002 :type apiuser: AuthUser
1987 :param repoid: Set the repository name or repository ID.
2003 :param repoid: Set the repository name or repository ID.
1988 :type repoid: str or int
2004 :type repoid: str or int
1989 :param userid: Set the user name of revoked user.
2005 :param userid: Set the user name of revoked user.
1990 :type userid: str or int
2006 :type userid: str or int
1991
2007
1992 Example error output:
2008 Example error output:
1993
2009
1994 .. code-block:: bash
2010 .. code-block:: bash
1995
2011
1996 id : <id_given_in_input>
2012 id : <id_given_in_input>
1997 result: {
2013 result: {
1998 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
2014 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1999 "success": true
2015 "success": true
2000 }
2016 }
2001 error: null
2017 error: null
2002 """
2018 """
2003
2019
2004 repo = get_repo_or_error(repoid)
2020 repo = get_repo_or_error(repoid)
2005 user = get_user_or_error(userid)
2021 user = get_user_or_error(userid)
2006 if not has_superadmin_permission(apiuser):
2022 if not has_superadmin_permission(apiuser):
2007 _perms = ('repository.admin',)
2023 _perms = ('repository.admin',)
2008 validate_repo_permissions(apiuser, repoid, repo, _perms)
2024 validate_repo_permissions(apiuser, repoid, repo, _perms)
2009
2025
2010 perm_deletions = [[user.user_id, None, "user"]]
2026 perm_deletions = [[user.user_id, None, "user"]]
2011 try:
2027 try:
2012 changes = RepoModel().update_permissions(
2028 changes = RepoModel().update_permissions(
2013 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2029 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2014
2030
2015 action_data = {
2031 action_data = {
2016 'added': changes['added'],
2032 'added': changes['added'],
2017 'updated': changes['updated'],
2033 'updated': changes['updated'],
2018 'deleted': changes['deleted'],
2034 'deleted': changes['deleted'],
2019 }
2035 }
2020 audit_logger.store_api(
2036 audit_logger.store_api(
2021 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2037 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2022 Session().commit()
2038 Session().commit()
2023 PermissionModel().flush_user_permission_caches(changes)
2039 PermissionModel().flush_user_permission_caches(changes)
2024
2040
2025 return {
2041 return {
2026 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
2042 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
2027 user.username, repo.repo_name
2043 user.username, repo.repo_name
2028 ),
2044 ),
2029 'success': True
2045 'success': True
2030 }
2046 }
2031 except Exception:
2047 except Exception:
2032 log.exception("Exception occurred while trying revoke permissions to repo")
2048 log.exception("Exception occurred while trying revoke permissions to repo")
2033 raise JSONRPCError(
2049 raise JSONRPCError(
2034 'failed to edit permission for user: `%s` in repo: `%s`' % (
2050 'failed to edit permission for user: `%s` in repo: `%s`' % (
2035 userid, repoid
2051 userid, repoid
2036 )
2052 )
2037 )
2053 )
2038
2054
2039
2055
2040 @jsonrpc_method()
2056 @jsonrpc_method()
2041 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2057 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2042 """
2058 """
2043 Grant permission for a user group on the specified repository,
2059 Grant permission for a user group on the specified repository,
2044 or update existing permissions.
2060 or update existing permissions.
2045
2061
2046 This command can only be run using an |authtoken| with admin
2062 This command can only be run using an |authtoken| with admin
2047 permissions on the |repo|.
2063 permissions on the |repo|.
2048
2064
2049 :param apiuser: This is filled automatically from the |authtoken|.
2065 :param apiuser: This is filled automatically from the |authtoken|.
2050 :type apiuser: AuthUser
2066 :type apiuser: AuthUser
2051 :param repoid: Set the repository name or repository ID.
2067 :param repoid: Set the repository name or repository ID.
2052 :type repoid: str or int
2068 :type repoid: str or int
2053 :param usergroupid: Specify the ID of the user group.
2069 :param usergroupid: Specify the ID of the user group.
2054 :type usergroupid: str or int
2070 :type usergroupid: str or int
2055 :param perm: Set the user group permissions using the following
2071 :param perm: Set the user group permissions using the following
2056 format: (repository.(none|read|write|admin))
2072 format: (repository.(none|read|write|admin))
2057 :type perm: str
2073 :type perm: str
2058
2074
2059 Example output:
2075 Example output:
2060
2076
2061 .. code-block:: bash
2077 .. code-block:: bash
2062
2078
2063 id : <id_given_in_input>
2079 id : <id_given_in_input>
2064 result : {
2080 result : {
2065 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2081 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2066 "success": true
2082 "success": true
2067
2083
2068 }
2084 }
2069 error : null
2085 error : null
2070
2086
2071 Example error output:
2087 Example error output:
2072
2088
2073 .. code-block:: bash
2089 .. code-block:: bash
2074
2090
2075 id : <id_given_in_input>
2091 id : <id_given_in_input>
2076 result : null
2092 result : null
2077 error : {
2093 error : {
2078 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2094 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2079 }
2095 }
2080
2096
2081 """
2097 """
2082
2098
2083 repo = get_repo_or_error(repoid)
2099 repo = get_repo_or_error(repoid)
2084 perm = get_perm_or_error(perm)
2100 perm = get_perm_or_error(perm)
2085 if not has_superadmin_permission(apiuser):
2101 if not has_superadmin_permission(apiuser):
2086 _perms = ('repository.admin',)
2102 _perms = ('repository.admin',)
2087 validate_repo_permissions(apiuser, repoid, repo, _perms)
2103 validate_repo_permissions(apiuser, repoid, repo, _perms)
2088
2104
2089 user_group = get_user_group_or_error(usergroupid)
2105 user_group = get_user_group_or_error(usergroupid)
2090 if not has_superadmin_permission(apiuser):
2106 if not has_superadmin_permission(apiuser):
2091 # check if we have at least read permission for this user group !
2107 # check if we have at least read permission for this user group !
2092 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2108 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2093 if not HasUserGroupPermissionAnyApi(*_perms)(
2109 if not HasUserGroupPermissionAnyApi(*_perms)(
2094 user=apiuser, user_group_name=user_group.users_group_name):
2110 user=apiuser, user_group_name=user_group.users_group_name):
2095 raise JSONRPCError(
2111 raise JSONRPCError(
2096 'user group `%s` does not exist' % (usergroupid,))
2112 'user group `%s` does not exist' % (usergroupid,))
2097
2113
2098 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2114 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2099 try:
2115 try:
2100 changes = RepoModel().update_permissions(
2116 changes = RepoModel().update_permissions(
2101 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2117 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2102 action_data = {
2118 action_data = {
2103 'added': changes['added'],
2119 'added': changes['added'],
2104 'updated': changes['updated'],
2120 'updated': changes['updated'],
2105 'deleted': changes['deleted'],
2121 'deleted': changes['deleted'],
2106 }
2122 }
2107 audit_logger.store_api(
2123 audit_logger.store_api(
2108 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2124 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2109 Session().commit()
2125 Session().commit()
2110 PermissionModel().flush_user_permission_caches(changes)
2126 PermissionModel().flush_user_permission_caches(changes)
2111
2127
2112 return {
2128 return {
2113 'msg': 'Granted perm: `%s` for user group: `%s` in '
2129 'msg': 'Granted perm: `%s` for user group: `%s` in '
2114 'repo: `%s`' % (
2130 'repo: `%s`' % (
2115 perm.permission_name, user_group.users_group_name,
2131 perm.permission_name, user_group.users_group_name,
2116 repo.repo_name
2132 repo.repo_name
2117 ),
2133 ),
2118 'success': True
2134 'success': True
2119 }
2135 }
2120 except Exception:
2136 except Exception:
2121 log.exception(
2137 log.exception(
2122 "Exception occurred while trying change permission on repo")
2138 "Exception occurred while trying change permission on repo")
2123 raise JSONRPCError(
2139 raise JSONRPCError(
2124 'failed to edit permission for user group: `%s` in '
2140 'failed to edit permission for user group: `%s` in '
2125 'repo: `%s`' % (
2141 'repo: `%s`' % (
2126 usergroupid, repo.repo_name
2142 usergroupid, repo.repo_name
2127 )
2143 )
2128 )
2144 )
2129
2145
2130
2146
2131 @jsonrpc_method()
2147 @jsonrpc_method()
2132 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2148 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2133 """
2149 """
2134 Revoke the permissions of a user group on a given repository.
2150 Revoke the permissions of a user group on a given repository.
2135
2151
2136 This command can only be run using an |authtoken| with admin
2152 This command can only be run using an |authtoken| with admin
2137 permissions on the |repo|.
2153 permissions on the |repo|.
2138
2154
2139 :param apiuser: This is filled automatically from the |authtoken|.
2155 :param apiuser: This is filled automatically from the |authtoken|.
2140 :type apiuser: AuthUser
2156 :type apiuser: AuthUser
2141 :param repoid: Set the repository name or repository ID.
2157 :param repoid: Set the repository name or repository ID.
2142 :type repoid: str or int
2158 :type repoid: str or int
2143 :param usergroupid: Specify the user group ID.
2159 :param usergroupid: Specify the user group ID.
2144 :type usergroupid: str or int
2160 :type usergroupid: str or int
2145
2161
2146 Example output:
2162 Example output:
2147
2163
2148 .. code-block:: bash
2164 .. code-block:: bash
2149
2165
2150 id : <id_given_in_input>
2166 id : <id_given_in_input>
2151 result: {
2167 result: {
2152 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2168 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2153 "success": true
2169 "success": true
2154 }
2170 }
2155 error: null
2171 error: null
2156 """
2172 """
2157
2173
2158 repo = get_repo_or_error(repoid)
2174 repo = get_repo_or_error(repoid)
2159 if not has_superadmin_permission(apiuser):
2175 if not has_superadmin_permission(apiuser):
2160 _perms = ('repository.admin',)
2176 _perms = ('repository.admin',)
2161 validate_repo_permissions(apiuser, repoid, repo, _perms)
2177 validate_repo_permissions(apiuser, repoid, repo, _perms)
2162
2178
2163 user_group = get_user_group_or_error(usergroupid)
2179 user_group = get_user_group_or_error(usergroupid)
2164 if not has_superadmin_permission(apiuser):
2180 if not has_superadmin_permission(apiuser):
2165 # check if we have at least read permission for this user group !
2181 # check if we have at least read permission for this user group !
2166 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2182 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2167 if not HasUserGroupPermissionAnyApi(*_perms)(
2183 if not HasUserGroupPermissionAnyApi(*_perms)(
2168 user=apiuser, user_group_name=user_group.users_group_name):
2184 user=apiuser, user_group_name=user_group.users_group_name):
2169 raise JSONRPCError(
2185 raise JSONRPCError(
2170 'user group `%s` does not exist' % (usergroupid,))
2186 'user group `%s` does not exist' % (usergroupid,))
2171
2187
2172 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2188 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2173 try:
2189 try:
2174 changes = RepoModel().update_permissions(
2190 changes = RepoModel().update_permissions(
2175 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2191 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2176 action_data = {
2192 action_data = {
2177 'added': changes['added'],
2193 'added': changes['added'],
2178 'updated': changes['updated'],
2194 'updated': changes['updated'],
2179 'deleted': changes['deleted'],
2195 'deleted': changes['deleted'],
2180 }
2196 }
2181 audit_logger.store_api(
2197 audit_logger.store_api(
2182 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2198 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2183 Session().commit()
2199 Session().commit()
2184 PermissionModel().flush_user_permission_caches(changes)
2200 PermissionModel().flush_user_permission_caches(changes)
2185
2201
2186 return {
2202 return {
2187 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2203 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2188 user_group.users_group_name, repo.repo_name
2204 user_group.users_group_name, repo.repo_name
2189 ),
2205 ),
2190 'success': True
2206 'success': True
2191 }
2207 }
2192 except Exception:
2208 except Exception:
2193 log.exception("Exception occurred while trying revoke "
2209 log.exception("Exception occurred while trying revoke "
2194 "user group permission on repo")
2210 "user group permission on repo")
2195 raise JSONRPCError(
2211 raise JSONRPCError(
2196 'failed to edit permission for user group: `%s` in '
2212 'failed to edit permission for user group: `%s` in '
2197 'repo: `%s`' % (
2213 'repo: `%s`' % (
2198 user_group.users_group_name, repo.repo_name
2214 user_group.users_group_name, repo.repo_name
2199 )
2215 )
2200 )
2216 )
2201
2217
2202
2218
2203 @jsonrpc_method()
2219 @jsonrpc_method()
2204 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2220 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2205 """
2221 """
2206 Triggers a pull on the given repository from a remote location. You
2222 Triggers a pull on the given repository from a remote location. You
2207 can use this to keep remote repositories up-to-date.
2223 can use this to keep remote repositories up-to-date.
2208
2224
2209 This command can only be run using an |authtoken| with admin
2225 This command can only be run using an |authtoken| with admin
2210 rights to the specified repository. For more information,
2226 rights to the specified repository. For more information,
2211 see :ref:`config-token-ref`.
2227 see :ref:`config-token-ref`.
2212
2228
2213 This command takes the following options:
2229 This command takes the following options:
2214
2230
2215 :param apiuser: This is filled automatically from the |authtoken|.
2231 :param apiuser: This is filled automatically from the |authtoken|.
2216 :type apiuser: AuthUser
2232 :type apiuser: AuthUser
2217 :param repoid: The repository name or repository ID.
2233 :param repoid: The repository name or repository ID.
2218 :type repoid: str or int
2234 :type repoid: str or int
2219 :param remote_uri: Optional remote URI to pass in for pull
2235 :param remote_uri: Optional remote URI to pass in for pull
2220 :type remote_uri: str
2236 :type remote_uri: str
2221
2237
2222 Example output:
2238 Example output:
2223
2239
2224 .. code-block:: bash
2240 .. code-block:: bash
2225
2241
2226 id : <id_given_in_input>
2242 id : <id_given_in_input>
2227 result : {
2243 result : {
2228 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2244 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2229 "repository": "<repository name>"
2245 "repository": "<repository name>"
2230 }
2246 }
2231 error : null
2247 error : null
2232
2248
2233 Example error output:
2249 Example error output:
2234
2250
2235 .. code-block:: bash
2251 .. code-block:: bash
2236
2252
2237 id : <id_given_in_input>
2253 id : <id_given_in_input>
2238 result : null
2254 result : null
2239 error : {
2255 error : {
2240 "Unable to push changes from `<remote_url>`"
2256 "Unable to push changes from `<remote_url>`"
2241 }
2257 }
2242
2258
2243 """
2259 """
2244
2260
2245 repo = get_repo_or_error(repoid)
2261 repo = get_repo_or_error(repoid)
2246 remote_uri = Optional.extract(remote_uri)
2262 remote_uri = Optional.extract(remote_uri)
2247 remote_uri_display = remote_uri or repo.clone_uri_hidden
2263 remote_uri_display = remote_uri or repo.clone_uri_hidden
2248 if not has_superadmin_permission(apiuser):
2264 if not has_superadmin_permission(apiuser):
2249 _perms = ('repository.admin',)
2265 _perms = ('repository.admin',)
2250 validate_repo_permissions(apiuser, repoid, repo, _perms)
2266 validate_repo_permissions(apiuser, repoid, repo, _perms)
2251
2267
2252 try:
2268 try:
2253 ScmModel().pull_changes(
2269 ScmModel().pull_changes(
2254 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2270 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2255 return {
2271 return {
2256 'msg': 'Pulled from url `%s` on repo `%s`' % (
2272 'msg': 'Pulled from url `%s` on repo `%s`' % (
2257 remote_uri_display, repo.repo_name),
2273 remote_uri_display, repo.repo_name),
2258 'repository': repo.repo_name
2274 'repository': repo.repo_name
2259 }
2275 }
2260 except Exception:
2276 except Exception:
2261 log.exception("Exception occurred while trying to "
2277 log.exception("Exception occurred while trying to "
2262 "pull changes from remote location")
2278 "pull changes from remote location")
2263 raise JSONRPCError(
2279 raise JSONRPCError(
2264 'Unable to pull changes from `%s`' % remote_uri_display
2280 'Unable to pull changes from `%s`' % remote_uri_display
2265 )
2281 )
2266
2282
2267
2283
2268 @jsonrpc_method()
2284 @jsonrpc_method()
2269 def strip(request, apiuser, repoid, revision, branch):
2285 def strip(request, apiuser, repoid, revision, branch):
2270 """
2286 """
2271 Strips the given revision from the specified repository.
2287 Strips the given revision from the specified repository.
2272
2288
2273 * This will remove the revision and all of its decendants.
2289 * This will remove the revision and all of its decendants.
2274
2290
2275 This command can only be run using an |authtoken| with admin rights to
2291 This command can only be run using an |authtoken| with admin rights to
2276 the specified repository.
2292 the specified repository.
2277
2293
2278 This command takes the following options:
2294 This command takes the following options:
2279
2295
2280 :param apiuser: This is filled automatically from the |authtoken|.
2296 :param apiuser: This is filled automatically from the |authtoken|.
2281 :type apiuser: AuthUser
2297 :type apiuser: AuthUser
2282 :param repoid: The repository name or repository ID.
2298 :param repoid: The repository name or repository ID.
2283 :type repoid: str or int
2299 :type repoid: str or int
2284 :param revision: The revision you wish to strip.
2300 :param revision: The revision you wish to strip.
2285 :type revision: str
2301 :type revision: str
2286 :param branch: The branch from which to strip the revision.
2302 :param branch: The branch from which to strip the revision.
2287 :type branch: str
2303 :type branch: str
2288
2304
2289 Example output:
2305 Example output:
2290
2306
2291 .. code-block:: bash
2307 .. code-block:: bash
2292
2308
2293 id : <id_given_in_input>
2309 id : <id_given_in_input>
2294 result : {
2310 result : {
2295 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2311 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2296 "repository": "<repository name>"
2312 "repository": "<repository name>"
2297 }
2313 }
2298 error : null
2314 error : null
2299
2315
2300 Example error output:
2316 Example error output:
2301
2317
2302 .. code-block:: bash
2318 .. code-block:: bash
2303
2319
2304 id : <id_given_in_input>
2320 id : <id_given_in_input>
2305 result : null
2321 result : null
2306 error : {
2322 error : {
2307 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2323 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2308 }
2324 }
2309
2325
2310 """
2326 """
2311
2327
2312 repo = get_repo_or_error(repoid)
2328 repo = get_repo_or_error(repoid)
2313 if not has_superadmin_permission(apiuser):
2329 if not has_superadmin_permission(apiuser):
2314 _perms = ('repository.admin',)
2330 _perms = ('repository.admin',)
2315 validate_repo_permissions(apiuser, repoid, repo, _perms)
2331 validate_repo_permissions(apiuser, repoid, repo, _perms)
2316
2332
2317 try:
2333 try:
2318 ScmModel().strip(repo, revision, branch)
2334 ScmModel().strip(repo, revision, branch)
2319 audit_logger.store_api(
2335 audit_logger.store_api(
2320 'repo.commit.strip', action_data={'commit_id': revision},
2336 'repo.commit.strip', action_data={'commit_id': revision},
2321 repo=repo,
2337 repo=repo,
2322 user=apiuser, commit=True)
2338 user=apiuser, commit=True)
2323
2339
2324 return {
2340 return {
2325 'msg': 'Stripped commit %s from repo `%s`' % (
2341 'msg': 'Stripped commit %s from repo `%s`' % (
2326 revision, repo.repo_name),
2342 revision, repo.repo_name),
2327 'repository': repo.repo_name
2343 'repository': repo.repo_name
2328 }
2344 }
2329 except Exception:
2345 except Exception:
2330 log.exception("Exception while trying to strip")
2346 log.exception("Exception while trying to strip")
2331 raise JSONRPCError(
2347 raise JSONRPCError(
2332 'Unable to strip commit %s from repo `%s`' % (
2348 'Unable to strip commit %s from repo `%s`' % (
2333 revision, repo.repo_name)
2349 revision, repo.repo_name)
2334 )
2350 )
2335
2351
2336
2352
2337 @jsonrpc_method()
2353 @jsonrpc_method()
2338 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2354 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2339 """
2355 """
2340 Returns all settings for a repository. If key is given it only returns the
2356 Returns all settings for a repository. If key is given it only returns the
2341 setting identified by the key or null.
2357 setting identified by the key or null.
2342
2358
2343 :param apiuser: This is filled automatically from the |authtoken|.
2359 :param apiuser: This is filled automatically from the |authtoken|.
2344 :type apiuser: AuthUser
2360 :type apiuser: AuthUser
2345 :param repoid: The repository name or repository id.
2361 :param repoid: The repository name or repository id.
2346 :type repoid: str or int
2362 :type repoid: str or int
2347 :param key: Key of the setting to return.
2363 :param key: Key of the setting to return.
2348 :type: key: Optional(str)
2364 :type: key: Optional(str)
2349
2365
2350 Example output:
2366 Example output:
2351
2367
2352 .. code-block:: bash
2368 .. code-block:: bash
2353
2369
2354 {
2370 {
2355 "error": null,
2371 "error": null,
2356 "id": 237,
2372 "id": 237,
2357 "result": {
2373 "result": {
2358 "extensions_largefiles": true,
2374 "extensions_largefiles": true,
2359 "extensions_evolve": true,
2375 "extensions_evolve": true,
2360 "hooks_changegroup_push_logger": true,
2376 "hooks_changegroup_push_logger": true,
2361 "hooks_changegroup_repo_size": false,
2377 "hooks_changegroup_repo_size": false,
2362 "hooks_outgoing_pull_logger": true,
2378 "hooks_outgoing_pull_logger": true,
2363 "phases_publish": "True",
2379 "phases_publish": "True",
2364 "rhodecode_hg_use_rebase_for_merging": true,
2380 "rhodecode_hg_use_rebase_for_merging": true,
2365 "rhodecode_pr_merge_enabled": true,
2381 "rhodecode_pr_merge_enabled": true,
2366 "rhodecode_use_outdated_comments": true
2382 "rhodecode_use_outdated_comments": true
2367 }
2383 }
2368 }
2384 }
2369 """
2385 """
2370
2386
2371 # Restrict access to this api method to super-admins, and repo admins only.
2387 # Restrict access to this api method to super-admins, and repo admins only.
2372 repo = get_repo_or_error(repoid)
2388 repo = get_repo_or_error(repoid)
2373 if not has_superadmin_permission(apiuser):
2389 if not has_superadmin_permission(apiuser):
2374 _perms = ('repository.admin',)
2390 _perms = ('repository.admin',)
2375 validate_repo_permissions(apiuser, repoid, repo, _perms)
2391 validate_repo_permissions(apiuser, repoid, repo, _perms)
2376
2392
2377 try:
2393 try:
2378 settings_model = VcsSettingsModel(repo=repo)
2394 settings_model = VcsSettingsModel(repo=repo)
2379 settings = settings_model.get_global_settings()
2395 settings = settings_model.get_global_settings()
2380 settings.update(settings_model.get_repo_settings())
2396 settings.update(settings_model.get_repo_settings())
2381
2397
2382 # If only a single setting is requested fetch it from all settings.
2398 # If only a single setting is requested fetch it from all settings.
2383 key = Optional.extract(key)
2399 key = Optional.extract(key)
2384 if key is not None:
2400 if key is not None:
2385 settings = settings.get(key, None)
2401 settings = settings.get(key, None)
2386 except Exception:
2402 except Exception:
2387 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2403 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2388 log.exception(msg)
2404 log.exception(msg)
2389 raise JSONRPCError(msg)
2405 raise JSONRPCError(msg)
2390
2406
2391 return settings
2407 return settings
2392
2408
2393
2409
2394 @jsonrpc_method()
2410 @jsonrpc_method()
2395 def set_repo_settings(request, apiuser, repoid, settings):
2411 def set_repo_settings(request, apiuser, repoid, settings):
2396 """
2412 """
2397 Update repository settings. Returns true on success.
2413 Update repository settings. Returns true on success.
2398
2414
2399 :param apiuser: This is filled automatically from the |authtoken|.
2415 :param apiuser: This is filled automatically from the |authtoken|.
2400 :type apiuser: AuthUser
2416 :type apiuser: AuthUser
2401 :param repoid: The repository name or repository id.
2417 :param repoid: The repository name or repository id.
2402 :type repoid: str or int
2418 :type repoid: str or int
2403 :param settings: The new settings for the repository.
2419 :param settings: The new settings for the repository.
2404 :type: settings: dict
2420 :type: settings: dict
2405
2421
2406 Example output:
2422 Example output:
2407
2423
2408 .. code-block:: bash
2424 .. code-block:: bash
2409
2425
2410 {
2426 {
2411 "error": null,
2427 "error": null,
2412 "id": 237,
2428 "id": 237,
2413 "result": true
2429 "result": true
2414 }
2430 }
2415 """
2431 """
2416 # Restrict access to this api method to super-admins, and repo admins only.
2432 # Restrict access to this api method to super-admins, and repo admins only.
2417 repo = get_repo_or_error(repoid)
2433 repo = get_repo_or_error(repoid)
2418 if not has_superadmin_permission(apiuser):
2434 if not has_superadmin_permission(apiuser):
2419 _perms = ('repository.admin',)
2435 _perms = ('repository.admin',)
2420 validate_repo_permissions(apiuser, repoid, repo, _perms)
2436 validate_repo_permissions(apiuser, repoid, repo, _perms)
2421
2437
2422 if type(settings) is not dict:
2438 if type(settings) is not dict:
2423 raise JSONRPCError('Settings have to be a JSON Object.')
2439 raise JSONRPCError('Settings have to be a JSON Object.')
2424
2440
2425 try:
2441 try:
2426 settings_model = VcsSettingsModel(repo=repoid)
2442 settings_model = VcsSettingsModel(repo=repoid)
2427
2443
2428 # Merge global, repo and incoming settings.
2444 # Merge global, repo and incoming settings.
2429 new_settings = settings_model.get_global_settings()
2445 new_settings = settings_model.get_global_settings()
2430 new_settings.update(settings_model.get_repo_settings())
2446 new_settings.update(settings_model.get_repo_settings())
2431 new_settings.update(settings)
2447 new_settings.update(settings)
2432
2448
2433 # Update the settings.
2449 # Update the settings.
2434 inherit_global_settings = new_settings.get(
2450 inherit_global_settings = new_settings.get(
2435 'inherit_global_settings', False)
2451 'inherit_global_settings', False)
2436 settings_model.create_or_update_repo_settings(
2452 settings_model.create_or_update_repo_settings(
2437 new_settings, inherit_global_settings=inherit_global_settings)
2453 new_settings, inherit_global_settings=inherit_global_settings)
2438 Session().commit()
2454 Session().commit()
2439 except Exception:
2455 except Exception:
2440 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2456 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2441 log.exception(msg)
2457 log.exception(msg)
2442 raise JSONRPCError(msg)
2458 raise JSONRPCError(msg)
2443
2459
2444 # Indicate success.
2460 # Indicate success.
2445 return True
2461 return True
2446
2462
2447
2463
2448 @jsonrpc_method()
2464 @jsonrpc_method()
2449 def maintenance(request, apiuser, repoid):
2465 def maintenance(request, apiuser, repoid):
2450 """
2466 """
2451 Triggers a maintenance on the given repository.
2467 Triggers a maintenance on the given repository.
2452
2468
2453 This command can only be run using an |authtoken| with admin
2469 This command can only be run using an |authtoken| with admin
2454 rights to the specified repository. For more information,
2470 rights to the specified repository. For more information,
2455 see :ref:`config-token-ref`.
2471 see :ref:`config-token-ref`.
2456
2472
2457 This command takes the following options:
2473 This command takes the following options:
2458
2474
2459 :param apiuser: This is filled automatically from the |authtoken|.
2475 :param apiuser: This is filled automatically from the |authtoken|.
2460 :type apiuser: AuthUser
2476 :type apiuser: AuthUser
2461 :param repoid: The repository name or repository ID.
2477 :param repoid: The repository name or repository ID.
2462 :type repoid: str or int
2478 :type repoid: str or int
2463
2479
2464 Example output:
2480 Example output:
2465
2481
2466 .. code-block:: bash
2482 .. code-block:: bash
2467
2483
2468 id : <id_given_in_input>
2484 id : <id_given_in_input>
2469 result : {
2485 result : {
2470 "msg": "executed maintenance command",
2486 "msg": "executed maintenance command",
2471 "executed_actions": [
2487 "executed_actions": [
2472 <action_message>, <action_message2>...
2488 <action_message>, <action_message2>...
2473 ],
2489 ],
2474 "repository": "<repository name>"
2490 "repository": "<repository name>"
2475 }
2491 }
2476 error : null
2492 error : null
2477
2493
2478 Example error output:
2494 Example error output:
2479
2495
2480 .. code-block:: bash
2496 .. code-block:: bash
2481
2497
2482 id : <id_given_in_input>
2498 id : <id_given_in_input>
2483 result : null
2499 result : null
2484 error : {
2500 error : {
2485 "Unable to execute maintenance on `<reponame>`"
2501 "Unable to execute maintenance on `<reponame>`"
2486 }
2502 }
2487
2503
2488 """
2504 """
2489
2505
2490 repo = get_repo_or_error(repoid)
2506 repo = get_repo_or_error(repoid)
2491 if not has_superadmin_permission(apiuser):
2507 if not has_superadmin_permission(apiuser):
2492 _perms = ('repository.admin',)
2508 _perms = ('repository.admin',)
2493 validate_repo_permissions(apiuser, repoid, repo, _perms)
2509 validate_repo_permissions(apiuser, repoid, repo, _perms)
2494
2510
2495 try:
2511 try:
2496 maintenance = repo_maintenance.RepoMaintenance()
2512 maintenance = repo_maintenance.RepoMaintenance()
2497 executed_actions = maintenance.execute(repo)
2513 executed_actions = maintenance.execute(repo)
2498
2514
2499 return {
2515 return {
2500 'msg': 'executed maintenance command',
2516 'msg': 'executed maintenance command',
2501 'executed_actions': executed_actions,
2517 'executed_actions': executed_actions,
2502 'repository': repo.repo_name
2518 'repository': repo.repo_name
2503 }
2519 }
2504 except Exception:
2520 except Exception:
2505 log.exception("Exception occurred while trying to run maintenance")
2521 log.exception("Exception occurred while trying to run maintenance")
2506 raise JSONRPCError(
2522 raise JSONRPCError(
2507 'Unable to execute maintenance on `%s`' % repo.repo_name)
2523 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,784 +1,792 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks, channelstream
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 single_commit = len(c.commit_ranges) == 1
119
119
120 c.changes = OrderedDict()
120 c.changes = OrderedDict()
121 c.lines_added = 0
121 c.lines_added = 0
122 c.lines_deleted = 0
122 c.lines_deleted = 0
123
123
124 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127
127
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
129 c.inline_comments = []
129 c.inline_comments = []
130 c.files = []
130 c.files = []
131
131
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135
135
136 # Single commit
136 # Single commit
137 if single_commit:
137 if single_commit:
138 commit = c.commit_ranges[0]
138 commit = c.commit_ranges[0]
139 c.comments = CommentsModel().get_comments(
139 c.comments = CommentsModel().get_comments(
140 self.db_repo.repo_id,
140 self.db_repo.repo_id,
141 revision=commit.raw_id)
141 revision=commit.raw_id)
142
142
143 # comments from PR
143 # comments from PR
144 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
145 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
146 with_revisions=True)
146 with_revisions=True)
147
147
148 prs = set()
148 prs = set()
149 reviewers = list()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
151 for c_status in statuses:
152
152
153 # extract associated pull-requests from votes
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
155 prs.add(c_status.pull_request)
156
156
157 # extract reviewers
157 # extract reviewers
158 _user_id = c_status.author.user_id
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
160 reviewers.append(
161 StrictAttributeDict({
161 StrictAttributeDict({
162 'user': c_status.author,
162 'user': c_status.author,
163
163
164 # fake attributed for commit, page that we don't have
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
165 # but we share the display with PR page
166 'mandatory': False,
166 'mandatory': False,
167 'reasons': [],
167 'reasons': [],
168 'rule_user_group_data': lambda: None
168 'rule_user_group_data': lambda: None
169 })
169 })
170 )
170 )
171 reviewers_duplicates.add(_user_id)
171 reviewers_duplicates.add(_user_id)
172
172
173 c.allowed_reviewers = reviewers
173 c.allowed_reviewers = reviewers
174 c.reviewers_count = len(reviewers)
174 c.reviewers_count = len(reviewers)
175 c.observers_count = 0
175 c.observers_count = 0
176
176
177 # from associated statuses, check the pull requests, and
177 # from associated statuses, check the pull requests, and
178 # show comments from them
178 # show comments from them
179 for pr in prs:
179 for pr in prs:
180 c.comments.extend(pr.comments)
180 c.comments.extend(pr.comments)
181
181
182 c.unresolved_comments = CommentsModel()\
182 c.unresolved_comments = CommentsModel()\
183 .get_commit_unresolved_todos(commit.raw_id)
183 .get_commit_unresolved_todos(commit.raw_id)
184 c.resolved_comments = CommentsModel()\
184 c.resolved_comments = CommentsModel()\
185 .get_commit_resolved_todos(commit.raw_id)
185 .get_commit_resolved_todos(commit.raw_id)
186
186
187 c.inline_comments_flat = CommentsModel()\
187 c.inline_comments_flat = CommentsModel()\
188 .get_commit_inline_comments(commit.raw_id)
188 .get_commit_inline_comments(commit.raw_id)
189
189
190 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
190 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
191 statuses, reviewers)
191 statuses, reviewers)
192
192
193 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
193 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
194
194
195 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
195 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
196
196
197 for review_obj, member, reasons, mandatory, status in review_statuses:
197 for review_obj, member, reasons, mandatory, status in review_statuses:
198 member_reviewer = h.reviewer_as_json(
198 member_reviewer = h.reviewer_as_json(
199 member, reasons=reasons, mandatory=mandatory, role=None,
199 member, reasons=reasons, mandatory=mandatory, role=None,
200 user_group=None
200 user_group=None
201 )
201 )
202
202
203 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
203 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
204 member_reviewer['review_status'] = current_review_status
204 member_reviewer['review_status'] = current_review_status
205 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
205 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
206 member_reviewer['allowed_to_update'] = False
206 member_reviewer['allowed_to_update'] = False
207 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
207 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
208
208
209 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
209 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
210
210
211 # NOTE(marcink): this uses the same voting logic as in pull-requests
211 # NOTE(marcink): this uses the same voting logic as in pull-requests
212 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
212 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
213 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
213 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
214 c.repo_name,
215 commit.raw_id
216 )
217
214
218 diff = None
215 diff = None
219 # Iterate over ranges (default commit view is always one commit)
216 # Iterate over ranges (default commit view is always one commit)
220 for commit in c.commit_ranges:
217 for commit in c.commit_ranges:
221 c.changes[commit.raw_id] = []
218 c.changes[commit.raw_id] = []
222
219
223 commit2 = commit
220 commit2 = commit
224 commit1 = commit.first_parent
221 commit1 = commit.first_parent
225
222
226 if method == 'show':
223 if method == 'show':
227 inline_comments = CommentsModel().get_inline_comments(
224 inline_comments = CommentsModel().get_inline_comments(
228 self.db_repo.repo_id, revision=commit.raw_id)
225 self.db_repo.repo_id, revision=commit.raw_id)
229 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
226 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
230 inline_comments))
227 inline_comments))
231 c.inline_comments = inline_comments
228 c.inline_comments = inline_comments
232
229
233 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
234 self.db_repo)
231 self.db_repo)
235 cache_file_path = diff_cache_exist(
232 cache_file_path = diff_cache_exist(
236 cache_path, 'diff', commit.raw_id,
233 cache_path, 'diff', commit.raw_id,
237 hide_whitespace_changes, diff_context, c.fulldiff)
234 hide_whitespace_changes, diff_context, c.fulldiff)
238
235
239 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
236 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
240 force_recache = str2bool(self.request.GET.get('force_recache'))
237 force_recache = str2bool(self.request.GET.get('force_recache'))
241
238
242 cached_diff = None
239 cached_diff = None
243 if caching_enabled:
240 if caching_enabled:
244 cached_diff = load_cached_diff(cache_file_path)
241 cached_diff = load_cached_diff(cache_file_path)
245
242
246 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
243 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
247 if not force_recache and has_proper_diff_cache:
244 if not force_recache and has_proper_diff_cache:
248 diffset = cached_diff['diff']
245 diffset = cached_diff['diff']
249 else:
246 else:
250 vcs_diff = self.rhodecode_vcs_repo.get_diff(
247 vcs_diff = self.rhodecode_vcs_repo.get_diff(
251 commit1, commit2,
248 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
249 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
250 context=diff_context)
254
251
255 diff_processor = diffs.DiffProcessor(
252 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=c.fulldiff)
254 file_limit=file_limit, show_full_diff=c.fulldiff)
258
255
259 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
260
257
261 diffset = codeblocks.DiffSet(
258 diffset = codeblocks.DiffSet(
262 repo_name=self.db_repo_name,
259 repo_name=self.db_repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
262
266 diffset = self.path_filter.render_patchset_filtered(
263 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
265
269 # save cached diff
266 # save cached diff
270 if caching_enabled:
267 if caching_enabled:
271 cache_diff(cache_file_path, diffset, None)
268 cache_diff(cache_file_path, diffset, None)
272
269
273 c.limited_diff = diffset.limited_diff
270 c.limited_diff = diffset.limited_diff
274 c.changes[commit.raw_id] = diffset
271 c.changes[commit.raw_id] = diffset
275 else:
272 else:
276 # TODO(marcink): no cache usage here...
273 # TODO(marcink): no cache usage here...
277 _diff = self.rhodecode_vcs_repo.get_diff(
274 _diff = self.rhodecode_vcs_repo.get_diff(
278 commit1, commit2,
275 commit1, commit2,
279 ignore_whitespace=hide_whitespace_changes, context=diff_context)
276 ignore_whitespace=hide_whitespace_changes, context=diff_context)
280 diff_processor = diffs.DiffProcessor(
277 diff_processor = diffs.DiffProcessor(
281 _diff, format='newdiff', diff_limit=diff_limit,
278 _diff, format='newdiff', diff_limit=diff_limit,
282 file_limit=file_limit, show_full_diff=c.fulldiff)
279 file_limit=file_limit, show_full_diff=c.fulldiff)
283 # downloads/raw we only need RAW diff nothing else
280 # downloads/raw we only need RAW diff nothing else
284 diff = self.path_filter.get_raw_patch(diff_processor)
281 diff = self.path_filter.get_raw_patch(diff_processor)
285 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
286
283
287 # sort comments by how they were generated
284 # sort comments by how they were generated
288 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
289 c.at_version_num = None
286 c.at_version_num = None
290
287
291 if len(c.commit_ranges) == 1:
288 if len(c.commit_ranges) == 1:
292 c.commit = c.commit_ranges[0]
289 c.commit = c.commit_ranges[0]
293 c.parent_tmpl = ''.join(
290 c.parent_tmpl = ''.join(
294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
295
292
296 if method == 'download':
293 if method == 'download':
297 response = Response(diff)
294 response = Response(diff)
298 response.content_type = 'text/plain'
295 response.content_type = 'text/plain'
299 response.content_disposition = (
296 response.content_disposition = (
300 'attachment; filename=%s.diff' % commit_id_range[:12])
297 'attachment; filename=%s.diff' % commit_id_range[:12])
301 return response
298 return response
302 elif method == 'patch':
299 elif method == 'patch':
303 c.diff = safe_unicode(diff)
300 c.diff = safe_unicode(diff)
304 patch = render(
301 patch = render(
305 'rhodecode:templates/changeset/patch_changeset.mako',
302 'rhodecode:templates/changeset/patch_changeset.mako',
306 self._get_template_context(c), self.request)
303 self._get_template_context(c), self.request)
307 response = Response(patch)
304 response = Response(patch)
308 response.content_type = 'text/plain'
305 response.content_type = 'text/plain'
309 return response
306 return response
310 elif method == 'raw':
307 elif method == 'raw':
311 response = Response(diff)
308 response = Response(diff)
312 response.content_type = 'text/plain'
309 response.content_type = 'text/plain'
313 return response
310 return response
314 elif method == 'show':
311 elif method == 'show':
315 if len(c.commit_ranges) == 1:
312 if len(c.commit_ranges) == 1:
316 html = render(
313 html = render(
317 'rhodecode:templates/changeset/changeset.mako',
314 'rhodecode:templates/changeset/changeset.mako',
318 self._get_template_context(c), self.request)
315 self._get_template_context(c), self.request)
319 return Response(html)
316 return Response(html)
320 else:
317 else:
321 c.ancestor = None
318 c.ancestor = None
322 c.target_repo = self.db_repo
319 c.target_repo = self.db_repo
323 html = render(
320 html = render(
324 'rhodecode:templates/changeset/changeset_range.mako',
321 'rhodecode:templates/changeset/changeset_range.mako',
325 self._get_template_context(c), self.request)
322 self._get_template_context(c), self.request)
326 return Response(html)
323 return Response(html)
327
324
328 raise HTTPBadRequest()
325 raise HTTPBadRequest()
329
326
330 @LoginRequired()
327 @LoginRequired()
331 @HasRepoPermissionAnyDecorator(
328 @HasRepoPermissionAnyDecorator(
332 'repository.read', 'repository.write', 'repository.admin')
329 'repository.read', 'repository.write', 'repository.admin')
333 @view_config(
330 @view_config(
334 route_name='repo_commit', request_method='GET',
331 route_name='repo_commit', request_method='GET',
335 renderer=None)
332 renderer=None)
336 def repo_commit_show(self):
333 def repo_commit_show(self):
337 commit_id = self.request.matchdict['commit_id']
334 commit_id = self.request.matchdict['commit_id']
338 return self._commit(commit_id, method='show')
335 return self._commit(commit_id, method='show')
339
336
340 @LoginRequired()
337 @LoginRequired()
341 @HasRepoPermissionAnyDecorator(
338 @HasRepoPermissionAnyDecorator(
342 'repository.read', 'repository.write', 'repository.admin')
339 'repository.read', 'repository.write', 'repository.admin')
343 @view_config(
340 @view_config(
344 route_name='repo_commit_raw', request_method='GET',
341 route_name='repo_commit_raw', request_method='GET',
345 renderer=None)
342 renderer=None)
346 @view_config(
343 @view_config(
347 route_name='repo_commit_raw_deprecated', request_method='GET',
344 route_name='repo_commit_raw_deprecated', request_method='GET',
348 renderer=None)
345 renderer=None)
349 def repo_commit_raw(self):
346 def repo_commit_raw(self):
350 commit_id = self.request.matchdict['commit_id']
347 commit_id = self.request.matchdict['commit_id']
351 return self._commit(commit_id, method='raw')
348 return self._commit(commit_id, method='raw')
352
349
353 @LoginRequired()
350 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
351 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
352 'repository.read', 'repository.write', 'repository.admin')
356 @view_config(
353 @view_config(
357 route_name='repo_commit_patch', request_method='GET',
354 route_name='repo_commit_patch', request_method='GET',
358 renderer=None)
355 renderer=None)
359 def repo_commit_patch(self):
356 def repo_commit_patch(self):
360 commit_id = self.request.matchdict['commit_id']
357 commit_id = self.request.matchdict['commit_id']
361 return self._commit(commit_id, method='patch')
358 return self._commit(commit_id, method='patch')
362
359
363 @LoginRequired()
360 @LoginRequired()
364 @HasRepoPermissionAnyDecorator(
361 @HasRepoPermissionAnyDecorator(
365 'repository.read', 'repository.write', 'repository.admin')
362 'repository.read', 'repository.write', 'repository.admin')
366 @view_config(
363 @view_config(
367 route_name='repo_commit_download', request_method='GET',
364 route_name='repo_commit_download', request_method='GET',
368 renderer=None)
365 renderer=None)
369 def repo_commit_download(self):
366 def repo_commit_download(self):
370 commit_id = self.request.matchdict['commit_id']
367 commit_id = self.request.matchdict['commit_id']
371 return self._commit(commit_id, method='download')
368 return self._commit(commit_id, method='download')
372
369
373 @LoginRequired()
370 @LoginRequired()
374 @NotAnonymous()
371 @NotAnonymous()
375 @HasRepoPermissionAnyDecorator(
372 @HasRepoPermissionAnyDecorator(
376 'repository.read', 'repository.write', 'repository.admin')
373 'repository.read', 'repository.write', 'repository.admin')
377 @CSRFRequired()
374 @CSRFRequired()
378 @view_config(
375 @view_config(
379 route_name='repo_commit_comment_create', request_method='POST',
376 route_name='repo_commit_comment_create', request_method='POST',
380 renderer='json_ext')
377 renderer='json_ext')
381 def repo_commit_comment_create(self):
378 def repo_commit_comment_create(self):
382 _ = self.request.translate
379 _ = self.request.translate
383 commit_id = self.request.matchdict['commit_id']
380 commit_id = self.request.matchdict['commit_id']
384
381
385 c = self.load_default_context()
382 c = self.load_default_context()
386 status = self.request.POST.get('changeset_status', None)
383 status = self.request.POST.get('changeset_status', None)
387 text = self.request.POST.get('text')
384 text = self.request.POST.get('text')
388 comment_type = self.request.POST.get('comment_type')
385 comment_type = self.request.POST.get('comment_type')
389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
390
387
391 if status:
388 if status:
392 text = text or (_('Status change %(transition_icon)s %(status)s')
389 text = text or (_('Status change %(transition_icon)s %(status)s')
393 % {'transition_icon': '>',
390 % {'transition_icon': '>',
394 'status': ChangesetStatus.get_status_lbl(status)})
391 'status': ChangesetStatus.get_status_lbl(status)})
395
392
396 multi_commit_ids = []
393 multi_commit_ids = []
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
395 if _commit_id not in ['', None, EmptyCommit.raw_id]:
399 if _commit_id not in multi_commit_ids:
396 if _commit_id not in multi_commit_ids:
400 multi_commit_ids.append(_commit_id)
397 multi_commit_ids.append(_commit_id)
401
398
402 commit_ids = multi_commit_ids or [commit_id]
399 commit_ids = multi_commit_ids or [commit_id]
403
400
404 comment = None
401 comment = None
405 for current_id in filter(None, commit_ids):
402 for current_id in filter(None, commit_ids):
406 comment = CommentsModel().create(
403 comment = CommentsModel().create(
407 text=text,
404 text=text,
408 repo=self.db_repo.repo_id,
405 repo=self.db_repo.repo_id,
409 user=self._rhodecode_db_user.user_id,
406 user=self._rhodecode_db_user.user_id,
410 commit_id=current_id,
407 commit_id=current_id,
411 f_path=self.request.POST.get('f_path'),
408 f_path=self.request.POST.get('f_path'),
412 line_no=self.request.POST.get('line'),
409 line_no=self.request.POST.get('line'),
413 status_change=(ChangesetStatus.get_status_lbl(status)
410 status_change=(ChangesetStatus.get_status_lbl(status)
414 if status else None),
411 if status else None),
415 status_change_type=status,
412 status_change_type=status,
416 comment_type=comment_type,
413 comment_type=comment_type,
417 resolves_comment_id=resolves_comment_id,
414 resolves_comment_id=resolves_comment_id,
418 auth_user=self._rhodecode_user
415 auth_user=self._rhodecode_user
419 )
416 )
417 is_inline = bool(comment.f_path and comment.line_no)
420
418
421 # get status if set !
419 # get status if set !
422 if status:
420 if status:
423 # if latest status was from pull request and it's closed
421 # if latest status was from pull request and it's closed
424 # disallow changing status !
422 # disallow changing status !
425 # dont_allow_on_closed_pull_request = True !
423 # dont_allow_on_closed_pull_request = True !
426
424
427 try:
425 try:
428 ChangesetStatusModel().set_status(
426 ChangesetStatusModel().set_status(
429 self.db_repo.repo_id,
427 self.db_repo.repo_id,
430 status,
428 status,
431 self._rhodecode_db_user.user_id,
429 self._rhodecode_db_user.user_id,
432 comment,
430 comment,
433 revision=current_id,
431 revision=current_id,
434 dont_allow_on_closed_pull_request=True
432 dont_allow_on_closed_pull_request=True
435 )
433 )
436 except StatusChangeOnClosedPullRequestError:
434 except StatusChangeOnClosedPullRequestError:
437 msg = _('Changing the status of a commit associated with '
435 msg = _('Changing the status of a commit associated with '
438 'a closed pull request is not allowed')
436 'a closed pull request is not allowed')
439 log.exception(msg)
437 log.exception(msg)
440 h.flash(msg, category='warning')
438 h.flash(msg, category='warning')
441 raise HTTPFound(h.route_path(
439 raise HTTPFound(h.route_path(
442 'repo_commit', repo_name=self.db_repo_name,
440 'repo_commit', repo_name=self.db_repo_name,
443 commit_id=current_id))
441 commit_id=current_id))
444
442
445 commit = self.db_repo.get_commit(current_id)
443 commit = self.db_repo.get_commit(current_id)
446 CommentsModel().trigger_commit_comment_hook(
444 CommentsModel().trigger_commit_comment_hook(
447 self.db_repo, self._rhodecode_user, 'create',
445 self.db_repo, self._rhodecode_user, 'create',
448 data={'comment': comment, 'commit': commit})
446 data={'comment': comment, 'commit': commit})
449
447
450 # finalize, commit and redirect
448 # finalize, commit and redirect
451 Session().commit()
449 Session().commit()
452
450
453 data = {
451 data = {
454 'target_id': h.safeid(h.safe_unicode(
452 'target_id': h.safeid(h.safe_unicode(
455 self.request.POST.get('f_path'))),
453 self.request.POST.get('f_path'))),
456 }
454 }
457 if comment:
455 if comment:
458 c.co = comment
456 c.co = comment
459 c.at_version_num = 0
457 c.at_version_num = 0
460 rendered_comment = render(
458 rendered_comment = render(
461 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 'rhodecode:templates/changeset/changeset_comment_block.mako',
462 self._get_template_context(c), self.request)
460 self._get_template_context(c), self.request)
463
461
464 data.update(comment.get_dict())
462 data.update(comment.get_dict())
465 data.update({'rendered_text': rendered_comment})
463 data.update({'rendered_text': rendered_comment})
466
464
465 comment_broadcast_channel = channelstream.comment_channel(
466 self.db_repo_name, commit_obj=commit)
467
468 comment_data = data
469 comment_type = 'inline' if is_inline else 'general'
470 channelstream.comment_channelstream_push(
471 self.request, comment_broadcast_channel, self._rhodecode_user,
472 _('posted a new {} comment').format(comment_type),
473 comment_data=comment_data)
474
467 return data
475 return data
468
476
469 @LoginRequired()
477 @LoginRequired()
470 @NotAnonymous()
478 @NotAnonymous()
471 @HasRepoPermissionAnyDecorator(
479 @HasRepoPermissionAnyDecorator(
472 'repository.read', 'repository.write', 'repository.admin')
480 'repository.read', 'repository.write', 'repository.admin')
473 @CSRFRequired()
481 @CSRFRequired()
474 @view_config(
482 @view_config(
475 route_name='repo_commit_comment_preview', request_method='POST',
483 route_name='repo_commit_comment_preview', request_method='POST',
476 renderer='string', xhr=True)
484 renderer='string', xhr=True)
477 def repo_commit_comment_preview(self):
485 def repo_commit_comment_preview(self):
478 # Technically a CSRF token is not needed as no state changes with this
486 # Technically a CSRF token is not needed as no state changes with this
479 # call. However, as this is a POST is better to have it, so automated
487 # call. However, as this is a POST is better to have it, so automated
480 # tools don't flag it as potential CSRF.
488 # tools don't flag it as potential CSRF.
481 # Post is required because the payload could be bigger than the maximum
489 # Post is required because the payload could be bigger than the maximum
482 # allowed by GET.
490 # allowed by GET.
483
491
484 text = self.request.POST.get('text')
492 text = self.request.POST.get('text')
485 renderer = self.request.POST.get('renderer') or 'rst'
493 renderer = self.request.POST.get('renderer') or 'rst'
486 if text:
494 if text:
487 return h.render(text, renderer=renderer, mentions=True,
495 return h.render(text, renderer=renderer, mentions=True,
488 repo_name=self.db_repo_name)
496 repo_name=self.db_repo_name)
489 return ''
497 return ''
490
498
491 @LoginRequired()
499 @LoginRequired()
492 @HasRepoPermissionAnyDecorator(
500 @HasRepoPermissionAnyDecorator(
493 'repository.read', 'repository.write', 'repository.admin')
501 'repository.read', 'repository.write', 'repository.admin')
494 @CSRFRequired()
502 @CSRFRequired()
495 @view_config(
503 @view_config(
496 route_name='repo_commit_comment_history_view', request_method='POST',
504 route_name='repo_commit_comment_history_view', request_method='POST',
497 renderer='string', xhr=True)
505 renderer='string', xhr=True)
498 def repo_commit_comment_history_view(self):
506 def repo_commit_comment_history_view(self):
499 c = self.load_default_context()
507 c = self.load_default_context()
500
508
501 comment_history_id = self.request.matchdict['comment_history_id']
509 comment_history_id = self.request.matchdict['comment_history_id']
502 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
510 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
503 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
511 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
504
512
505 if is_repo_comment:
513 if is_repo_comment:
506 c.comment_history = comment_history
514 c.comment_history = comment_history
507
515
508 rendered_comment = render(
516 rendered_comment = render(
509 'rhodecode:templates/changeset/comment_history.mako',
517 'rhodecode:templates/changeset/comment_history.mako',
510 self._get_template_context(c)
518 self._get_template_context(c)
511 , self.request)
519 , self.request)
512 return rendered_comment
520 return rendered_comment
513 else:
521 else:
514 log.warning('No permissions for user %s to show comment_history_id: %s',
522 log.warning('No permissions for user %s to show comment_history_id: %s',
515 self._rhodecode_db_user, comment_history_id)
523 self._rhodecode_db_user, comment_history_id)
516 raise HTTPNotFound()
524 raise HTTPNotFound()
517
525
518 @LoginRequired()
526 @LoginRequired()
519 @NotAnonymous()
527 @NotAnonymous()
520 @HasRepoPermissionAnyDecorator(
528 @HasRepoPermissionAnyDecorator(
521 'repository.read', 'repository.write', 'repository.admin')
529 'repository.read', 'repository.write', 'repository.admin')
522 @CSRFRequired()
530 @CSRFRequired()
523 @view_config(
531 @view_config(
524 route_name='repo_commit_comment_attachment_upload', request_method='POST',
532 route_name='repo_commit_comment_attachment_upload', request_method='POST',
525 renderer='json_ext', xhr=True)
533 renderer='json_ext', xhr=True)
526 def repo_commit_comment_attachment_upload(self):
534 def repo_commit_comment_attachment_upload(self):
527 c = self.load_default_context()
535 c = self.load_default_context()
528 upload_key = 'attachment'
536 upload_key = 'attachment'
529
537
530 file_obj = self.request.POST.get(upload_key)
538 file_obj = self.request.POST.get(upload_key)
531
539
532 if file_obj is None:
540 if file_obj is None:
533 self.request.response.status = 400
541 self.request.response.status = 400
534 return {'store_fid': None,
542 return {'store_fid': None,
535 'access_path': None,
543 'access_path': None,
536 'error': '{} data field is missing'.format(upload_key)}
544 'error': '{} data field is missing'.format(upload_key)}
537
545
538 if not hasattr(file_obj, 'filename'):
546 if not hasattr(file_obj, 'filename'):
539 self.request.response.status = 400
547 self.request.response.status = 400
540 return {'store_fid': None,
548 return {'store_fid': None,
541 'access_path': None,
549 'access_path': None,
542 'error': 'filename cannot be read from the data field'}
550 'error': 'filename cannot be read from the data field'}
543
551
544 filename = file_obj.filename
552 filename = file_obj.filename
545 file_display_name = filename
553 file_display_name = filename
546
554
547 metadata = {
555 metadata = {
548 'user_uploaded': {'username': self._rhodecode_user.username,
556 'user_uploaded': {'username': self._rhodecode_user.username,
549 'user_id': self._rhodecode_user.user_id,
557 'user_id': self._rhodecode_user.user_id,
550 'ip': self._rhodecode_user.ip_addr}}
558 'ip': self._rhodecode_user.ip_addr}}
551
559
552 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
560 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
553 allowed_extensions = [
561 allowed_extensions = [
554 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
562 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
555 '.pptx', '.txt', '.xlsx', '.zip']
563 '.pptx', '.txt', '.xlsx', '.zip']
556 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
564 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
557
565
558 try:
566 try:
559 storage = store_utils.get_file_storage(self.request.registry.settings)
567 storage = store_utils.get_file_storage(self.request.registry.settings)
560 store_uid, metadata = storage.save_file(
568 store_uid, metadata = storage.save_file(
561 file_obj.file, filename, extra_metadata=metadata,
569 file_obj.file, filename, extra_metadata=metadata,
562 extensions=allowed_extensions, max_filesize=max_file_size)
570 extensions=allowed_extensions, max_filesize=max_file_size)
563 except FileNotAllowedException:
571 except FileNotAllowedException:
564 self.request.response.status = 400
572 self.request.response.status = 400
565 permitted_extensions = ', '.join(allowed_extensions)
573 permitted_extensions = ', '.join(allowed_extensions)
566 error_msg = 'File `{}` is not allowed. ' \
574 error_msg = 'File `{}` is not allowed. ' \
567 'Only following extensions are permitted: {}'.format(
575 'Only following extensions are permitted: {}'.format(
568 filename, permitted_extensions)
576 filename, permitted_extensions)
569 return {'store_fid': None,
577 return {'store_fid': None,
570 'access_path': None,
578 'access_path': None,
571 'error': error_msg}
579 'error': error_msg}
572 except FileOverSizeException:
580 except FileOverSizeException:
573 self.request.response.status = 400
581 self.request.response.status = 400
574 limit_mb = h.format_byte_size_binary(max_file_size)
582 limit_mb = h.format_byte_size_binary(max_file_size)
575 return {'store_fid': None,
583 return {'store_fid': None,
576 'access_path': None,
584 'access_path': None,
577 'error': 'File {} is exceeding allowed limit of {}.'.format(
585 'error': 'File {} is exceeding allowed limit of {}.'.format(
578 filename, limit_mb)}
586 filename, limit_mb)}
579
587
580 try:
588 try:
581 entry = FileStore.create(
589 entry = FileStore.create(
582 file_uid=store_uid, filename=metadata["filename"],
590 file_uid=store_uid, filename=metadata["filename"],
583 file_hash=metadata["sha256"], file_size=metadata["size"],
591 file_hash=metadata["sha256"], file_size=metadata["size"],
584 file_display_name=file_display_name,
592 file_display_name=file_display_name,
585 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
593 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
586 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
594 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
587 scope_repo_id=self.db_repo.repo_id
595 scope_repo_id=self.db_repo.repo_id
588 )
596 )
589 Session().add(entry)
597 Session().add(entry)
590 Session().commit()
598 Session().commit()
591 log.debug('Stored upload in DB as %s', entry)
599 log.debug('Stored upload in DB as %s', entry)
592 except Exception:
600 except Exception:
593 log.exception('Failed to store file %s', filename)
601 log.exception('Failed to store file %s', filename)
594 self.request.response.status = 400
602 self.request.response.status = 400
595 return {'store_fid': None,
603 return {'store_fid': None,
596 'access_path': None,
604 'access_path': None,
597 'error': 'File {} failed to store in DB.'.format(filename)}
605 'error': 'File {} failed to store in DB.'.format(filename)}
598
606
599 Session().commit()
607 Session().commit()
600
608
601 return {
609 return {
602 'store_fid': store_uid,
610 'store_fid': store_uid,
603 'access_path': h.route_path(
611 'access_path': h.route_path(
604 'download_file', fid=store_uid),
612 'download_file', fid=store_uid),
605 'fqn_access_path': h.route_url(
613 'fqn_access_path': h.route_url(
606 'download_file', fid=store_uid),
614 'download_file', fid=store_uid),
607 'repo_access_path': h.route_path(
615 'repo_access_path': h.route_path(
608 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
616 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
609 'repo_fqn_access_path': h.route_url(
617 'repo_fqn_access_path': h.route_url(
610 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
618 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
611 }
619 }
612
620
613 @LoginRequired()
621 @LoginRequired()
614 @NotAnonymous()
622 @NotAnonymous()
615 @HasRepoPermissionAnyDecorator(
623 @HasRepoPermissionAnyDecorator(
616 'repository.read', 'repository.write', 'repository.admin')
624 'repository.read', 'repository.write', 'repository.admin')
617 @CSRFRequired()
625 @CSRFRequired()
618 @view_config(
626 @view_config(
619 route_name='repo_commit_comment_delete', request_method='POST',
627 route_name='repo_commit_comment_delete', request_method='POST',
620 renderer='json_ext')
628 renderer='json_ext')
621 def repo_commit_comment_delete(self):
629 def repo_commit_comment_delete(self):
622 commit_id = self.request.matchdict['commit_id']
630 commit_id = self.request.matchdict['commit_id']
623 comment_id = self.request.matchdict['comment_id']
631 comment_id = self.request.matchdict['comment_id']
624
632
625 comment = ChangesetComment.get_or_404(comment_id)
633 comment = ChangesetComment.get_or_404(comment_id)
626 if not comment:
634 if not comment:
627 log.debug('Comment with id:%s not found, skipping', comment_id)
635 log.debug('Comment with id:%s not found, skipping', comment_id)
628 # comment already deleted in another call probably
636 # comment already deleted in another call probably
629 return True
637 return True
630
638
631 if comment.immutable:
639 if comment.immutable:
632 # don't allow deleting comments that are immutable
640 # don't allow deleting comments that are immutable
633 raise HTTPForbidden()
641 raise HTTPForbidden()
634
642
635 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
643 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
636 super_admin = h.HasPermissionAny('hg.admin')()
644 super_admin = h.HasPermissionAny('hg.admin')()
637 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
645 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
638 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
646 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
639 comment_repo_admin = is_repo_admin and is_repo_comment
647 comment_repo_admin = is_repo_admin and is_repo_comment
640
648
641 if super_admin or comment_owner or comment_repo_admin:
649 if super_admin or comment_owner or comment_repo_admin:
642 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
650 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
643 Session().commit()
651 Session().commit()
644 return True
652 return True
645 else:
653 else:
646 log.warning('No permissions for user %s to delete comment_id: %s',
654 log.warning('No permissions for user %s to delete comment_id: %s',
647 self._rhodecode_db_user, comment_id)
655 self._rhodecode_db_user, comment_id)
648 raise HTTPNotFound()
656 raise HTTPNotFound()
649
657
650 @LoginRequired()
658 @LoginRequired()
651 @NotAnonymous()
659 @NotAnonymous()
652 @HasRepoPermissionAnyDecorator(
660 @HasRepoPermissionAnyDecorator(
653 'repository.read', 'repository.write', 'repository.admin')
661 'repository.read', 'repository.write', 'repository.admin')
654 @CSRFRequired()
662 @CSRFRequired()
655 @view_config(
663 @view_config(
656 route_name='repo_commit_comment_edit', request_method='POST',
664 route_name='repo_commit_comment_edit', request_method='POST',
657 renderer='json_ext')
665 renderer='json_ext')
658 def repo_commit_comment_edit(self):
666 def repo_commit_comment_edit(self):
659 self.load_default_context()
667 self.load_default_context()
660
668
661 comment_id = self.request.matchdict['comment_id']
669 comment_id = self.request.matchdict['comment_id']
662 comment = ChangesetComment.get_or_404(comment_id)
670 comment = ChangesetComment.get_or_404(comment_id)
663
671
664 if comment.immutable:
672 if comment.immutable:
665 # don't allow deleting comments that are immutable
673 # don't allow deleting comments that are immutable
666 raise HTTPForbidden()
674 raise HTTPForbidden()
667
675
668 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
676 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
669 super_admin = h.HasPermissionAny('hg.admin')()
677 super_admin = h.HasPermissionAny('hg.admin')()
670 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
678 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
671 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
679 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
672 comment_repo_admin = is_repo_admin and is_repo_comment
680 comment_repo_admin = is_repo_admin and is_repo_comment
673
681
674 if super_admin or comment_owner or comment_repo_admin:
682 if super_admin or comment_owner or comment_repo_admin:
675 text = self.request.POST.get('text')
683 text = self.request.POST.get('text')
676 version = self.request.POST.get('version')
684 version = self.request.POST.get('version')
677 if text == comment.text:
685 if text == comment.text:
678 log.warning(
686 log.warning(
679 'Comment(repo): '
687 'Comment(repo): '
680 'Trying to create new version '
688 'Trying to create new version '
681 'with the same comment body {}'.format(
689 'with the same comment body {}'.format(
682 comment_id,
690 comment_id,
683 )
691 )
684 )
692 )
685 raise HTTPNotFound()
693 raise HTTPNotFound()
686
694
687 if version.isdigit():
695 if version.isdigit():
688 version = int(version)
696 version = int(version)
689 else:
697 else:
690 log.warning(
698 log.warning(
691 'Comment(repo): Wrong version type {} {} '
699 'Comment(repo): Wrong version type {} {} '
692 'for comment {}'.format(
700 'for comment {}'.format(
693 version,
701 version,
694 type(version),
702 type(version),
695 comment_id,
703 comment_id,
696 )
704 )
697 )
705 )
698 raise HTTPNotFound()
706 raise HTTPNotFound()
699
707
700 try:
708 try:
701 comment_history = CommentsModel().edit(
709 comment_history = CommentsModel().edit(
702 comment_id=comment_id,
710 comment_id=comment_id,
703 text=text,
711 text=text,
704 auth_user=self._rhodecode_user,
712 auth_user=self._rhodecode_user,
705 version=version,
713 version=version,
706 )
714 )
707 except CommentVersionMismatch:
715 except CommentVersionMismatch:
708 raise HTTPConflict()
716 raise HTTPConflict()
709
717
710 if not comment_history:
718 if not comment_history:
711 raise HTTPNotFound()
719 raise HTTPNotFound()
712
720
713 commit_id = self.request.matchdict['commit_id']
721 commit_id = self.request.matchdict['commit_id']
714 commit = self.db_repo.get_commit(commit_id)
722 commit = self.db_repo.get_commit(commit_id)
715 CommentsModel().trigger_commit_comment_hook(
723 CommentsModel().trigger_commit_comment_hook(
716 self.db_repo, self._rhodecode_user, 'edit',
724 self.db_repo, self._rhodecode_user, 'edit',
717 data={'comment': comment, 'commit': commit})
725 data={'comment': comment, 'commit': commit})
718
726
719 Session().commit()
727 Session().commit()
720 return {
728 return {
721 'comment_history_id': comment_history.comment_history_id,
729 'comment_history_id': comment_history.comment_history_id,
722 'comment_id': comment.comment_id,
730 'comment_id': comment.comment_id,
723 'comment_version': comment_history.version,
731 'comment_version': comment_history.version,
724 'comment_author_username': comment_history.author.username,
732 'comment_author_username': comment_history.author.username,
725 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
733 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
726 'comment_created_on': h.age_component(comment_history.created_on,
734 'comment_created_on': h.age_component(comment_history.created_on,
727 time_is_local=True),
735 time_is_local=True),
728 }
736 }
729 else:
737 else:
730 log.warning('No permissions for user %s to edit comment_id: %s',
738 log.warning('No permissions for user %s to edit comment_id: %s',
731 self._rhodecode_db_user, comment_id)
739 self._rhodecode_db_user, comment_id)
732 raise HTTPNotFound()
740 raise HTTPNotFound()
733
741
734 @LoginRequired()
742 @LoginRequired()
735 @HasRepoPermissionAnyDecorator(
743 @HasRepoPermissionAnyDecorator(
736 'repository.read', 'repository.write', 'repository.admin')
744 'repository.read', 'repository.write', 'repository.admin')
737 @view_config(
745 @view_config(
738 route_name='repo_commit_data', request_method='GET',
746 route_name='repo_commit_data', request_method='GET',
739 renderer='json_ext', xhr=True)
747 renderer='json_ext', xhr=True)
740 def repo_commit_data(self):
748 def repo_commit_data(self):
741 commit_id = self.request.matchdict['commit_id']
749 commit_id = self.request.matchdict['commit_id']
742 self.load_default_context()
750 self.load_default_context()
743
751
744 try:
752 try:
745 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
753 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
746 except CommitDoesNotExistError as e:
754 except CommitDoesNotExistError as e:
747 return EmptyCommit(message=str(e))
755 return EmptyCommit(message=str(e))
748
756
749 @LoginRequired()
757 @LoginRequired()
750 @HasRepoPermissionAnyDecorator(
758 @HasRepoPermissionAnyDecorator(
751 'repository.read', 'repository.write', 'repository.admin')
759 'repository.read', 'repository.write', 'repository.admin')
752 @view_config(
760 @view_config(
753 route_name='repo_commit_children', request_method='GET',
761 route_name='repo_commit_children', request_method='GET',
754 renderer='json_ext', xhr=True)
762 renderer='json_ext', xhr=True)
755 def repo_commit_children(self):
763 def repo_commit_children(self):
756 commit_id = self.request.matchdict['commit_id']
764 commit_id = self.request.matchdict['commit_id']
757 self.load_default_context()
765 self.load_default_context()
758
766
759 try:
767 try:
760 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
768 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
761 children = commit.children
769 children = commit.children
762 except CommitDoesNotExistError:
770 except CommitDoesNotExistError:
763 children = []
771 children = []
764
772
765 result = {"results": children}
773 result = {"results": children}
766 return result
774 return result
767
775
768 @LoginRequired()
776 @LoginRequired()
769 @HasRepoPermissionAnyDecorator(
777 @HasRepoPermissionAnyDecorator(
770 'repository.read', 'repository.write', 'repository.admin')
778 'repository.read', 'repository.write', 'repository.admin')
771 @view_config(
779 @view_config(
772 route_name='repo_commit_parents', request_method='GET',
780 route_name='repo_commit_parents', request_method='GET',
773 renderer='json_ext')
781 renderer='json_ext')
774 def repo_commit_parents(self):
782 def repo_commit_parents(self):
775 commit_id = self.request.matchdict['commit_id']
783 commit_id = self.request.matchdict['commit_id']
776 self.load_default_context()
784 self.load_default_context()
777
785
778 try:
786 try:
779 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
787 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
780 parents = commit.parents
788 parents = commit.parents
781 except CommitDoesNotExistError:
789 except CommitDoesNotExistError:
782 parents = []
790 parents = []
783 result = {"results": parents}
791 result = {"results": parents}
784 return result
792 return result
@@ -1,1794 +1,1806 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, 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 (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 PullRequestReviewers)
50 PullRequestReviewers)
51 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.scm import ScmModel
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60
60
61 def load_default_context(self):
61 def load_default_context(self):
62 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 # backward compat., we use for OLD PRs a plain renderer
65 # backward compat., we use for OLD PRs a plain renderer
66 c.renderer = 'plain'
66 c.renderer = 'plain'
67 return c
67 return c
68
68
69 def _get_pull_requests_list(
69 def _get_pull_requests_list(
70 self, repo_name, source, filter_type, opened_by, statuses):
70 self, repo_name, source, filter_type, opened_by, statuses):
71
71
72 draw, start, limit = self._extract_chunk(self.request)
72 draw, start, limit = self._extract_chunk(self.request)
73 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 _render = self.request.get_partial_renderer(
74 _render = self.request.get_partial_renderer(
75 'rhodecode:templates/data_table/_dt_elements.mako')
75 'rhodecode:templates/data_table/_dt_elements.mako')
76
76
77 # pagination
77 # pagination
78
78
79 if filter_type == 'awaiting_review':
79 if filter_type == 'awaiting_review':
80 pull_requests = PullRequestModel().get_awaiting_review(
80 pull_requests = PullRequestModel().get_awaiting_review(
81 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 statuses=statuses, offset=start, length=limit,
82 statuses=statuses, offset=start, length=limit,
83 order_by=order_by, order_dir=order_dir)
83 order_by=order_by, order_dir=order_dir)
84 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 repo_name, search_q=search_q, source=source, statuses=statuses,
85 repo_name, search_q=search_q, source=source, statuses=statuses,
86 opened_by=opened_by)
86 opened_by=opened_by)
87 elif filter_type == 'awaiting_my_review':
87 elif filter_type == 'awaiting_my_review':
88 pull_requests = PullRequestModel().get_awaiting_my_review(
88 pull_requests = PullRequestModel().get_awaiting_my_review(
89 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 offset=start, length=limit, order_by=order_by,
91 offset=start, length=limit, order_by=order_by,
92 order_dir=order_dir)
92 order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 statuses=statuses, opened_by=opened_by)
95 statuses=statuses, opened_by=opened_by)
96 else:
96 else:
97 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
103 opened_by=opened_by)
104
104
105 data = []
105 data = []
106 comments_model = CommentsModel()
106 comments_model = CommentsModel()
107 for pr in pull_requests:
107 for pr in pull_requests:
108 comments = comments_model.get_all_comments(
108 comments = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr)
109 self.db_repo.repo_id, pull_request=pr)
110
110
111 data.append({
111 data.append({
112 'name': _render('pullrequest_name',
112 'name': _render('pullrequest_name',
113 pr.pull_request_id, pr.pull_request_state,
113 pr.pull_request_id, pr.pull_request_state,
114 pr.work_in_progress, pr.target_repo.repo_name),
114 pr.work_in_progress, pr.target_repo.repo_name),
115 'name_raw': pr.pull_request_id,
115 'name_raw': pr.pull_request_id,
116 'status': _render('pullrequest_status',
116 'status': _render('pullrequest_status',
117 pr.calculated_review_status()),
117 pr.calculated_review_status()),
118 'title': _render('pullrequest_title', pr.title, pr.description),
118 'title': _render('pullrequest_title', pr.title, pr.description),
119 'description': h.escape(pr.description),
119 'description': h.escape(pr.description),
120 'updated_on': _render('pullrequest_updated_on',
120 'updated_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.updated_on)),
121 h.datetime_to_time(pr.updated_on)),
122 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'updated_on_raw': h.datetime_to_time(pr.updated_on),
123 'created_on': _render('pullrequest_updated_on',
123 'created_on': _render('pullrequest_updated_on',
124 h.datetime_to_time(pr.created_on)),
124 h.datetime_to_time(pr.created_on)),
125 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'created_on_raw': h.datetime_to_time(pr.created_on),
126 'state': pr.pull_request_state,
126 'state': pr.pull_request_state,
127 'author': _render('pullrequest_author',
127 'author': _render('pullrequest_author',
128 pr.author.full_contact, ),
128 pr.author.full_contact, ),
129 'author_raw': pr.author.full_name,
129 'author_raw': pr.author.full_name,
130 'comments': _render('pullrequest_comments', len(comments)),
130 'comments': _render('pullrequest_comments', len(comments)),
131 'comments_raw': len(comments),
131 'comments_raw': len(comments),
132 'closed': pr.is_closed(),
132 'closed': pr.is_closed(),
133 })
133 })
134
134
135 data = ({
135 data = ({
136 'draw': draw,
136 'draw': draw,
137 'data': data,
137 'data': data,
138 'recordsTotal': pull_requests_total_count,
138 'recordsTotal': pull_requests_total_count,
139 'recordsFiltered': pull_requests_total_count,
139 'recordsFiltered': pull_requests_total_count,
140 })
140 })
141 return data
141 return data
142
142
143 @LoginRequired()
143 @LoginRequired()
144 @HasRepoPermissionAnyDecorator(
144 @HasRepoPermissionAnyDecorator(
145 'repository.read', 'repository.write', 'repository.admin')
145 'repository.read', 'repository.write', 'repository.admin')
146 @view_config(
146 @view_config(
147 route_name='pullrequest_show_all', request_method='GET',
147 route_name='pullrequest_show_all', request_method='GET',
148 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
149 def pull_request_list(self):
149 def pull_request_list(self):
150 c = self.load_default_context()
150 c = self.load_default_context()
151
151
152 req_get = self.request.GET
152 req_get = self.request.GET
153 c.source = str2bool(req_get.get('source'))
153 c.source = str2bool(req_get.get('source'))
154 c.closed = str2bool(req_get.get('closed'))
154 c.closed = str2bool(req_get.get('closed'))
155 c.my = str2bool(req_get.get('my'))
155 c.my = str2bool(req_get.get('my'))
156 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
157 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
158
158
159 c.active = 'open'
159 c.active = 'open'
160 if c.my:
160 if c.my:
161 c.active = 'my'
161 c.active = 'my'
162 if c.closed:
162 if c.closed:
163 c.active = 'closed'
163 c.active = 'closed'
164 if c.awaiting_review and not c.source:
164 if c.awaiting_review and not c.source:
165 c.active = 'awaiting'
165 c.active = 'awaiting'
166 if c.source and not c.awaiting_review:
166 if c.source and not c.awaiting_review:
167 c.active = 'source'
167 c.active = 'source'
168 if c.awaiting_my_review:
168 if c.awaiting_my_review:
169 c.active = 'awaiting_my'
169 c.active = 'awaiting_my'
170
170
171 return self._get_template_context(c)
171 return self._get_template_context(c)
172
172
173 @LoginRequired()
173 @LoginRequired()
174 @HasRepoPermissionAnyDecorator(
174 @HasRepoPermissionAnyDecorator(
175 'repository.read', 'repository.write', 'repository.admin')
175 'repository.read', 'repository.write', 'repository.admin')
176 @view_config(
176 @view_config(
177 route_name='pullrequest_show_all_data', request_method='GET',
177 route_name='pullrequest_show_all_data', request_method='GET',
178 renderer='json_ext', xhr=True)
178 renderer='json_ext', xhr=True)
179 def pull_request_list_data(self):
179 def pull_request_list_data(self):
180 self.load_default_context()
180 self.load_default_context()
181
181
182 # additional filters
182 # additional filters
183 req_get = self.request.GET
183 req_get = self.request.GET
184 source = str2bool(req_get.get('source'))
184 source = str2bool(req_get.get('source'))
185 closed = str2bool(req_get.get('closed'))
185 closed = str2bool(req_get.get('closed'))
186 my = str2bool(req_get.get('my'))
186 my = str2bool(req_get.get('my'))
187 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_review = str2bool(req_get.get('awaiting_review'))
188 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
189
189
190 filter_type = 'awaiting_review' if awaiting_review \
190 filter_type = 'awaiting_review' if awaiting_review \
191 else 'awaiting_my_review' if awaiting_my_review \
191 else 'awaiting_my_review' if awaiting_my_review \
192 else None
192 else None
193
193
194 opened_by = None
194 opened_by = None
195 if my:
195 if my:
196 opened_by = [self._rhodecode_user.user_id]
196 opened_by = [self._rhodecode_user.user_id]
197
197
198 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
199 if closed:
199 if closed:
200 statuses = [PullRequest.STATUS_CLOSED]
200 statuses = [PullRequest.STATUS_CLOSED]
201
201
202 data = self._get_pull_requests_list(
202 data = self._get_pull_requests_list(
203 repo_name=self.db_repo_name, source=source,
203 repo_name=self.db_repo_name, source=source,
204 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
205
205
206 return data
206 return data
207
207
208 def _is_diff_cache_enabled(self, target_repo):
208 def _is_diff_cache_enabled(self, target_repo):
209 caching_enabled = self._get_general_setting(
209 caching_enabled = self._get_general_setting(
210 target_repo, 'rhodecode_diff_cache')
210 target_repo, 'rhodecode_diff_cache')
211 log.debug('Diff caching enabled: %s', caching_enabled)
211 log.debug('Diff caching enabled: %s', caching_enabled)
212 return caching_enabled
212 return caching_enabled
213
213
214 def _get_diffset(self, source_repo_name, source_repo,
214 def _get_diffset(self, source_repo_name, source_repo,
215 ancestor_commit,
215 ancestor_commit,
216 source_ref_id, target_ref_id,
216 source_ref_id, target_ref_id,
217 target_commit, source_commit, diff_limit, file_limit,
217 target_commit, source_commit, diff_limit, file_limit,
218 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
219
219
220 if use_ancestor:
220 if use_ancestor:
221 # we might want to not use it for versions
221 # we might want to not use it for versions
222 target_ref_id = ancestor_commit.raw_id
222 target_ref_id = ancestor_commit.raw_id
223
223
224 vcs_diff = PullRequestModel().get_diff(
224 vcs_diff = PullRequestModel().get_diff(
225 source_repo, source_ref_id, target_ref_id,
225 source_repo, source_ref_id, target_ref_id,
226 hide_whitespace_changes, diff_context)
226 hide_whitespace_changes, diff_context)
227
227
228 diff_processor = diffs.DiffProcessor(
228 diff_processor = diffs.DiffProcessor(
229 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 vcs_diff, format='newdiff', diff_limit=diff_limit,
230 file_limit=file_limit, show_full_diff=fulldiff)
230 file_limit=file_limit, show_full_diff=fulldiff)
231
231
232 _parsed = diff_processor.prepare()
232 _parsed = diff_processor.prepare()
233
233
234 diffset = codeblocks.DiffSet(
234 diffset = codeblocks.DiffSet(
235 repo_name=self.db_repo_name,
235 repo_name=self.db_repo_name,
236 source_repo_name=source_repo_name,
236 source_repo_name=source_repo_name,
237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
239 )
239 )
240 diffset = self.path_filter.render_patchset_filtered(
240 diffset = self.path_filter.render_patchset_filtered(
241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
242
242
243 return diffset
243 return diffset
244
244
245 def _get_range_diffset(self, source_scm, source_repo,
245 def _get_range_diffset(self, source_scm, source_repo,
246 commit1, commit2, diff_limit, file_limit,
246 commit1, commit2, diff_limit, file_limit,
247 fulldiff, hide_whitespace_changes, diff_context):
247 fulldiff, hide_whitespace_changes, diff_context):
248 vcs_diff = source_scm.get_diff(
248 vcs_diff = source_scm.get_diff(
249 commit1, commit2,
249 commit1, commit2,
250 ignore_whitespace=hide_whitespace_changes,
250 ignore_whitespace=hide_whitespace_changes,
251 context=diff_context)
251 context=diff_context)
252
252
253 diff_processor = diffs.DiffProcessor(
253 diff_processor = diffs.DiffProcessor(
254 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 vcs_diff, format='newdiff', diff_limit=diff_limit,
255 file_limit=file_limit, show_full_diff=fulldiff)
255 file_limit=file_limit, show_full_diff=fulldiff)
256
256
257 _parsed = diff_processor.prepare()
257 _parsed = diff_processor.prepare()
258
258
259 diffset = codeblocks.DiffSet(
259 diffset = codeblocks.DiffSet(
260 repo_name=source_repo.repo_name,
260 repo_name=source_repo.repo_name,
261 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 source_node_getter=codeblocks.diffset_node_getter(commit1),
262 target_node_getter=codeblocks.diffset_node_getter(commit2))
262 target_node_getter=codeblocks.diffset_node_getter(commit2))
263
263
264 diffset = self.path_filter.render_patchset_filtered(
264 diffset = self.path_filter.render_patchset_filtered(
265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
266
266
267 return diffset
267 return diffset
268
268
269 def register_comments_vars(self, c, pull_request, versions):
269 def register_comments_vars(self, c, pull_request, versions):
270 comments_model = CommentsModel()
270 comments_model = CommentsModel()
271
271
272 # GENERAL COMMENTS with versions #
272 # GENERAL COMMENTS with versions #
273 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 q = comments_model._all_general_comments_of_pull_request(pull_request)
274 q = q.order_by(ChangesetComment.comment_id.asc())
274 q = q.order_by(ChangesetComment.comment_id.asc())
275 general_comments = q
275 general_comments = q
276
276
277 # pick comments we want to render at current version
277 # pick comments we want to render at current version
278 c.comment_versions = comments_model.aggregate_comments(
278 c.comment_versions = comments_model.aggregate_comments(
279 general_comments, versions, c.at_version_num)
279 general_comments, versions, c.at_version_num)
280
280
281 # INLINE COMMENTS with versions #
281 # INLINE COMMENTS with versions #
282 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 q = comments_model._all_inline_comments_of_pull_request(pull_request)
283 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
284 inline_comments = q
284 inline_comments = q
285
285
286 c.inline_versions = comments_model.aggregate_comments(
286 c.inline_versions = comments_model.aggregate_comments(
287 inline_comments, versions, c.at_version_num, inline=True)
287 inline_comments, versions, c.at_version_num, inline=True)
288
288
289 # Comments inline+general
289 # Comments inline+general
290 if c.at_version:
290 if c.at_version:
291 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
292 c.comments = c.comment_versions[c.at_version_num]['display']
292 c.comments = c.comment_versions[c.at_version_num]['display']
293 else:
293 else:
294 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
295 c.comments = c.comment_versions[c.at_version_num]['until']
295 c.comments = c.comment_versions[c.at_version_num]['until']
296
296
297 return general_comments, inline_comments
297 return general_comments, inline_comments
298
298
299 @LoginRequired()
299 @LoginRequired()
300 @HasRepoPermissionAnyDecorator(
300 @HasRepoPermissionAnyDecorator(
301 'repository.read', 'repository.write', 'repository.admin')
301 'repository.read', 'repository.write', 'repository.admin')
302 @view_config(
302 @view_config(
303 route_name='pullrequest_show', request_method='GET',
303 route_name='pullrequest_show', request_method='GET',
304 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
304 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
305 def pull_request_show(self):
305 def pull_request_show(self):
306 _ = self.request.translate
306 _ = self.request.translate
307 c = self.load_default_context()
307 c = self.load_default_context()
308
308
309 pull_request = PullRequest.get_or_404(
309 pull_request = PullRequest.get_or_404(
310 self.request.matchdict['pull_request_id'])
310 self.request.matchdict['pull_request_id'])
311 pull_request_id = pull_request.pull_request_id
311 pull_request_id = pull_request.pull_request_id
312
312
313 c.state_progressing = pull_request.is_state_changing()
313 c.state_progressing = pull_request.is_state_changing()
314 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
315 pull_request.target_repo.repo_name, pull_request.pull_request_id)
316
315
317 _new_state = {
316 _new_state = {
318 'created': PullRequest.STATE_CREATED,
317 'created': PullRequest.STATE_CREATED,
319 }.get(self.request.GET.get('force_state'))
318 }.get(self.request.GET.get('force_state'))
320
319
321 if c.is_super_admin and _new_state:
320 if c.is_super_admin and _new_state:
322 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
321 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
323 h.flash(
322 h.flash(
324 _('Pull Request state was force changed to `{}`').format(_new_state),
323 _('Pull Request state was force changed to `{}`').format(_new_state),
325 category='success')
324 category='success')
326 Session().commit()
325 Session().commit()
327
326
328 raise HTTPFound(h.route_path(
327 raise HTTPFound(h.route_path(
329 'pullrequest_show', repo_name=self.db_repo_name,
328 'pullrequest_show', repo_name=self.db_repo_name,
330 pull_request_id=pull_request_id))
329 pull_request_id=pull_request_id))
331
330
332 version = self.request.GET.get('version')
331 version = self.request.GET.get('version')
333 from_version = self.request.GET.get('from_version') or version
332 from_version = self.request.GET.get('from_version') or version
334 merge_checks = self.request.GET.get('merge_checks')
333 merge_checks = self.request.GET.get('merge_checks')
335 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
336 force_refresh = str2bool(self.request.GET.get('force_refresh'))
335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
337 c.range_diff_on = self.request.GET.get('range-diff') == "1"
336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
338
337
339 # fetch global flags of ignore ws or context lines
338 # fetch global flags of ignore ws or context lines
340 diff_context = diffs.get_diff_context(self.request)
339 diff_context = diffs.get_diff_context(self.request)
341 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
342
341
343 (pull_request_latest,
342 (pull_request_latest,
344 pull_request_at_ver,
343 pull_request_at_ver,
345 pull_request_display_obj,
344 pull_request_display_obj,
346 at_version) = PullRequestModel().get_pr_version(
345 at_version) = PullRequestModel().get_pr_version(
347 pull_request_id, version=version)
346 pull_request_id, version=version)
348
347
349 pr_closed = pull_request_latest.is_closed()
348 pr_closed = pull_request_latest.is_closed()
350
349
351 if pr_closed and (version or from_version):
350 if pr_closed and (version or from_version):
352 # not allow to browse versions for closed PR
351 # not allow to browse versions for closed PR
353 raise HTTPFound(h.route_path(
352 raise HTTPFound(h.route_path(
354 'pullrequest_show', repo_name=self.db_repo_name,
353 'pullrequest_show', repo_name=self.db_repo_name,
355 pull_request_id=pull_request_id))
354 pull_request_id=pull_request_id))
356
355
357 versions = pull_request_display_obj.versions()
356 versions = pull_request_display_obj.versions()
358 # used to store per-commit range diffs
357 # used to store per-commit range diffs
359 c.changes = collections.OrderedDict()
358 c.changes = collections.OrderedDict()
360
359
361 c.at_version = at_version
360 c.at_version = at_version
362 c.at_version_num = (at_version
361 c.at_version_num = (at_version
363 if at_version and at_version != PullRequest.LATEST_VER
362 if at_version and at_version != PullRequest.LATEST_VER
364 else None)
363 else None)
365
364
366 c.at_version_index = ChangesetComment.get_index_from_version(
365 c.at_version_index = ChangesetComment.get_index_from_version(
367 c.at_version_num, versions)
366 c.at_version_num, versions)
368
367
369 (prev_pull_request_latest,
368 (prev_pull_request_latest,
370 prev_pull_request_at_ver,
369 prev_pull_request_at_ver,
371 prev_pull_request_display_obj,
370 prev_pull_request_display_obj,
372 prev_at_version) = PullRequestModel().get_pr_version(
371 prev_at_version) = PullRequestModel().get_pr_version(
373 pull_request_id, version=from_version)
372 pull_request_id, version=from_version)
374
373
375 c.from_version = prev_at_version
374 c.from_version = prev_at_version
376 c.from_version_num = (prev_at_version
375 c.from_version_num = (prev_at_version
377 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
378 else None)
377 else None)
379 c.from_version_index = ChangesetComment.get_index_from_version(
378 c.from_version_index = ChangesetComment.get_index_from_version(
380 c.from_version_num, versions)
379 c.from_version_num, versions)
381
380
382 # define if we're in COMPARE mode or VIEW at version mode
381 # define if we're in COMPARE mode or VIEW at version mode
383 compare = at_version != prev_at_version
382 compare = at_version != prev_at_version
384
383
385 # pull_requests repo_name we opened it against
384 # pull_requests repo_name we opened it against
386 # ie. target_repo must match
385 # ie. target_repo must match
387 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
388 log.warning('Mismatch between the current repo: %s, and target %s',
387 log.warning('Mismatch between the current repo: %s, and target %s',
389 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
390 raise HTTPNotFound()
389 raise HTTPNotFound()
391
390
392 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
393
392
394 c.pull_request = pull_request_display_obj
393 c.pull_request = pull_request_display_obj
395 c.renderer = pull_request_at_ver.description_renderer or c.renderer
394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
396 c.pull_request_latest = pull_request_latest
395 c.pull_request_latest = pull_request_latest
397
396
398 # inject latest version
397 # inject latest version
399 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
400 c.versions = versions + [latest_ver]
399 c.versions = versions + [latest_ver]
401
400
402 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
403 c.allowed_to_change_status = False
402 c.allowed_to_change_status = False
404 c.allowed_to_update = False
403 c.allowed_to_update = False
405 c.allowed_to_merge = False
404 c.allowed_to_merge = False
406 c.allowed_to_delete = False
405 c.allowed_to_delete = False
407 c.allowed_to_comment = False
406 c.allowed_to_comment = False
408 c.allowed_to_close = False
407 c.allowed_to_close = False
409 else:
408 else:
410 can_change_status = PullRequestModel().check_user_change_status(
409 can_change_status = PullRequestModel().check_user_change_status(
411 pull_request_at_ver, self._rhodecode_user)
410 pull_request_at_ver, self._rhodecode_user)
412 c.allowed_to_change_status = can_change_status and not pr_closed
411 c.allowed_to_change_status = can_change_status and not pr_closed
413
412
414 c.allowed_to_update = PullRequestModel().check_user_update(
413 c.allowed_to_update = PullRequestModel().check_user_update(
415 pull_request_latest, self._rhodecode_user) and not pr_closed
414 pull_request_latest, self._rhodecode_user) and not pr_closed
416 c.allowed_to_merge = PullRequestModel().check_user_merge(
415 c.allowed_to_merge = PullRequestModel().check_user_merge(
417 pull_request_latest, self._rhodecode_user) and not pr_closed
416 pull_request_latest, self._rhodecode_user) and not pr_closed
418 c.allowed_to_delete = PullRequestModel().check_user_delete(
417 c.allowed_to_delete = PullRequestModel().check_user_delete(
419 pull_request_latest, self._rhodecode_user) and not pr_closed
418 pull_request_latest, self._rhodecode_user) and not pr_closed
420 c.allowed_to_comment = not pr_closed
419 c.allowed_to_comment = not pr_closed
421 c.allowed_to_close = c.allowed_to_merge and not pr_closed
420 c.allowed_to_close = c.allowed_to_merge and not pr_closed
422
421
423 c.forbid_adding_reviewers = False
422 c.forbid_adding_reviewers = False
424 c.forbid_author_to_review = False
423 c.forbid_author_to_review = False
425 c.forbid_commit_author_to_review = False
424 c.forbid_commit_author_to_review = False
426
425
427 if pull_request_latest.reviewer_data and \
426 if pull_request_latest.reviewer_data and \
428 'rules' in pull_request_latest.reviewer_data:
427 'rules' in pull_request_latest.reviewer_data:
429 rules = pull_request_latest.reviewer_data['rules'] or {}
428 rules = pull_request_latest.reviewer_data['rules'] or {}
430 try:
429 try:
431 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
432 c.forbid_author_to_review = rules.get('forbid_author_to_review')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
433 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
434 except Exception:
433 except Exception:
435 pass
434 pass
436
435
437 # check merge capabilities
436 # check merge capabilities
438 _merge_check = MergeCheck.validate(
437 _merge_check = MergeCheck.validate(
439 pull_request_latest, auth_user=self._rhodecode_user,
438 pull_request_latest, auth_user=self._rhodecode_user,
440 translator=self.request.translate,
439 translator=self.request.translate,
441 force_shadow_repo_refresh=force_refresh)
440 force_shadow_repo_refresh=force_refresh)
442
441
443 c.pr_merge_errors = _merge_check.error_details
442 c.pr_merge_errors = _merge_check.error_details
444 c.pr_merge_possible = not _merge_check.failed
443 c.pr_merge_possible = not _merge_check.failed
445 c.pr_merge_message = _merge_check.merge_msg
444 c.pr_merge_message = _merge_check.merge_msg
446 c.pr_merge_source_commit = _merge_check.source_commit
445 c.pr_merge_source_commit = _merge_check.source_commit
447 c.pr_merge_target_commit = _merge_check.target_commit
446 c.pr_merge_target_commit = _merge_check.target_commit
448
447
449 c.pr_merge_info = MergeCheck.get_merge_conditions(
448 c.pr_merge_info = MergeCheck.get_merge_conditions(
450 pull_request_latest, translator=self.request.translate)
449 pull_request_latest, translator=self.request.translate)
451
450
452 c.pull_request_review_status = _merge_check.review_status
451 c.pull_request_review_status = _merge_check.review_status
453 if merge_checks:
452 if merge_checks:
454 self.request.override_renderer = \
453 self.request.override_renderer = \
455 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
456 return self._get_template_context(c)
455 return self._get_template_context(c)
457
456
458 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
459 c.reviewers_count = pull_request.reviewers_count
458 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
459 c.observers_count = pull_request.observers_count
461
460
462 # reviewers and statuses
461 # reviewers and statuses
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
462 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
463 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
464 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
466
465
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
466 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
468 member_reviewer = h.reviewer_as_json(
467 member_reviewer = h.reviewer_as_json(
469 member, reasons=reasons, mandatory=mandatory,
468 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
469 role=review_obj.role,
471 user_group=review_obj.rule_user_group_data()
470 user_group=review_obj.rule_user_group_data()
472 )
471 )
473
472
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
473 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
475 member_reviewer['review_status'] = current_review_status
474 member_reviewer['review_status'] = current_review_status
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
475 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
476 member_reviewer['allowed_to_update'] = c.allowed_to_update
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
477 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
479
478
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
479 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
481
480
482 for observer_obj, member in pull_request_at_ver.observers():
481 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
482 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
483 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
484 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
485 user_group=observer_obj.rule_user_group_data()
487 )
486 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
487 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
488 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
489
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
490 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
491
493 general_comments, inline_comments = \
492 general_comments, inline_comments = \
494 self.register_comments_vars(c, pull_request_latest, versions)
493 self.register_comments_vars(c, pull_request_latest, versions)
495
494
496 # TODOs
495 # TODOs
497 c.unresolved_comments = CommentsModel() \
496 c.unresolved_comments = CommentsModel() \
498 .get_pull_request_unresolved_todos(pull_request_latest)
497 .get_pull_request_unresolved_todos(pull_request_latest)
499 c.resolved_comments = CommentsModel() \
498 c.resolved_comments = CommentsModel() \
500 .get_pull_request_resolved_todos(pull_request_latest)
499 .get_pull_request_resolved_todos(pull_request_latest)
501
500
502 # if we use version, then do not show later comments
501 # if we use version, then do not show later comments
503 # than current version
502 # than current version
504 display_inline_comments = collections.defaultdict(
503 display_inline_comments = collections.defaultdict(
505 lambda: collections.defaultdict(list))
504 lambda: collections.defaultdict(list))
506 for co in inline_comments:
505 for co in inline_comments:
507 if c.at_version_num:
506 if c.at_version_num:
508 # pick comments that are at least UPTO given version, so we
507 # pick comments that are at least UPTO given version, so we
509 # don't render comments for higher version
508 # don't render comments for higher version
510 should_render = co.pull_request_version_id and \
509 should_render = co.pull_request_version_id and \
511 co.pull_request_version_id <= c.at_version_num
510 co.pull_request_version_id <= c.at_version_num
512 else:
511 else:
513 # showing all, for 'latest'
512 # showing all, for 'latest'
514 should_render = True
513 should_render = True
515
514
516 if should_render:
515 if should_render:
517 display_inline_comments[co.f_path][co.line_no].append(co)
516 display_inline_comments[co.f_path][co.line_no].append(co)
518
517
519 # load diff data into template context, if we use compare mode then
518 # load diff data into template context, if we use compare mode then
520 # diff is calculated based on changes between versions of PR
519 # diff is calculated based on changes between versions of PR
521
520
522 source_repo = pull_request_at_ver.source_repo
521 source_repo = pull_request_at_ver.source_repo
523 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
522 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
524
523
525 target_repo = pull_request_at_ver.target_repo
524 target_repo = pull_request_at_ver.target_repo
526 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
525 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
527
526
528 if compare:
527 if compare:
529 # in compare switch the diff base to latest commit from prev version
528 # in compare switch the diff base to latest commit from prev version
530 target_ref_id = prev_pull_request_display_obj.revisions[0]
529 target_ref_id = prev_pull_request_display_obj.revisions[0]
531
530
532 # despite opening commits for bookmarks/branches/tags, we always
531 # despite opening commits for bookmarks/branches/tags, we always
533 # convert this to rev to prevent changes after bookmark or branch change
532 # convert this to rev to prevent changes after bookmark or branch change
534 c.source_ref_type = 'rev'
533 c.source_ref_type = 'rev'
535 c.source_ref = source_ref_id
534 c.source_ref = source_ref_id
536
535
537 c.target_ref_type = 'rev'
536 c.target_ref_type = 'rev'
538 c.target_ref = target_ref_id
537 c.target_ref = target_ref_id
539
538
540 c.source_repo = source_repo
539 c.source_repo = source_repo
541 c.target_repo = target_repo
540 c.target_repo = target_repo
542
541
543 c.commit_ranges = []
542 c.commit_ranges = []
544 source_commit = EmptyCommit()
543 source_commit = EmptyCommit()
545 target_commit = EmptyCommit()
544 target_commit = EmptyCommit()
546 c.missing_requirements = False
545 c.missing_requirements = False
547
546
548 source_scm = source_repo.scm_instance()
547 source_scm = source_repo.scm_instance()
549 target_scm = target_repo.scm_instance()
548 target_scm = target_repo.scm_instance()
550
549
551 shadow_scm = None
550 shadow_scm = None
552 try:
551 try:
553 shadow_scm = pull_request_latest.get_shadow_repo()
552 shadow_scm = pull_request_latest.get_shadow_repo()
554 except Exception:
553 except Exception:
555 log.debug('Failed to get shadow repo', exc_info=True)
554 log.debug('Failed to get shadow repo', exc_info=True)
556 # try first the existing source_repo, and then shadow
555 # try first the existing source_repo, and then shadow
557 # repo if we can obtain one
556 # repo if we can obtain one
558 commits_source_repo = source_scm
557 commits_source_repo = source_scm
559 if shadow_scm:
558 if shadow_scm:
560 commits_source_repo = shadow_scm
559 commits_source_repo = shadow_scm
561
560
562 c.commits_source_repo = commits_source_repo
561 c.commits_source_repo = commits_source_repo
563 c.ancestor = None # set it to None, to hide it from PR view
562 c.ancestor = None # set it to None, to hide it from PR view
564
563
565 # empty version means latest, so we keep this to prevent
564 # empty version means latest, so we keep this to prevent
566 # double caching
565 # double caching
567 version_normalized = version or PullRequest.LATEST_VER
566 version_normalized = version or PullRequest.LATEST_VER
568 from_version_normalized = from_version or PullRequest.LATEST_VER
567 from_version_normalized = from_version or PullRequest.LATEST_VER
569
568
570 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
569 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
571 cache_file_path = diff_cache_exist(
570 cache_file_path = diff_cache_exist(
572 cache_path, 'pull_request', pull_request_id, version_normalized,
571 cache_path, 'pull_request', pull_request_id, version_normalized,
573 from_version_normalized, source_ref_id, target_ref_id,
572 from_version_normalized, source_ref_id, target_ref_id,
574 hide_whitespace_changes, diff_context, c.fulldiff)
573 hide_whitespace_changes, diff_context, c.fulldiff)
575
574
576 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
575 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
577 force_recache = self.get_recache_flag()
576 force_recache = self.get_recache_flag()
578
577
579 cached_diff = None
578 cached_diff = None
580 if caching_enabled:
579 if caching_enabled:
581 cached_diff = load_cached_diff(cache_file_path)
580 cached_diff = load_cached_diff(cache_file_path)
582
581
583 has_proper_commit_cache = (
582 has_proper_commit_cache = (
584 cached_diff and cached_diff.get('commits')
583 cached_diff and cached_diff.get('commits')
585 and len(cached_diff.get('commits', [])) == 5
584 and len(cached_diff.get('commits', [])) == 5
586 and cached_diff.get('commits')[0]
585 and cached_diff.get('commits')[0]
587 and cached_diff.get('commits')[3])
586 and cached_diff.get('commits')[3])
588
587
589 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
588 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
590 diff_commit_cache = \
589 diff_commit_cache = \
591 (ancestor_commit, commit_cache, missing_requirements,
590 (ancestor_commit, commit_cache, missing_requirements,
592 source_commit, target_commit) = cached_diff['commits']
591 source_commit, target_commit) = cached_diff['commits']
593 else:
592 else:
594 # NOTE(marcink): we reach potentially unreachable errors when a PR has
593 # NOTE(marcink): we reach potentially unreachable errors when a PR has
595 # merge errors resulting in potentially hidden commits in the shadow repo.
594 # merge errors resulting in potentially hidden commits in the shadow repo.
596 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
595 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
597 and _merge_check.merge_response
596 and _merge_check.merge_response
598 maybe_unreachable = maybe_unreachable \
597 maybe_unreachable = maybe_unreachable \
599 and _merge_check.merge_response.metadata.get('unresolved_files')
598 and _merge_check.merge_response.metadata.get('unresolved_files')
600 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
599 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
601 diff_commit_cache = \
600 diff_commit_cache = \
602 (ancestor_commit, commit_cache, missing_requirements,
601 (ancestor_commit, commit_cache, missing_requirements,
603 source_commit, target_commit) = self.get_commits(
602 source_commit, target_commit) = self.get_commits(
604 commits_source_repo,
603 commits_source_repo,
605 pull_request_at_ver,
604 pull_request_at_ver,
606 source_commit,
605 source_commit,
607 source_ref_id,
606 source_ref_id,
608 source_scm,
607 source_scm,
609 target_commit,
608 target_commit,
610 target_ref_id,
609 target_ref_id,
611 target_scm,
610 target_scm,
612 maybe_unreachable=maybe_unreachable)
611 maybe_unreachable=maybe_unreachable)
613
612
614 # register our commit range
613 # register our commit range
615 for comm in commit_cache.values():
614 for comm in commit_cache.values():
616 c.commit_ranges.append(comm)
615 c.commit_ranges.append(comm)
617
616
618 c.missing_requirements = missing_requirements
617 c.missing_requirements = missing_requirements
619 c.ancestor_commit = ancestor_commit
618 c.ancestor_commit = ancestor_commit
620 c.statuses = source_repo.statuses(
619 c.statuses = source_repo.statuses(
621 [x.raw_id for x in c.commit_ranges])
620 [x.raw_id for x in c.commit_ranges])
622
621
623 # auto collapse if we have more than limit
622 # auto collapse if we have more than limit
624 collapse_limit = diffs.DiffProcessor._collapse_commits_over
623 collapse_limit = diffs.DiffProcessor._collapse_commits_over
625 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
624 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
626 c.compare_mode = compare
625 c.compare_mode = compare
627
626
628 # diff_limit is the old behavior, will cut off the whole diff
627 # diff_limit is the old behavior, will cut off the whole diff
629 # if the limit is applied otherwise will just hide the
628 # if the limit is applied otherwise will just hide the
630 # big files from the front-end
629 # big files from the front-end
631 diff_limit = c.visual.cut_off_limit_diff
630 diff_limit = c.visual.cut_off_limit_diff
632 file_limit = c.visual.cut_off_limit_file
631 file_limit = c.visual.cut_off_limit_file
633
632
634 c.missing_commits = False
633 c.missing_commits = False
635 if (c.missing_requirements
634 if (c.missing_requirements
636 or isinstance(source_commit, EmptyCommit)
635 or isinstance(source_commit, EmptyCommit)
637 or source_commit == target_commit):
636 or source_commit == target_commit):
638
637
639 c.missing_commits = True
638 c.missing_commits = True
640 else:
639 else:
641 c.inline_comments = display_inline_comments
640 c.inline_comments = display_inline_comments
642
641
643 use_ancestor = True
642 use_ancestor = True
644 if from_version_normalized != version_normalized:
643 if from_version_normalized != version_normalized:
645 use_ancestor = False
644 use_ancestor = False
646
645
647 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
646 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
648 if not force_recache and has_proper_diff_cache:
647 if not force_recache and has_proper_diff_cache:
649 c.diffset = cached_diff['diff']
648 c.diffset = cached_diff['diff']
650 else:
649 else:
651 try:
650 try:
652 c.diffset = self._get_diffset(
651 c.diffset = self._get_diffset(
653 c.source_repo.repo_name, commits_source_repo,
652 c.source_repo.repo_name, commits_source_repo,
654 c.ancestor_commit,
653 c.ancestor_commit,
655 source_ref_id, target_ref_id,
654 source_ref_id, target_ref_id,
656 target_commit, source_commit,
655 target_commit, source_commit,
657 diff_limit, file_limit, c.fulldiff,
656 diff_limit, file_limit, c.fulldiff,
658 hide_whitespace_changes, diff_context,
657 hide_whitespace_changes, diff_context,
659 use_ancestor=use_ancestor
658 use_ancestor=use_ancestor
660 )
659 )
661
660
662 # save cached diff
661 # save cached diff
663 if caching_enabled:
662 if caching_enabled:
664 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
663 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
665 except CommitDoesNotExistError:
664 except CommitDoesNotExistError:
666 log.exception('Failed to generate diffset')
665 log.exception('Failed to generate diffset')
667 c.missing_commits = True
666 c.missing_commits = True
668
667
669 if not c.missing_commits:
668 if not c.missing_commits:
670
669
671 c.limited_diff = c.diffset.limited_diff
670 c.limited_diff = c.diffset.limited_diff
672
671
673 # calculate removed files that are bound to comments
672 # calculate removed files that are bound to comments
674 comment_deleted_files = [
673 comment_deleted_files = [
675 fname for fname in display_inline_comments
674 fname for fname in display_inline_comments
676 if fname not in c.diffset.file_stats]
675 if fname not in c.diffset.file_stats]
677
676
678 c.deleted_files_comments = collections.defaultdict(dict)
677 c.deleted_files_comments = collections.defaultdict(dict)
679 for fname, per_line_comments in display_inline_comments.items():
678 for fname, per_line_comments in display_inline_comments.items():
680 if fname in comment_deleted_files:
679 if fname in comment_deleted_files:
681 c.deleted_files_comments[fname]['stats'] = 0
680 c.deleted_files_comments[fname]['stats'] = 0
682 c.deleted_files_comments[fname]['comments'] = list()
681 c.deleted_files_comments[fname]['comments'] = list()
683 for lno, comments in per_line_comments.items():
682 for lno, comments in per_line_comments.items():
684 c.deleted_files_comments[fname]['comments'].extend(comments)
683 c.deleted_files_comments[fname]['comments'].extend(comments)
685
684
686 # maybe calculate the range diff
685 # maybe calculate the range diff
687 if c.range_diff_on:
686 if c.range_diff_on:
688 # TODO(marcink): set whitespace/context
687 # TODO(marcink): set whitespace/context
689 context_lcl = 3
688 context_lcl = 3
690 ign_whitespace_lcl = False
689 ign_whitespace_lcl = False
691
690
692 for commit in c.commit_ranges:
691 for commit in c.commit_ranges:
693 commit2 = commit
692 commit2 = commit
694 commit1 = commit.first_parent
693 commit1 = commit.first_parent
695
694
696 range_diff_cache_file_path = diff_cache_exist(
695 range_diff_cache_file_path = diff_cache_exist(
697 cache_path, 'diff', commit.raw_id,
696 cache_path, 'diff', commit.raw_id,
698 ign_whitespace_lcl, context_lcl, c.fulldiff)
697 ign_whitespace_lcl, context_lcl, c.fulldiff)
699
698
700 cached_diff = None
699 cached_diff = None
701 if caching_enabled:
700 if caching_enabled:
702 cached_diff = load_cached_diff(range_diff_cache_file_path)
701 cached_diff = load_cached_diff(range_diff_cache_file_path)
703
702
704 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
703 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
705 if not force_recache and has_proper_diff_cache:
704 if not force_recache and has_proper_diff_cache:
706 diffset = cached_diff['diff']
705 diffset = cached_diff['diff']
707 else:
706 else:
708 diffset = self._get_range_diffset(
707 diffset = self._get_range_diffset(
709 commits_source_repo, source_repo,
708 commits_source_repo, source_repo,
710 commit1, commit2, diff_limit, file_limit,
709 commit1, commit2, diff_limit, file_limit,
711 c.fulldiff, ign_whitespace_lcl, context_lcl
710 c.fulldiff, ign_whitespace_lcl, context_lcl
712 )
711 )
713
712
714 # save cached diff
713 # save cached diff
715 if caching_enabled:
714 if caching_enabled:
716 cache_diff(range_diff_cache_file_path, diffset, None)
715 cache_diff(range_diff_cache_file_path, diffset, None)
717
716
718 c.changes[commit.raw_id] = diffset
717 c.changes[commit.raw_id] = diffset
719
718
720 # this is a hack to properly display links, when creating PR, the
719 # this is a hack to properly display links, when creating PR, the
721 # compare view and others uses different notation, and
720 # compare view and others uses different notation, and
722 # compare_commits.mako renders links based on the target_repo.
721 # compare_commits.mako renders links based on the target_repo.
723 # We need to swap that here to generate it properly on the html side
722 # We need to swap that here to generate it properly on the html side
724 c.target_repo = c.source_repo
723 c.target_repo = c.source_repo
725
724
726 c.commit_statuses = ChangesetStatus.STATUSES
725 c.commit_statuses = ChangesetStatus.STATUSES
727
726
728 c.show_version_changes = not pr_closed
727 c.show_version_changes = not pr_closed
729 if c.show_version_changes:
728 if c.show_version_changes:
730 cur_obj = pull_request_at_ver
729 cur_obj = pull_request_at_ver
731 prev_obj = prev_pull_request_at_ver
730 prev_obj = prev_pull_request_at_ver
732
731
733 old_commit_ids = prev_obj.revisions
732 old_commit_ids = prev_obj.revisions
734 new_commit_ids = cur_obj.revisions
733 new_commit_ids = cur_obj.revisions
735 commit_changes = PullRequestModel()._calculate_commit_id_changes(
734 commit_changes = PullRequestModel()._calculate_commit_id_changes(
736 old_commit_ids, new_commit_ids)
735 old_commit_ids, new_commit_ids)
737 c.commit_changes_summary = commit_changes
736 c.commit_changes_summary = commit_changes
738
737
739 # calculate the diff for commits between versions
738 # calculate the diff for commits between versions
740 c.commit_changes = []
739 c.commit_changes = []
741
740
742 def mark(cs, fw):
741 def mark(cs, fw):
743 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
742 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
744
743
745 for c_type, raw_id in mark(commit_changes.added, 'a') \
744 for c_type, raw_id in mark(commit_changes.added, 'a') \
746 + mark(commit_changes.removed, 'r') \
745 + mark(commit_changes.removed, 'r') \
747 + mark(commit_changes.common, 'c'):
746 + mark(commit_changes.common, 'c'):
748
747
749 if raw_id in commit_cache:
748 if raw_id in commit_cache:
750 commit = commit_cache[raw_id]
749 commit = commit_cache[raw_id]
751 else:
750 else:
752 try:
751 try:
753 commit = commits_source_repo.get_commit(raw_id)
752 commit = commits_source_repo.get_commit(raw_id)
754 except CommitDoesNotExistError:
753 except CommitDoesNotExistError:
755 # in case we fail extracting still use "dummy" commit
754 # in case we fail extracting still use "dummy" commit
756 # for display in commit diff
755 # for display in commit diff
757 commit = h.AttributeDict(
756 commit = h.AttributeDict(
758 {'raw_id': raw_id,
757 {'raw_id': raw_id,
759 'message': 'EMPTY or MISSING COMMIT'})
758 'message': 'EMPTY or MISSING COMMIT'})
760 c.commit_changes.append([c_type, commit])
759 c.commit_changes.append([c_type, commit])
761
760
762 # current user review statuses for each version
761 # current user review statuses for each version
763 c.review_versions = {}
762 c.review_versions = {}
764 if self._rhodecode_user.user_id in c.allowed_reviewers:
763 if self._rhodecode_user.user_id in c.allowed_reviewers:
765 for co in general_comments:
764 for co in general_comments:
766 if co.author.user_id == self._rhodecode_user.user_id:
765 if co.author.user_id == self._rhodecode_user.user_id:
767 status = co.status_change
766 status = co.status_change
768 if status:
767 if status:
769 _ver_pr = status[0].comment.pull_request_version_id
768 _ver_pr = status[0].comment.pull_request_version_id
770 c.review_versions[_ver_pr] = status[0]
769 c.review_versions[_ver_pr] = status[0]
771
770
772 return self._get_template_context(c)
771 return self._get_template_context(c)
773
772
774 def get_commits(
773 def get_commits(
775 self, commits_source_repo, pull_request_at_ver, source_commit,
774 self, commits_source_repo, pull_request_at_ver, source_commit,
776 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
775 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
777 maybe_unreachable=False):
776 maybe_unreachable=False):
778
777
779 commit_cache = collections.OrderedDict()
778 commit_cache = collections.OrderedDict()
780 missing_requirements = False
779 missing_requirements = False
781
780
782 try:
781 try:
783 pre_load = ["author", "date", "message", "branch", "parents"]
782 pre_load = ["author", "date", "message", "branch", "parents"]
784
783
785 pull_request_commits = pull_request_at_ver.revisions
784 pull_request_commits = pull_request_at_ver.revisions
786 log.debug('Loading %s commits from %s',
785 log.debug('Loading %s commits from %s',
787 len(pull_request_commits), commits_source_repo)
786 len(pull_request_commits), commits_source_repo)
788
787
789 for rev in pull_request_commits:
788 for rev in pull_request_commits:
790 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
789 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
791 maybe_unreachable=maybe_unreachable)
790 maybe_unreachable=maybe_unreachable)
792 commit_cache[comm.raw_id] = comm
791 commit_cache[comm.raw_id] = comm
793
792
794 # Order here matters, we first need to get target, and then
793 # Order here matters, we first need to get target, and then
795 # the source
794 # the source
796 target_commit = commits_source_repo.get_commit(
795 target_commit = commits_source_repo.get_commit(
797 commit_id=safe_str(target_ref_id))
796 commit_id=safe_str(target_ref_id))
798
797
799 source_commit = commits_source_repo.get_commit(
798 source_commit = commits_source_repo.get_commit(
800 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
799 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
801 except CommitDoesNotExistError:
800 except CommitDoesNotExistError:
802 log.warning('Failed to get commit from `{}` repo'.format(
801 log.warning('Failed to get commit from `{}` repo'.format(
803 commits_source_repo), exc_info=True)
802 commits_source_repo), exc_info=True)
804 except RepositoryRequirementError:
803 except RepositoryRequirementError:
805 log.warning('Failed to get all required data from repo', exc_info=True)
804 log.warning('Failed to get all required data from repo', exc_info=True)
806 missing_requirements = True
805 missing_requirements = True
807
806
808 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
807 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
809
808
810 try:
809 try:
811 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
810 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
812 except Exception:
811 except Exception:
813 ancestor_commit = None
812 ancestor_commit = None
814
813
815 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
814 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
816
815
817 def assure_not_empty_repo(self):
816 def assure_not_empty_repo(self):
818 _ = self.request.translate
817 _ = self.request.translate
819
818
820 try:
819 try:
821 self.db_repo.scm_instance().get_commit()
820 self.db_repo.scm_instance().get_commit()
822 except EmptyRepositoryError:
821 except EmptyRepositoryError:
823 h.flash(h.literal(_('There are no commits yet')),
822 h.flash(h.literal(_('There are no commits yet')),
824 category='warning')
823 category='warning')
825 raise HTTPFound(
824 raise HTTPFound(
826 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
825 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
827
826
828 @LoginRequired()
827 @LoginRequired()
829 @NotAnonymous()
828 @NotAnonymous()
830 @HasRepoPermissionAnyDecorator(
829 @HasRepoPermissionAnyDecorator(
831 'repository.read', 'repository.write', 'repository.admin')
830 'repository.read', 'repository.write', 'repository.admin')
832 @view_config(
831 @view_config(
833 route_name='pullrequest_new', request_method='GET',
832 route_name='pullrequest_new', request_method='GET',
834 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
833 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
835 def pull_request_new(self):
834 def pull_request_new(self):
836 _ = self.request.translate
835 _ = self.request.translate
837 c = self.load_default_context()
836 c = self.load_default_context()
838
837
839 self.assure_not_empty_repo()
838 self.assure_not_empty_repo()
840 source_repo = self.db_repo
839 source_repo = self.db_repo
841
840
842 commit_id = self.request.GET.get('commit')
841 commit_id = self.request.GET.get('commit')
843 branch_ref = self.request.GET.get('branch')
842 branch_ref = self.request.GET.get('branch')
844 bookmark_ref = self.request.GET.get('bookmark')
843 bookmark_ref = self.request.GET.get('bookmark')
845
844
846 try:
845 try:
847 source_repo_data = PullRequestModel().generate_repo_data(
846 source_repo_data = PullRequestModel().generate_repo_data(
848 source_repo, commit_id=commit_id,
847 source_repo, commit_id=commit_id,
849 branch=branch_ref, bookmark=bookmark_ref,
848 branch=branch_ref, bookmark=bookmark_ref,
850 translator=self.request.translate)
849 translator=self.request.translate)
851 except CommitDoesNotExistError as e:
850 except CommitDoesNotExistError as e:
852 log.exception(e)
851 log.exception(e)
853 h.flash(_('Commit does not exist'), 'error')
852 h.flash(_('Commit does not exist'), 'error')
854 raise HTTPFound(
853 raise HTTPFound(
855 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
854 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
856
855
857 default_target_repo = source_repo
856 default_target_repo = source_repo
858
857
859 if source_repo.parent and c.has_origin_repo_read_perm:
858 if source_repo.parent and c.has_origin_repo_read_perm:
860 parent_vcs_obj = source_repo.parent.scm_instance()
859 parent_vcs_obj = source_repo.parent.scm_instance()
861 if parent_vcs_obj and not parent_vcs_obj.is_empty():
860 if parent_vcs_obj and not parent_vcs_obj.is_empty():
862 # change default if we have a parent repo
861 # change default if we have a parent repo
863 default_target_repo = source_repo.parent
862 default_target_repo = source_repo.parent
864
863
865 target_repo_data = PullRequestModel().generate_repo_data(
864 target_repo_data = PullRequestModel().generate_repo_data(
866 default_target_repo, translator=self.request.translate)
865 default_target_repo, translator=self.request.translate)
867
866
868 selected_source_ref = source_repo_data['refs']['selected_ref']
867 selected_source_ref = source_repo_data['refs']['selected_ref']
869 title_source_ref = ''
868 title_source_ref = ''
870 if selected_source_ref:
869 if selected_source_ref:
871 title_source_ref = selected_source_ref.split(':', 2)[1]
870 title_source_ref = selected_source_ref.split(':', 2)[1]
872 c.default_title = PullRequestModel().generate_pullrequest_title(
871 c.default_title = PullRequestModel().generate_pullrequest_title(
873 source=source_repo.repo_name,
872 source=source_repo.repo_name,
874 source_ref=title_source_ref,
873 source_ref=title_source_ref,
875 target=default_target_repo.repo_name
874 target=default_target_repo.repo_name
876 )
875 )
877
876
878 c.default_repo_data = {
877 c.default_repo_data = {
879 'source_repo_name': source_repo.repo_name,
878 'source_repo_name': source_repo.repo_name,
880 'source_refs_json': json.dumps(source_repo_data),
879 'source_refs_json': json.dumps(source_repo_data),
881 'target_repo_name': default_target_repo.repo_name,
880 'target_repo_name': default_target_repo.repo_name,
882 'target_refs_json': json.dumps(target_repo_data),
881 'target_refs_json': json.dumps(target_repo_data),
883 }
882 }
884 c.default_source_ref = selected_source_ref
883 c.default_source_ref = selected_source_ref
885
884
886 return self._get_template_context(c)
885 return self._get_template_context(c)
887
886
888 @LoginRequired()
887 @LoginRequired()
889 @NotAnonymous()
888 @NotAnonymous()
890 @HasRepoPermissionAnyDecorator(
889 @HasRepoPermissionAnyDecorator(
891 'repository.read', 'repository.write', 'repository.admin')
890 'repository.read', 'repository.write', 'repository.admin')
892 @view_config(
891 @view_config(
893 route_name='pullrequest_repo_refs', request_method='GET',
892 route_name='pullrequest_repo_refs', request_method='GET',
894 renderer='json_ext', xhr=True)
893 renderer='json_ext', xhr=True)
895 def pull_request_repo_refs(self):
894 def pull_request_repo_refs(self):
896 self.load_default_context()
895 self.load_default_context()
897 target_repo_name = self.request.matchdict['target_repo_name']
896 target_repo_name = self.request.matchdict['target_repo_name']
898 repo = Repository.get_by_repo_name(target_repo_name)
897 repo = Repository.get_by_repo_name(target_repo_name)
899 if not repo:
898 if not repo:
900 raise HTTPNotFound()
899 raise HTTPNotFound()
901
900
902 target_perm = HasRepoPermissionAny(
901 target_perm = HasRepoPermissionAny(
903 'repository.read', 'repository.write', 'repository.admin')(
902 'repository.read', 'repository.write', 'repository.admin')(
904 target_repo_name)
903 target_repo_name)
905 if not target_perm:
904 if not target_perm:
906 raise HTTPNotFound()
905 raise HTTPNotFound()
907
906
908 return PullRequestModel().generate_repo_data(
907 return PullRequestModel().generate_repo_data(
909 repo, translator=self.request.translate)
908 repo, translator=self.request.translate)
910
909
911 @LoginRequired()
910 @LoginRequired()
912 @NotAnonymous()
911 @NotAnonymous()
913 @HasRepoPermissionAnyDecorator(
912 @HasRepoPermissionAnyDecorator(
914 'repository.read', 'repository.write', 'repository.admin')
913 'repository.read', 'repository.write', 'repository.admin')
915 @view_config(
914 @view_config(
916 route_name='pullrequest_repo_targets', request_method='GET',
915 route_name='pullrequest_repo_targets', request_method='GET',
917 renderer='json_ext', xhr=True)
916 renderer='json_ext', xhr=True)
918 def pullrequest_repo_targets(self):
917 def pullrequest_repo_targets(self):
919 _ = self.request.translate
918 _ = self.request.translate
920 filter_query = self.request.GET.get('query')
919 filter_query = self.request.GET.get('query')
921
920
922 # get the parents
921 # get the parents
923 parent_target_repos = []
922 parent_target_repos = []
924 if self.db_repo.parent:
923 if self.db_repo.parent:
925 parents_query = Repository.query() \
924 parents_query = Repository.query() \
926 .order_by(func.length(Repository.repo_name)) \
925 .order_by(func.length(Repository.repo_name)) \
927 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
926 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
928
927
929 if filter_query:
928 if filter_query:
930 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
929 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
931 parents_query = parents_query.filter(
930 parents_query = parents_query.filter(
932 Repository.repo_name.ilike(ilike_expression))
931 Repository.repo_name.ilike(ilike_expression))
933 parents = parents_query.limit(20).all()
932 parents = parents_query.limit(20).all()
934
933
935 for parent in parents:
934 for parent in parents:
936 parent_vcs_obj = parent.scm_instance()
935 parent_vcs_obj = parent.scm_instance()
937 if parent_vcs_obj and not parent_vcs_obj.is_empty():
936 if parent_vcs_obj and not parent_vcs_obj.is_empty():
938 parent_target_repos.append(parent)
937 parent_target_repos.append(parent)
939
938
940 # get other forks, and repo itself
939 # get other forks, and repo itself
941 query = Repository.query() \
940 query = Repository.query() \
942 .order_by(func.length(Repository.repo_name)) \
941 .order_by(func.length(Repository.repo_name)) \
943 .filter(
942 .filter(
944 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
943 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
945 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
944 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
946 ) \
945 ) \
947 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
946 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
948
947
949 if filter_query:
948 if filter_query:
950 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
949 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
951 query = query.filter(Repository.repo_name.ilike(ilike_expression))
950 query = query.filter(Repository.repo_name.ilike(ilike_expression))
952
951
953 limit = max(20 - len(parent_target_repos), 5) # not less then 5
952 limit = max(20 - len(parent_target_repos), 5) # not less then 5
954 target_repos = query.limit(limit).all()
953 target_repos = query.limit(limit).all()
955
954
956 all_target_repos = target_repos + parent_target_repos
955 all_target_repos = target_repos + parent_target_repos
957
956
958 repos = []
957 repos = []
959 # This checks permissions to the repositories
958 # This checks permissions to the repositories
960 for obj in ScmModel().get_repos(all_target_repos):
959 for obj in ScmModel().get_repos(all_target_repos):
961 repos.append({
960 repos.append({
962 'id': obj['name'],
961 'id': obj['name'],
963 'text': obj['name'],
962 'text': obj['name'],
964 'type': 'repo',
963 'type': 'repo',
965 'repo_id': obj['dbrepo']['repo_id'],
964 'repo_id': obj['dbrepo']['repo_id'],
966 'repo_type': obj['dbrepo']['repo_type'],
965 'repo_type': obj['dbrepo']['repo_type'],
967 'private': obj['dbrepo']['private'],
966 'private': obj['dbrepo']['private'],
968
967
969 })
968 })
970
969
971 data = {
970 data = {
972 'more': False,
971 'more': False,
973 'results': [{
972 'results': [{
974 'text': _('Repositories'),
973 'text': _('Repositories'),
975 'children': repos
974 'children': repos
976 }] if repos else []
975 }] if repos else []
977 }
976 }
978 return data
977 return data
979
978
980 def _get_existing_ids(self, post_data):
979 def _get_existing_ids(self, post_data):
981 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
980 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
982
981
983 @LoginRequired()
982 @LoginRequired()
984 @NotAnonymous()
983 @NotAnonymous()
985 @HasRepoPermissionAnyDecorator(
984 @HasRepoPermissionAnyDecorator(
986 'repository.read', 'repository.write', 'repository.admin')
985 'repository.read', 'repository.write', 'repository.admin')
987 @view_config(
986 @view_config(
988 route_name='pullrequest_comments', request_method='POST',
987 route_name='pullrequest_comments', request_method='POST',
989 renderer='string_html', xhr=True)
988 renderer='string_html', xhr=True)
990 def pullrequest_comments(self):
989 def pullrequest_comments(self):
991 self.load_default_context()
990 self.load_default_context()
992
991
993 pull_request = PullRequest.get_or_404(
992 pull_request = PullRequest.get_or_404(
994 self.request.matchdict['pull_request_id'])
993 self.request.matchdict['pull_request_id'])
995 pull_request_id = pull_request.pull_request_id
994 pull_request_id = pull_request.pull_request_id
996 version = self.request.GET.get('version')
995 version = self.request.GET.get('version')
997
996
998 _render = self.request.get_partial_renderer(
997 _render = self.request.get_partial_renderer(
999 'rhodecode:templates/base/sidebar.mako')
998 'rhodecode:templates/base/sidebar.mako')
1000 c = _render.get_call_context()
999 c = _render.get_call_context()
1001
1000
1002 (pull_request_latest,
1001 (pull_request_latest,
1003 pull_request_at_ver,
1002 pull_request_at_ver,
1004 pull_request_display_obj,
1003 pull_request_display_obj,
1005 at_version) = PullRequestModel().get_pr_version(
1004 at_version) = PullRequestModel().get_pr_version(
1006 pull_request_id, version=version)
1005 pull_request_id, version=version)
1007 versions = pull_request_display_obj.versions()
1006 versions = pull_request_display_obj.versions()
1008 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1007 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1009 c.versions = versions + [latest_ver]
1008 c.versions = versions + [latest_ver]
1010
1009
1011 c.at_version = at_version
1010 c.at_version = at_version
1012 c.at_version_num = (at_version
1011 c.at_version_num = (at_version
1013 if at_version and at_version != PullRequest.LATEST_VER
1012 if at_version and at_version != PullRequest.LATEST_VER
1014 else None)
1013 else None)
1015
1014
1016 self.register_comments_vars(c, pull_request_latest, versions)
1015 self.register_comments_vars(c, pull_request_latest, versions)
1017 all_comments = c.inline_comments_flat + c.comments
1016 all_comments = c.inline_comments_flat + c.comments
1018
1017
1019 existing_ids = self._get_existing_ids(self.request.POST)
1018 existing_ids = self._get_existing_ids(self.request.POST)
1020 return _render('comments_table', all_comments, len(all_comments),
1019 return _render('comments_table', all_comments, len(all_comments),
1021 existing_ids=existing_ids)
1020 existing_ids=existing_ids)
1022
1021
1023 @LoginRequired()
1022 @LoginRequired()
1024 @NotAnonymous()
1023 @NotAnonymous()
1025 @HasRepoPermissionAnyDecorator(
1024 @HasRepoPermissionAnyDecorator(
1026 'repository.read', 'repository.write', 'repository.admin')
1025 'repository.read', 'repository.write', 'repository.admin')
1027 @view_config(
1026 @view_config(
1028 route_name='pullrequest_todos', request_method='POST',
1027 route_name='pullrequest_todos', request_method='POST',
1029 renderer='string_html', xhr=True)
1028 renderer='string_html', xhr=True)
1030 def pullrequest_todos(self):
1029 def pullrequest_todos(self):
1031 self.load_default_context()
1030 self.load_default_context()
1032
1031
1033 pull_request = PullRequest.get_or_404(
1032 pull_request = PullRequest.get_or_404(
1034 self.request.matchdict['pull_request_id'])
1033 self.request.matchdict['pull_request_id'])
1035 pull_request_id = pull_request.pull_request_id
1034 pull_request_id = pull_request.pull_request_id
1036 version = self.request.GET.get('version')
1035 version = self.request.GET.get('version')
1037
1036
1038 _render = self.request.get_partial_renderer(
1037 _render = self.request.get_partial_renderer(
1039 'rhodecode:templates/base/sidebar.mako')
1038 'rhodecode:templates/base/sidebar.mako')
1040 c = _render.get_call_context()
1039 c = _render.get_call_context()
1041 (pull_request_latest,
1040 (pull_request_latest,
1042 pull_request_at_ver,
1041 pull_request_at_ver,
1043 pull_request_display_obj,
1042 pull_request_display_obj,
1044 at_version) = PullRequestModel().get_pr_version(
1043 at_version) = PullRequestModel().get_pr_version(
1045 pull_request_id, version=version)
1044 pull_request_id, version=version)
1046 versions = pull_request_display_obj.versions()
1045 versions = pull_request_display_obj.versions()
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1046 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 c.versions = versions + [latest_ver]
1047 c.versions = versions + [latest_ver]
1049
1048
1050 c.at_version = at_version
1049 c.at_version = at_version
1051 c.at_version_num = (at_version
1050 c.at_version_num = (at_version
1052 if at_version and at_version != PullRequest.LATEST_VER
1051 if at_version and at_version != PullRequest.LATEST_VER
1053 else None)
1052 else None)
1054
1053
1055 c.unresolved_comments = CommentsModel() \
1054 c.unresolved_comments = CommentsModel() \
1056 .get_pull_request_unresolved_todos(pull_request)
1055 .get_pull_request_unresolved_todos(pull_request)
1057 c.resolved_comments = CommentsModel() \
1056 c.resolved_comments = CommentsModel() \
1058 .get_pull_request_resolved_todos(pull_request)
1057 .get_pull_request_resolved_todos(pull_request)
1059
1058
1060 all_comments = c.unresolved_comments + c.resolved_comments
1059 all_comments = c.unresolved_comments + c.resolved_comments
1061 existing_ids = self._get_existing_ids(self.request.POST)
1060 existing_ids = self._get_existing_ids(self.request.POST)
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1061 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 todo_comments=True, existing_ids=existing_ids)
1062 todo_comments=True, existing_ids=existing_ids)
1064
1063
1065 @LoginRequired()
1064 @LoginRequired()
1066 @NotAnonymous()
1065 @NotAnonymous()
1067 @HasRepoPermissionAnyDecorator(
1066 @HasRepoPermissionAnyDecorator(
1068 'repository.read', 'repository.write', 'repository.admin')
1067 'repository.read', 'repository.write', 'repository.admin')
1069 @CSRFRequired()
1068 @CSRFRequired()
1070 @view_config(
1069 @view_config(
1071 route_name='pullrequest_create', request_method='POST',
1070 route_name='pullrequest_create', request_method='POST',
1072 renderer=None)
1071 renderer=None)
1073 def pull_request_create(self):
1072 def pull_request_create(self):
1074 _ = self.request.translate
1073 _ = self.request.translate
1075 self.assure_not_empty_repo()
1074 self.assure_not_empty_repo()
1076 self.load_default_context()
1075 self.load_default_context()
1077
1076
1078 controls = peppercorn.parse(self.request.POST.items())
1077 controls = peppercorn.parse(self.request.POST.items())
1079
1078
1080 try:
1079 try:
1081 form = PullRequestForm(
1080 form = PullRequestForm(
1082 self.request.translate, self.db_repo.repo_id)()
1081 self.request.translate, self.db_repo.repo_id)()
1083 _form = form.to_python(controls)
1082 _form = form.to_python(controls)
1084 except formencode.Invalid as errors:
1083 except formencode.Invalid as errors:
1085 if errors.error_dict.get('revisions'):
1084 if errors.error_dict.get('revisions'):
1086 msg = 'Revisions: %s' % errors.error_dict['revisions']
1085 msg = 'Revisions: %s' % errors.error_dict['revisions']
1087 elif errors.error_dict.get('pullrequest_title'):
1086 elif errors.error_dict.get('pullrequest_title'):
1088 msg = errors.error_dict.get('pullrequest_title')
1087 msg = errors.error_dict.get('pullrequest_title')
1089 else:
1088 else:
1090 msg = _('Error creating pull request: {}').format(errors)
1089 msg = _('Error creating pull request: {}').format(errors)
1091 log.exception(msg)
1090 log.exception(msg)
1092 h.flash(msg, 'error')
1091 h.flash(msg, 'error')
1093
1092
1094 # would rather just go back to form ...
1093 # would rather just go back to form ...
1095 raise HTTPFound(
1094 raise HTTPFound(
1096 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1095 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1097
1096
1098 source_repo = _form['source_repo']
1097 source_repo = _form['source_repo']
1099 source_ref = _form['source_ref']
1098 source_ref = _form['source_ref']
1100 target_repo = _form['target_repo']
1099 target_repo = _form['target_repo']
1101 target_ref = _form['target_ref']
1100 target_ref = _form['target_ref']
1102 commit_ids = _form['revisions'][::-1]
1101 commit_ids = _form['revisions'][::-1]
1103 common_ancestor_id = _form['common_ancestor']
1102 common_ancestor_id = _form['common_ancestor']
1104
1103
1105 # find the ancestor for this pr
1104 # find the ancestor for this pr
1106 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1105 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1107 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1106 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1108
1107
1109 if not (source_db_repo or target_db_repo):
1108 if not (source_db_repo or target_db_repo):
1110 h.flash(_('source_repo or target repo not found'), category='error')
1109 h.flash(_('source_repo or target repo not found'), category='error')
1111 raise HTTPFound(
1110 raise HTTPFound(
1112 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1111 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1113
1112
1114 # re-check permissions again here
1113 # re-check permissions again here
1115 # source_repo we must have read permissions
1114 # source_repo we must have read permissions
1116
1115
1117 source_perm = HasRepoPermissionAny(
1116 source_perm = HasRepoPermissionAny(
1118 'repository.read', 'repository.write', 'repository.admin')(
1117 'repository.read', 'repository.write', 'repository.admin')(
1119 source_db_repo.repo_name)
1118 source_db_repo.repo_name)
1120 if not source_perm:
1119 if not source_perm:
1121 msg = _('Not Enough permissions to source repo `{}`.'.format(
1120 msg = _('Not Enough permissions to source repo `{}`.'.format(
1122 source_db_repo.repo_name))
1121 source_db_repo.repo_name))
1123 h.flash(msg, category='error')
1122 h.flash(msg, category='error')
1124 # copy the args back to redirect
1123 # copy the args back to redirect
1125 org_query = self.request.GET.mixed()
1124 org_query = self.request.GET.mixed()
1126 raise HTTPFound(
1125 raise HTTPFound(
1127 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1126 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1128 _query=org_query))
1127 _query=org_query))
1129
1128
1130 # target repo we must have read permissions, and also later on
1129 # target repo we must have read permissions, and also later on
1131 # we want to check branch permissions here
1130 # we want to check branch permissions here
1132 target_perm = HasRepoPermissionAny(
1131 target_perm = HasRepoPermissionAny(
1133 'repository.read', 'repository.write', 'repository.admin')(
1132 'repository.read', 'repository.write', 'repository.admin')(
1134 target_db_repo.repo_name)
1133 target_db_repo.repo_name)
1135 if not target_perm:
1134 if not target_perm:
1136 msg = _('Not Enough permissions to target repo `{}`.'.format(
1135 msg = _('Not Enough permissions to target repo `{}`.'.format(
1137 target_db_repo.repo_name))
1136 target_db_repo.repo_name))
1138 h.flash(msg, category='error')
1137 h.flash(msg, category='error')
1139 # copy the args back to redirect
1138 # copy the args back to redirect
1140 org_query = self.request.GET.mixed()
1139 org_query = self.request.GET.mixed()
1141 raise HTTPFound(
1140 raise HTTPFound(
1142 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1143 _query=org_query))
1142 _query=org_query))
1144
1143
1145 source_scm = source_db_repo.scm_instance()
1144 source_scm = source_db_repo.scm_instance()
1146 target_scm = target_db_repo.scm_instance()
1145 target_scm = target_db_repo.scm_instance()
1147
1146
1148 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1147 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1149 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1148 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1150
1149
1151 ancestor = source_scm.get_common_ancestor(
1150 ancestor = source_scm.get_common_ancestor(
1152 source_commit.raw_id, target_commit.raw_id, target_scm)
1151 source_commit.raw_id, target_commit.raw_id, target_scm)
1153
1152
1154 # recalculate target ref based on ancestor
1153 # recalculate target ref based on ancestor
1155 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1154 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1156 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1155 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1157
1156
1158 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1157 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1159 PullRequestModel().get_reviewer_functions()
1158 PullRequestModel().get_reviewer_functions()
1160
1159
1161 # recalculate reviewers logic, to make sure we can validate this
1160 # recalculate reviewers logic, to make sure we can validate this
1162 reviewer_rules = get_default_reviewers_data(
1161 reviewer_rules = get_default_reviewers_data(
1163 self._rhodecode_db_user, source_db_repo,
1162 self._rhodecode_db_user, source_db_repo,
1164 source_commit, target_db_repo, target_commit)
1163 source_commit, target_db_repo, target_commit)
1165
1164
1166 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1165 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1167 observers = validate_observers(_form['observer_members'], reviewer_rules)
1166 observers = validate_observers(_form['observer_members'], reviewer_rules)
1168
1167
1169 pullrequest_title = _form['pullrequest_title']
1168 pullrequest_title = _form['pullrequest_title']
1170 title_source_ref = source_ref.split(':', 2)[1]
1169 title_source_ref = source_ref.split(':', 2)[1]
1171 if not pullrequest_title:
1170 if not pullrequest_title:
1172 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1171 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1173 source=source_repo,
1172 source=source_repo,
1174 source_ref=title_source_ref,
1173 source_ref=title_source_ref,
1175 target=target_repo
1174 target=target_repo
1176 )
1175 )
1177
1176
1178 description = _form['pullrequest_desc']
1177 description = _form['pullrequest_desc']
1179 description_renderer = _form['description_renderer']
1178 description_renderer = _form['description_renderer']
1180
1179
1181 try:
1180 try:
1182 pull_request = PullRequestModel().create(
1181 pull_request = PullRequestModel().create(
1183 created_by=self._rhodecode_user.user_id,
1182 created_by=self._rhodecode_user.user_id,
1184 source_repo=source_repo,
1183 source_repo=source_repo,
1185 source_ref=source_ref,
1184 source_ref=source_ref,
1186 target_repo=target_repo,
1185 target_repo=target_repo,
1187 target_ref=target_ref,
1186 target_ref=target_ref,
1188 revisions=commit_ids,
1187 revisions=commit_ids,
1189 common_ancestor_id=common_ancestor_id,
1188 common_ancestor_id=common_ancestor_id,
1190 reviewers=reviewers,
1189 reviewers=reviewers,
1191 observers=observers,
1190 observers=observers,
1192 title=pullrequest_title,
1191 title=pullrequest_title,
1193 description=description,
1192 description=description,
1194 description_renderer=description_renderer,
1193 description_renderer=description_renderer,
1195 reviewer_data=reviewer_rules,
1194 reviewer_data=reviewer_rules,
1196 auth_user=self._rhodecode_user
1195 auth_user=self._rhodecode_user
1197 )
1196 )
1198 Session().commit()
1197 Session().commit()
1199
1198
1200 h.flash(_('Successfully opened new pull request'),
1199 h.flash(_('Successfully opened new pull request'),
1201 category='success')
1200 category='success')
1202 except Exception:
1201 except Exception:
1203 msg = _('Error occurred during creation of this pull request.')
1202 msg = _('Error occurred during creation of this pull request.')
1204 log.exception(msg)
1203 log.exception(msg)
1205 h.flash(msg, category='error')
1204 h.flash(msg, category='error')
1206
1205
1207 # copy the args back to redirect
1206 # copy the args back to redirect
1208 org_query = self.request.GET.mixed()
1207 org_query = self.request.GET.mixed()
1209 raise HTTPFound(
1208 raise HTTPFound(
1210 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1209 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1211 _query=org_query))
1210 _query=org_query))
1212
1211
1213 raise HTTPFound(
1212 raise HTTPFound(
1214 h.route_path('pullrequest_show', repo_name=target_repo,
1213 h.route_path('pullrequest_show', repo_name=target_repo,
1215 pull_request_id=pull_request.pull_request_id))
1214 pull_request_id=pull_request.pull_request_id))
1216
1215
1217 @LoginRequired()
1216 @LoginRequired()
1218 @NotAnonymous()
1217 @NotAnonymous()
1219 @HasRepoPermissionAnyDecorator(
1218 @HasRepoPermissionAnyDecorator(
1220 'repository.read', 'repository.write', 'repository.admin')
1219 'repository.read', 'repository.write', 'repository.admin')
1221 @CSRFRequired()
1220 @CSRFRequired()
1222 @view_config(
1221 @view_config(
1223 route_name='pullrequest_update', request_method='POST',
1222 route_name='pullrequest_update', request_method='POST',
1224 renderer='json_ext')
1223 renderer='json_ext')
1225 def pull_request_update(self):
1224 def pull_request_update(self):
1226 pull_request = PullRequest.get_or_404(
1225 pull_request = PullRequest.get_or_404(
1227 self.request.matchdict['pull_request_id'])
1226 self.request.matchdict['pull_request_id'])
1228 _ = self.request.translate
1227 _ = self.request.translate
1229
1228
1230 c = self.load_default_context()
1229 c = self.load_default_context()
1231 redirect_url = None
1230 redirect_url = None
1232
1231
1233 if pull_request.is_closed():
1232 if pull_request.is_closed():
1234 log.debug('update: forbidden because pull request is closed')
1233 log.debug('update: forbidden because pull request is closed')
1235 msg = _(u'Cannot update closed pull requests.')
1234 msg = _(u'Cannot update closed pull requests.')
1236 h.flash(msg, category='error')
1235 h.flash(msg, category='error')
1237 return {'response': True,
1236 return {'response': True,
1238 'redirect_url': redirect_url}
1237 'redirect_url': redirect_url}
1239
1238
1240 is_state_changing = pull_request.is_state_changing()
1239 is_state_changing = pull_request.is_state_changing()
1241 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1240 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1242 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1243
1241
1244 # only owner or admin can update it
1242 # only owner or admin can update it
1245 allowed_to_update = PullRequestModel().check_user_update(
1243 allowed_to_update = PullRequestModel().check_user_update(
1246 pull_request, self._rhodecode_user)
1244 pull_request, self._rhodecode_user)
1247
1245
1248 if allowed_to_update:
1246 if allowed_to_update:
1249 controls = peppercorn.parse(self.request.POST.items())
1247 controls = peppercorn.parse(self.request.POST.items())
1250 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1248 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1251
1249
1252 if 'review_members' in controls:
1250 if 'review_members' in controls:
1253 self._update_reviewers(
1251 self._update_reviewers(
1254 c,
1252 c,
1255 pull_request, controls['review_members'],
1253 pull_request, controls['review_members'],
1256 pull_request.reviewer_data,
1254 pull_request.reviewer_data,
1257 PullRequestReviewers.ROLE_REVIEWER)
1255 PullRequestReviewers.ROLE_REVIEWER)
1258 elif 'observer_members' in controls:
1256 elif 'observer_members' in controls:
1259 self._update_reviewers(
1257 self._update_reviewers(
1260 c,
1258 c,
1261 pull_request, controls['observer_members'],
1259 pull_request, controls['observer_members'],
1262 pull_request.reviewer_data,
1260 pull_request.reviewer_data,
1263 PullRequestReviewers.ROLE_OBSERVER)
1261 PullRequestReviewers.ROLE_OBSERVER)
1264 elif str2bool(self.request.POST.get('update_commits', 'false')):
1262 elif str2bool(self.request.POST.get('update_commits', 'false')):
1265 if is_state_changing:
1263 if is_state_changing:
1266 log.debug('commits update: forbidden because pull request is in state %s',
1264 log.debug('commits update: forbidden because pull request is in state %s',
1267 pull_request.pull_request_state)
1265 pull_request.pull_request_state)
1268 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1266 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1269 u'Current state is: `{}`').format(
1267 u'Current state is: `{}`').format(
1270 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1268 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1271 h.flash(msg, category='error')
1269 h.flash(msg, category='error')
1272 return {'response': True,
1270 return {'response': True,
1273 'redirect_url': redirect_url}
1271 'redirect_url': redirect_url}
1274
1272
1275 self._update_commits(c, pull_request)
1273 self._update_commits(c, pull_request)
1276 if force_refresh:
1274 if force_refresh:
1277 redirect_url = h.route_path(
1275 redirect_url = h.route_path(
1278 'pullrequest_show', repo_name=self.db_repo_name,
1276 'pullrequest_show', repo_name=self.db_repo_name,
1279 pull_request_id=pull_request.pull_request_id,
1277 pull_request_id=pull_request.pull_request_id,
1280 _query={"force_refresh": 1})
1278 _query={"force_refresh": 1})
1281 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1279 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1282 self._edit_pull_request(pull_request)
1280 self._edit_pull_request(pull_request)
1283 else:
1281 else:
1284 log.error('Unhandled update data.')
1282 log.error('Unhandled update data.')
1285 raise HTTPBadRequest()
1283 raise HTTPBadRequest()
1286
1284
1287 return {'response': True,
1285 return {'response': True,
1288 'redirect_url': redirect_url}
1286 'redirect_url': redirect_url}
1289 raise HTTPForbidden()
1287 raise HTTPForbidden()
1290
1288
1291 def _edit_pull_request(self, pull_request):
1289 def _edit_pull_request(self, pull_request):
1292 """
1290 """
1293 Edit title and description
1291 Edit title and description
1294 """
1292 """
1295 _ = self.request.translate
1293 _ = self.request.translate
1296
1294
1297 try:
1295 try:
1298 PullRequestModel().edit(
1296 PullRequestModel().edit(
1299 pull_request,
1297 pull_request,
1300 self.request.POST.get('title'),
1298 self.request.POST.get('title'),
1301 self.request.POST.get('description'),
1299 self.request.POST.get('description'),
1302 self.request.POST.get('description_renderer'),
1300 self.request.POST.get('description_renderer'),
1303 self._rhodecode_user)
1301 self._rhodecode_user)
1304 except ValueError:
1302 except ValueError:
1305 msg = _(u'Cannot update closed pull requests.')
1303 msg = _(u'Cannot update closed pull requests.')
1306 h.flash(msg, category='error')
1304 h.flash(msg, category='error')
1307 return
1305 return
1308 else:
1306 else:
1309 Session().commit()
1307 Session().commit()
1310
1308
1311 msg = _(u'Pull request title & description updated.')
1309 msg = _(u'Pull request title & description updated.')
1312 h.flash(msg, category='success')
1310 h.flash(msg, category='success')
1313 return
1311 return
1314
1312
1315 def _update_commits(self, c, pull_request):
1313 def _update_commits(self, c, pull_request):
1316 _ = self.request.translate
1314 _ = self.request.translate
1317
1315
1318 with pull_request.set_state(PullRequest.STATE_UPDATING):
1316 with pull_request.set_state(PullRequest.STATE_UPDATING):
1319 resp = PullRequestModel().update_commits(
1317 resp = PullRequestModel().update_commits(
1320 pull_request, self._rhodecode_db_user)
1318 pull_request, self._rhodecode_db_user)
1321
1319
1322 if resp.executed:
1320 if resp.executed:
1323
1321
1324 if resp.target_changed and resp.source_changed:
1322 if resp.target_changed and resp.source_changed:
1325 changed = 'target and source repositories'
1323 changed = 'target and source repositories'
1326 elif resp.target_changed and not resp.source_changed:
1324 elif resp.target_changed and not resp.source_changed:
1327 changed = 'target repository'
1325 changed = 'target repository'
1328 elif not resp.target_changed and resp.source_changed:
1326 elif not resp.target_changed and resp.source_changed:
1329 changed = 'source repository'
1327 changed = 'source repository'
1330 else:
1328 else:
1331 changed = 'nothing'
1329 changed = 'nothing'
1332
1330
1333 msg = _(u'Pull request updated to "{source_commit_id}" with '
1331 msg = _(u'Pull request updated to "{source_commit_id}" with '
1334 u'{count_added} added, {count_removed} removed commits. '
1332 u'{count_added} added, {count_removed} removed commits. '
1335 u'Source of changes: {change_source}.')
1333 u'Source of changes: {change_source}.')
1336 msg = msg.format(
1334 msg = msg.format(
1337 source_commit_id=pull_request.source_ref_parts.commit_id,
1335 source_commit_id=pull_request.source_ref_parts.commit_id,
1338 count_added=len(resp.changes.added),
1336 count_added=len(resp.changes.added),
1339 count_removed=len(resp.changes.removed),
1337 count_removed=len(resp.changes.removed),
1340 change_source=changed)
1338 change_source=changed)
1341 h.flash(msg, category='success')
1339 h.flash(msg, category='success')
1342 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1340 channelstream.pr_update_channelstream_push(
1341 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1343 else:
1342 else:
1344 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1343 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1345 warning_reasons = [
1344 warning_reasons = [
1346 UpdateFailureReason.NO_CHANGE,
1345 UpdateFailureReason.NO_CHANGE,
1347 UpdateFailureReason.WRONG_REF_TYPE,
1346 UpdateFailureReason.WRONG_REF_TYPE,
1348 ]
1347 ]
1349 category = 'warning' if resp.reason in warning_reasons else 'error'
1348 category = 'warning' if resp.reason in warning_reasons else 'error'
1350 h.flash(msg, category=category)
1349 h.flash(msg, category=category)
1351
1350
1352 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1351 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1353 _ = self.request.translate
1352 _ = self.request.translate
1354
1353
1355 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1354 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1356 PullRequestModel().get_reviewer_functions()
1355 PullRequestModel().get_reviewer_functions()
1357
1356
1358 if role == PullRequestReviewers.ROLE_REVIEWER:
1357 if role == PullRequestReviewers.ROLE_REVIEWER:
1359 try:
1358 try:
1360 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1359 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1361 except ValueError as e:
1360 except ValueError as e:
1362 log.error('Reviewers Validation: {}'.format(e))
1361 log.error('Reviewers Validation: {}'.format(e))
1363 h.flash(e, category='error')
1362 h.flash(e, category='error')
1364 return
1363 return
1365
1364
1366 old_calculated_status = pull_request.calculated_review_status()
1365 old_calculated_status = pull_request.calculated_review_status()
1367 PullRequestModel().update_reviewers(
1366 PullRequestModel().update_reviewers(
1368 pull_request, reviewers, self._rhodecode_user)
1367 pull_request, reviewers, self._rhodecode_user)
1369
1368
1370 Session().commit()
1369 Session().commit()
1371
1370
1372 msg = _('Pull request reviewers updated.')
1371 msg = _('Pull request reviewers updated.')
1373 h.flash(msg, category='success')
1372 h.flash(msg, category='success')
1374 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1373 channelstream.pr_update_channelstream_push(
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1375
1375
1376 # trigger status changed if change in reviewers changes the status
1376 # trigger status changed if change in reviewers changes the status
1377 calculated_status = pull_request.calculated_review_status()
1377 calculated_status = pull_request.calculated_review_status()
1378 if old_calculated_status != calculated_status:
1378 if old_calculated_status != calculated_status:
1379 PullRequestModel().trigger_pull_request_hook(
1379 PullRequestModel().trigger_pull_request_hook(
1380 pull_request, self._rhodecode_user, 'review_status_change',
1380 pull_request, self._rhodecode_user, 'review_status_change',
1381 data={'status': calculated_status})
1381 data={'status': calculated_status})
1382
1382
1383 elif role == PullRequestReviewers.ROLE_OBSERVER:
1383 elif role == PullRequestReviewers.ROLE_OBSERVER:
1384 try:
1384 try:
1385 observers = validate_observers(review_members, reviewer_rules)
1385 observers = validate_observers(review_members, reviewer_rules)
1386 except ValueError as e:
1386 except ValueError as e:
1387 log.error('Observers Validation: {}'.format(e))
1387 log.error('Observers Validation: {}'.format(e))
1388 h.flash(e, category='error')
1388 h.flash(e, category='error')
1389 return
1389 return
1390
1390
1391 PullRequestModel().update_observers(
1391 PullRequestModel().update_observers(
1392 pull_request, observers, self._rhodecode_user)
1392 pull_request, observers, self._rhodecode_user)
1393
1393
1394 Session().commit()
1394 Session().commit()
1395 msg = _('Pull request observers updated.')
1395 msg = _('Pull request observers updated.')
1396 h.flash(msg, category='success')
1396 h.flash(msg, category='success')
1397 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1397 channelstream.pr_update_channelstream_push(
1398 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1398
1399
1399 @LoginRequired()
1400 @LoginRequired()
1400 @NotAnonymous()
1401 @NotAnonymous()
1401 @HasRepoPermissionAnyDecorator(
1402 @HasRepoPermissionAnyDecorator(
1402 'repository.read', 'repository.write', 'repository.admin')
1403 'repository.read', 'repository.write', 'repository.admin')
1403 @CSRFRequired()
1404 @CSRFRequired()
1404 @view_config(
1405 @view_config(
1405 route_name='pullrequest_merge', request_method='POST',
1406 route_name='pullrequest_merge', request_method='POST',
1406 renderer='json_ext')
1407 renderer='json_ext')
1407 def pull_request_merge(self):
1408 def pull_request_merge(self):
1408 """
1409 """
1409 Merge will perform a server-side merge of the specified
1410 Merge will perform a server-side merge of the specified
1410 pull request, if the pull request is approved and mergeable.
1411 pull request, if the pull request is approved and mergeable.
1411 After successful merging, the pull request is automatically
1412 After successful merging, the pull request is automatically
1412 closed, with a relevant comment.
1413 closed, with a relevant comment.
1413 """
1414 """
1414 pull_request = PullRequest.get_or_404(
1415 pull_request = PullRequest.get_or_404(
1415 self.request.matchdict['pull_request_id'])
1416 self.request.matchdict['pull_request_id'])
1416 _ = self.request.translate
1417 _ = self.request.translate
1417
1418
1418 if pull_request.is_state_changing():
1419 if pull_request.is_state_changing():
1419 log.debug('show: forbidden because pull request is in state %s',
1420 log.debug('show: forbidden because pull request is in state %s',
1420 pull_request.pull_request_state)
1421 pull_request.pull_request_state)
1421 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1422 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1422 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1423 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1423 pull_request.pull_request_state)
1424 pull_request.pull_request_state)
1424 h.flash(msg, category='error')
1425 h.flash(msg, category='error')
1425 raise HTTPFound(
1426 raise HTTPFound(
1426 h.route_path('pullrequest_show',
1427 h.route_path('pullrequest_show',
1427 repo_name=pull_request.target_repo.repo_name,
1428 repo_name=pull_request.target_repo.repo_name,
1428 pull_request_id=pull_request.pull_request_id))
1429 pull_request_id=pull_request.pull_request_id))
1429
1430
1430 self.load_default_context()
1431 self.load_default_context()
1431
1432
1432 with pull_request.set_state(PullRequest.STATE_UPDATING):
1433 with pull_request.set_state(PullRequest.STATE_UPDATING):
1433 check = MergeCheck.validate(
1434 check = MergeCheck.validate(
1434 pull_request, auth_user=self._rhodecode_user,
1435 pull_request, auth_user=self._rhodecode_user,
1435 translator=self.request.translate)
1436 translator=self.request.translate)
1436 merge_possible = not check.failed
1437 merge_possible = not check.failed
1437
1438
1438 for err_type, error_msg in check.errors:
1439 for err_type, error_msg in check.errors:
1439 h.flash(error_msg, category=err_type)
1440 h.flash(error_msg, category=err_type)
1440
1441
1441 if merge_possible:
1442 if merge_possible:
1442 log.debug("Pre-conditions checked, trying to merge.")
1443 log.debug("Pre-conditions checked, trying to merge.")
1443 extras = vcs_operation_context(
1444 extras = vcs_operation_context(
1444 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1445 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1445 username=self._rhodecode_db_user.username, action='push',
1446 username=self._rhodecode_db_user.username, action='push',
1446 scm=pull_request.target_repo.repo_type)
1447 scm=pull_request.target_repo.repo_type)
1447 with pull_request.set_state(PullRequest.STATE_UPDATING):
1448 with pull_request.set_state(PullRequest.STATE_UPDATING):
1448 self._merge_pull_request(
1449 self._merge_pull_request(
1449 pull_request, self._rhodecode_db_user, extras)
1450 pull_request, self._rhodecode_db_user, extras)
1450 else:
1451 else:
1451 log.debug("Pre-conditions failed, NOT merging.")
1452 log.debug("Pre-conditions failed, NOT merging.")
1452
1453
1453 raise HTTPFound(
1454 raise HTTPFound(
1454 h.route_path('pullrequest_show',
1455 h.route_path('pullrequest_show',
1455 repo_name=pull_request.target_repo.repo_name,
1456 repo_name=pull_request.target_repo.repo_name,
1456 pull_request_id=pull_request.pull_request_id))
1457 pull_request_id=pull_request.pull_request_id))
1457
1458
1458 def _merge_pull_request(self, pull_request, user, extras):
1459 def _merge_pull_request(self, pull_request, user, extras):
1459 _ = self.request.translate
1460 _ = self.request.translate
1460 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1461 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1461
1462
1462 if merge_resp.executed:
1463 if merge_resp.executed:
1463 log.debug("The merge was successful, closing the pull request.")
1464 log.debug("The merge was successful, closing the pull request.")
1464 PullRequestModel().close_pull_request(
1465 PullRequestModel().close_pull_request(
1465 pull_request.pull_request_id, user)
1466 pull_request.pull_request_id, user)
1466 Session().commit()
1467 Session().commit()
1467 msg = _('Pull request was successfully merged and closed.')
1468 msg = _('Pull request was successfully merged and closed.')
1468 h.flash(msg, category='success')
1469 h.flash(msg, category='success')
1469 else:
1470 else:
1470 log.debug(
1471 log.debug(
1471 "The merge was not successful. Merge response: %s", merge_resp)
1472 "The merge was not successful. Merge response: %s", merge_resp)
1472 msg = merge_resp.merge_status_message
1473 msg = merge_resp.merge_status_message
1473 h.flash(msg, category='error')
1474 h.flash(msg, category='error')
1474
1475
1475 @LoginRequired()
1476 @LoginRequired()
1476 @NotAnonymous()
1477 @NotAnonymous()
1477 @HasRepoPermissionAnyDecorator(
1478 @HasRepoPermissionAnyDecorator(
1478 'repository.read', 'repository.write', 'repository.admin')
1479 'repository.read', 'repository.write', 'repository.admin')
1479 @CSRFRequired()
1480 @CSRFRequired()
1480 @view_config(
1481 @view_config(
1481 route_name='pullrequest_delete', request_method='POST',
1482 route_name='pullrequest_delete', request_method='POST',
1482 renderer='json_ext')
1483 renderer='json_ext')
1483 def pull_request_delete(self):
1484 def pull_request_delete(self):
1484 _ = self.request.translate
1485 _ = self.request.translate
1485
1486
1486 pull_request = PullRequest.get_or_404(
1487 pull_request = PullRequest.get_or_404(
1487 self.request.matchdict['pull_request_id'])
1488 self.request.matchdict['pull_request_id'])
1488 self.load_default_context()
1489 self.load_default_context()
1489
1490
1490 pr_closed = pull_request.is_closed()
1491 pr_closed = pull_request.is_closed()
1491 allowed_to_delete = PullRequestModel().check_user_delete(
1492 allowed_to_delete = PullRequestModel().check_user_delete(
1492 pull_request, self._rhodecode_user) and not pr_closed
1493 pull_request, self._rhodecode_user) and not pr_closed
1493
1494
1494 # only owner can delete it !
1495 # only owner can delete it !
1495 if allowed_to_delete:
1496 if allowed_to_delete:
1496 PullRequestModel().delete(pull_request, self._rhodecode_user)
1497 PullRequestModel().delete(pull_request, self._rhodecode_user)
1497 Session().commit()
1498 Session().commit()
1498 h.flash(_('Successfully deleted pull request'),
1499 h.flash(_('Successfully deleted pull request'),
1499 category='success')
1500 category='success')
1500 raise HTTPFound(h.route_path('pullrequest_show_all',
1501 raise HTTPFound(h.route_path('pullrequest_show_all',
1501 repo_name=self.db_repo_name))
1502 repo_name=self.db_repo_name))
1502
1503
1503 log.warning('user %s tried to delete pull request without access',
1504 log.warning('user %s tried to delete pull request without access',
1504 self._rhodecode_user)
1505 self._rhodecode_user)
1505 raise HTTPNotFound()
1506 raise HTTPNotFound()
1506
1507
1507 @LoginRequired()
1508 @LoginRequired()
1508 @NotAnonymous()
1509 @NotAnonymous()
1509 @HasRepoPermissionAnyDecorator(
1510 @HasRepoPermissionAnyDecorator(
1510 'repository.read', 'repository.write', 'repository.admin')
1511 'repository.read', 'repository.write', 'repository.admin')
1511 @CSRFRequired()
1512 @CSRFRequired()
1512 @view_config(
1513 @view_config(
1513 route_name='pullrequest_comment_create', request_method='POST',
1514 route_name='pullrequest_comment_create', request_method='POST',
1514 renderer='json_ext')
1515 renderer='json_ext')
1515 def pull_request_comment_create(self):
1516 def pull_request_comment_create(self):
1516 _ = self.request.translate
1517 _ = self.request.translate
1517
1518
1518 pull_request = PullRequest.get_or_404(
1519 pull_request = PullRequest.get_or_404(
1519 self.request.matchdict['pull_request_id'])
1520 self.request.matchdict['pull_request_id'])
1520 pull_request_id = pull_request.pull_request_id
1521 pull_request_id = pull_request.pull_request_id
1521
1522
1522 if pull_request.is_closed():
1523 if pull_request.is_closed():
1523 log.debug('comment: forbidden because pull request is closed')
1524 log.debug('comment: forbidden because pull request is closed')
1524 raise HTTPForbidden()
1525 raise HTTPForbidden()
1525
1526
1526 allowed_to_comment = PullRequestModel().check_user_comment(
1527 allowed_to_comment = PullRequestModel().check_user_comment(
1527 pull_request, self._rhodecode_user)
1528 pull_request, self._rhodecode_user)
1528 if not allowed_to_comment:
1529 if not allowed_to_comment:
1529 log.debug('comment: forbidden because pull request is from forbidden repo')
1530 log.debug('comment: forbidden because pull request is from forbidden repo')
1530 raise HTTPForbidden()
1531 raise HTTPForbidden()
1531
1532
1532 c = self.load_default_context()
1533 c = self.load_default_context()
1533
1534
1534 status = self.request.POST.get('changeset_status', None)
1535 status = self.request.POST.get('changeset_status', None)
1535 text = self.request.POST.get('text')
1536 text = self.request.POST.get('text')
1536 comment_type = self.request.POST.get('comment_type')
1537 comment_type = self.request.POST.get('comment_type')
1537 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1538 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1538 close_pull_request = self.request.POST.get('close_pull_request')
1539 close_pull_request = self.request.POST.get('close_pull_request')
1539
1540
1540 # the logic here should work like following, if we submit close
1541 # the logic here should work like following, if we submit close
1541 # pr comment, use `close_pull_request_with_comment` function
1542 # pr comment, use `close_pull_request_with_comment` function
1542 # else handle regular comment logic
1543 # else handle regular comment logic
1543
1544
1544 if close_pull_request:
1545 if close_pull_request:
1545 # only owner or admin or person with write permissions
1546 # only owner or admin or person with write permissions
1546 allowed_to_close = PullRequestModel().check_user_update(
1547 allowed_to_close = PullRequestModel().check_user_update(
1547 pull_request, self._rhodecode_user)
1548 pull_request, self._rhodecode_user)
1548 if not allowed_to_close:
1549 if not allowed_to_close:
1549 log.debug('comment: forbidden because not allowed to close '
1550 log.debug('comment: forbidden because not allowed to close '
1550 'pull request %s', pull_request_id)
1551 'pull request %s', pull_request_id)
1551 raise HTTPForbidden()
1552 raise HTTPForbidden()
1552
1553
1553 # This also triggers `review_status_change`
1554 # This also triggers `review_status_change`
1554 comment, status = PullRequestModel().close_pull_request_with_comment(
1555 comment, status = PullRequestModel().close_pull_request_with_comment(
1555 pull_request, self._rhodecode_user, self.db_repo, message=text,
1556 pull_request, self._rhodecode_user, self.db_repo, message=text,
1556 auth_user=self._rhodecode_user)
1557 auth_user=self._rhodecode_user)
1557 Session().flush()
1558 Session().flush()
1558
1559
1559 PullRequestModel().trigger_pull_request_hook(
1560 PullRequestModel().trigger_pull_request_hook(
1560 pull_request, self._rhodecode_user, 'comment',
1561 pull_request, self._rhodecode_user, 'comment',
1561 data={'comment': comment})
1562 data={'comment': comment})
1562
1563
1563 else:
1564 else:
1564 # regular comment case, could be inline, or one with status.
1565 # regular comment case, could be inline, or one with status.
1565 # for that one we check also permissions
1566 # for that one we check also permissions
1566
1567
1567 allowed_to_change_status = PullRequestModel().check_user_change_status(
1568 allowed_to_change_status = PullRequestModel().check_user_change_status(
1568 pull_request, self._rhodecode_user)
1569 pull_request, self._rhodecode_user)
1569
1570
1570 if status and allowed_to_change_status:
1571 if status and allowed_to_change_status:
1571 message = (_('Status change %(transition_icon)s %(status)s')
1572 message = (_('Status change %(transition_icon)s %(status)s')
1572 % {'transition_icon': '>',
1573 % {'transition_icon': '>',
1573 'status': ChangesetStatus.get_status_lbl(status)})
1574 'status': ChangesetStatus.get_status_lbl(status)})
1574 text = text or message
1575 text = text or message
1575
1576
1576 comment = CommentsModel().create(
1577 comment = CommentsModel().create(
1577 text=text,
1578 text=text,
1578 repo=self.db_repo.repo_id,
1579 repo=self.db_repo.repo_id,
1579 user=self._rhodecode_user.user_id,
1580 user=self._rhodecode_user.user_id,
1580 pull_request=pull_request,
1581 pull_request=pull_request,
1581 f_path=self.request.POST.get('f_path'),
1582 f_path=self.request.POST.get('f_path'),
1582 line_no=self.request.POST.get('line'),
1583 line_no=self.request.POST.get('line'),
1583 status_change=(ChangesetStatus.get_status_lbl(status)
1584 status_change=(ChangesetStatus.get_status_lbl(status)
1584 if status and allowed_to_change_status else None),
1585 if status and allowed_to_change_status else None),
1585 status_change_type=(status
1586 status_change_type=(status
1586 if status and allowed_to_change_status else None),
1587 if status and allowed_to_change_status else None),
1587 comment_type=comment_type,
1588 comment_type=comment_type,
1588 resolves_comment_id=resolves_comment_id,
1589 resolves_comment_id=resolves_comment_id,
1589 auth_user=self._rhodecode_user
1590 auth_user=self._rhodecode_user
1590 )
1591 )
1592 is_inline = bool(comment.f_path and comment.line_no)
1591
1593
1592 if allowed_to_change_status:
1594 if allowed_to_change_status:
1593 # calculate old status before we change it
1595 # calculate old status before we change it
1594 old_calculated_status = pull_request.calculated_review_status()
1596 old_calculated_status = pull_request.calculated_review_status()
1595
1597
1596 # get status if set !
1598 # get status if set !
1597 if status:
1599 if status:
1598 ChangesetStatusModel().set_status(
1600 ChangesetStatusModel().set_status(
1599 self.db_repo.repo_id,
1601 self.db_repo.repo_id,
1600 status,
1602 status,
1601 self._rhodecode_user.user_id,
1603 self._rhodecode_user.user_id,
1602 comment,
1604 comment,
1603 pull_request=pull_request
1605 pull_request=pull_request
1604 )
1606 )
1605
1607
1606 Session().flush()
1608 Session().flush()
1607 # this is somehow required to get access to some relationship
1609 # this is somehow required to get access to some relationship
1608 # loaded on comment
1610 # loaded on comment
1609 Session().refresh(comment)
1611 Session().refresh(comment)
1610
1612
1611 PullRequestModel().trigger_pull_request_hook(
1613 PullRequestModel().trigger_pull_request_hook(
1612 pull_request, self._rhodecode_user, 'comment',
1614 pull_request, self._rhodecode_user, 'comment',
1613 data={'comment': comment})
1615 data={'comment': comment})
1614
1616
1615 # we now calculate the status of pull request, and based on that
1617 # we now calculate the status of pull request, and based on that
1616 # calculation we set the commits status
1618 # calculation we set the commits status
1617 calculated_status = pull_request.calculated_review_status()
1619 calculated_status = pull_request.calculated_review_status()
1618 if old_calculated_status != calculated_status:
1620 if old_calculated_status != calculated_status:
1619 PullRequestModel().trigger_pull_request_hook(
1621 PullRequestModel().trigger_pull_request_hook(
1620 pull_request, self._rhodecode_user, 'review_status_change',
1622 pull_request, self._rhodecode_user, 'review_status_change',
1621 data={'status': calculated_status})
1623 data={'status': calculated_status})
1622
1624
1623 Session().commit()
1625 Session().commit()
1624
1626
1625 data = {
1627 data = {
1626 'target_id': h.safeid(h.safe_unicode(
1628 'target_id': h.safeid(h.safe_unicode(
1627 self.request.POST.get('f_path'))),
1629 self.request.POST.get('f_path'))),
1628 }
1630 }
1629 if comment:
1631 if comment:
1630 c.co = comment
1632 c.co = comment
1631 c.at_version_num = None
1633 c.at_version_num = None
1632 rendered_comment = render(
1634 rendered_comment = render(
1633 'rhodecode:templates/changeset/changeset_comment_block.mako',
1635 'rhodecode:templates/changeset/changeset_comment_block.mako',
1634 self._get_template_context(c), self.request)
1636 self._get_template_context(c), self.request)
1635
1637
1636 data.update(comment.get_dict())
1638 data.update(comment.get_dict())
1637 data.update({'rendered_text': rendered_comment})
1639 data.update({'rendered_text': rendered_comment})
1638
1640
1641 comment_broadcast_channel = channelstream.comment_channel(
1642 self.db_repo_name, pull_request_obj=pull_request)
1643
1644 comment_data = data
1645 comment_type = 'inline' if is_inline else 'general'
1646 channelstream.comment_channelstream_push(
1647 self.request, comment_broadcast_channel, self._rhodecode_user,
1648 _('posted a new {} comment').format(comment_type),
1649 comment_data=comment_data)
1650
1639 return data
1651 return data
1640
1652
1641 @LoginRequired()
1653 @LoginRequired()
1642 @NotAnonymous()
1654 @NotAnonymous()
1643 @HasRepoPermissionAnyDecorator(
1655 @HasRepoPermissionAnyDecorator(
1644 'repository.read', 'repository.write', 'repository.admin')
1656 'repository.read', 'repository.write', 'repository.admin')
1645 @CSRFRequired()
1657 @CSRFRequired()
1646 @view_config(
1658 @view_config(
1647 route_name='pullrequest_comment_delete', request_method='POST',
1659 route_name='pullrequest_comment_delete', request_method='POST',
1648 renderer='json_ext')
1660 renderer='json_ext')
1649 def pull_request_comment_delete(self):
1661 def pull_request_comment_delete(self):
1650 pull_request = PullRequest.get_or_404(
1662 pull_request = PullRequest.get_or_404(
1651 self.request.matchdict['pull_request_id'])
1663 self.request.matchdict['pull_request_id'])
1652
1664
1653 comment = ChangesetComment.get_or_404(
1665 comment = ChangesetComment.get_or_404(
1654 self.request.matchdict['comment_id'])
1666 self.request.matchdict['comment_id'])
1655 comment_id = comment.comment_id
1667 comment_id = comment.comment_id
1656
1668
1657 if comment.immutable:
1669 if comment.immutable:
1658 # don't allow deleting comments that are immutable
1670 # don't allow deleting comments that are immutable
1659 raise HTTPForbidden()
1671 raise HTTPForbidden()
1660
1672
1661 if pull_request.is_closed():
1673 if pull_request.is_closed():
1662 log.debug('comment: forbidden because pull request is closed')
1674 log.debug('comment: forbidden because pull request is closed')
1663 raise HTTPForbidden()
1675 raise HTTPForbidden()
1664
1676
1665 if not comment:
1677 if not comment:
1666 log.debug('Comment with id:%s not found, skipping', comment_id)
1678 log.debug('Comment with id:%s not found, skipping', comment_id)
1667 # comment already deleted in another call probably
1679 # comment already deleted in another call probably
1668 return True
1680 return True
1669
1681
1670 if comment.pull_request.is_closed():
1682 if comment.pull_request.is_closed():
1671 # don't allow deleting comments on closed pull request
1683 # don't allow deleting comments on closed pull request
1672 raise HTTPForbidden()
1684 raise HTTPForbidden()
1673
1685
1674 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1686 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1675 super_admin = h.HasPermissionAny('hg.admin')()
1687 super_admin = h.HasPermissionAny('hg.admin')()
1676 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1688 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1677 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1689 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1678 comment_repo_admin = is_repo_admin and is_repo_comment
1690 comment_repo_admin = is_repo_admin and is_repo_comment
1679
1691
1680 if super_admin or comment_owner or comment_repo_admin:
1692 if super_admin or comment_owner or comment_repo_admin:
1681 old_calculated_status = comment.pull_request.calculated_review_status()
1693 old_calculated_status = comment.pull_request.calculated_review_status()
1682 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1694 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1683 Session().commit()
1695 Session().commit()
1684 calculated_status = comment.pull_request.calculated_review_status()
1696 calculated_status = comment.pull_request.calculated_review_status()
1685 if old_calculated_status != calculated_status:
1697 if old_calculated_status != calculated_status:
1686 PullRequestModel().trigger_pull_request_hook(
1698 PullRequestModel().trigger_pull_request_hook(
1687 comment.pull_request, self._rhodecode_user, 'review_status_change',
1699 comment.pull_request, self._rhodecode_user, 'review_status_change',
1688 data={'status': calculated_status})
1700 data={'status': calculated_status})
1689 return True
1701 return True
1690 else:
1702 else:
1691 log.warning('No permissions for user %s to delete comment_id: %s',
1703 log.warning('No permissions for user %s to delete comment_id: %s',
1692 self._rhodecode_db_user, comment_id)
1704 self._rhodecode_db_user, comment_id)
1693 raise HTTPNotFound()
1705 raise HTTPNotFound()
1694
1706
1695 @LoginRequired()
1707 @LoginRequired()
1696 @NotAnonymous()
1708 @NotAnonymous()
1697 @HasRepoPermissionAnyDecorator(
1709 @HasRepoPermissionAnyDecorator(
1698 'repository.read', 'repository.write', 'repository.admin')
1710 'repository.read', 'repository.write', 'repository.admin')
1699 @CSRFRequired()
1711 @CSRFRequired()
1700 @view_config(
1712 @view_config(
1701 route_name='pullrequest_comment_edit', request_method='POST',
1713 route_name='pullrequest_comment_edit', request_method='POST',
1702 renderer='json_ext')
1714 renderer='json_ext')
1703 def pull_request_comment_edit(self):
1715 def pull_request_comment_edit(self):
1704 self.load_default_context()
1716 self.load_default_context()
1705
1717
1706 pull_request = PullRequest.get_or_404(
1718 pull_request = PullRequest.get_or_404(
1707 self.request.matchdict['pull_request_id']
1719 self.request.matchdict['pull_request_id']
1708 )
1720 )
1709 comment = ChangesetComment.get_or_404(
1721 comment = ChangesetComment.get_or_404(
1710 self.request.matchdict['comment_id']
1722 self.request.matchdict['comment_id']
1711 )
1723 )
1712 comment_id = comment.comment_id
1724 comment_id = comment.comment_id
1713
1725
1714 if comment.immutable:
1726 if comment.immutable:
1715 # don't allow deleting comments that are immutable
1727 # don't allow deleting comments that are immutable
1716 raise HTTPForbidden()
1728 raise HTTPForbidden()
1717
1729
1718 if pull_request.is_closed():
1730 if pull_request.is_closed():
1719 log.debug('comment: forbidden because pull request is closed')
1731 log.debug('comment: forbidden because pull request is closed')
1720 raise HTTPForbidden()
1732 raise HTTPForbidden()
1721
1733
1722 if not comment:
1734 if not comment:
1723 log.debug('Comment with id:%s not found, skipping', comment_id)
1735 log.debug('Comment with id:%s not found, skipping', comment_id)
1724 # comment already deleted in another call probably
1736 # comment already deleted in another call probably
1725 return True
1737 return True
1726
1738
1727 if comment.pull_request.is_closed():
1739 if comment.pull_request.is_closed():
1728 # don't allow deleting comments on closed pull request
1740 # don't allow deleting comments on closed pull request
1729 raise HTTPForbidden()
1741 raise HTTPForbidden()
1730
1742
1731 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1743 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1732 super_admin = h.HasPermissionAny('hg.admin')()
1744 super_admin = h.HasPermissionAny('hg.admin')()
1733 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1745 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1734 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1746 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1735 comment_repo_admin = is_repo_admin and is_repo_comment
1747 comment_repo_admin = is_repo_admin and is_repo_comment
1736
1748
1737 if super_admin or comment_owner or comment_repo_admin:
1749 if super_admin or comment_owner or comment_repo_admin:
1738 text = self.request.POST.get('text')
1750 text = self.request.POST.get('text')
1739 version = self.request.POST.get('version')
1751 version = self.request.POST.get('version')
1740 if text == comment.text:
1752 if text == comment.text:
1741 log.warning(
1753 log.warning(
1742 'Comment(PR): '
1754 'Comment(PR): '
1743 'Trying to create new version '
1755 'Trying to create new version '
1744 'with the same comment body {}'.format(
1756 'with the same comment body {}'.format(
1745 comment_id,
1757 comment_id,
1746 )
1758 )
1747 )
1759 )
1748 raise HTTPNotFound()
1760 raise HTTPNotFound()
1749
1761
1750 if version.isdigit():
1762 if version.isdigit():
1751 version = int(version)
1763 version = int(version)
1752 else:
1764 else:
1753 log.warning(
1765 log.warning(
1754 'Comment(PR): Wrong version type {} {} '
1766 'Comment(PR): Wrong version type {} {} '
1755 'for comment {}'.format(
1767 'for comment {}'.format(
1756 version,
1768 version,
1757 type(version),
1769 type(version),
1758 comment_id,
1770 comment_id,
1759 )
1771 )
1760 )
1772 )
1761 raise HTTPNotFound()
1773 raise HTTPNotFound()
1762
1774
1763 try:
1775 try:
1764 comment_history = CommentsModel().edit(
1776 comment_history = CommentsModel().edit(
1765 comment_id=comment_id,
1777 comment_id=comment_id,
1766 text=text,
1778 text=text,
1767 auth_user=self._rhodecode_user,
1779 auth_user=self._rhodecode_user,
1768 version=version,
1780 version=version,
1769 )
1781 )
1770 except CommentVersionMismatch:
1782 except CommentVersionMismatch:
1771 raise HTTPConflict()
1783 raise HTTPConflict()
1772
1784
1773 if not comment_history:
1785 if not comment_history:
1774 raise HTTPNotFound()
1786 raise HTTPNotFound()
1775
1787
1776 Session().commit()
1788 Session().commit()
1777
1789
1778 PullRequestModel().trigger_pull_request_hook(
1790 PullRequestModel().trigger_pull_request_hook(
1779 pull_request, self._rhodecode_user, 'comment_edit',
1791 pull_request, self._rhodecode_user, 'comment_edit',
1780 data={'comment': comment})
1792 data={'comment': comment})
1781
1793
1782 return {
1794 return {
1783 'comment_history_id': comment_history.comment_history_id,
1795 'comment_history_id': comment_history.comment_history_id,
1784 'comment_id': comment.comment_id,
1796 'comment_id': comment.comment_id,
1785 'comment_version': comment_history.version,
1797 'comment_version': comment_history.version,
1786 'comment_author_username': comment_history.author.username,
1798 'comment_author_username': comment_history.author.username,
1787 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1799 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1788 'comment_created_on': h.age_component(comment_history.created_on,
1800 'comment_created_on': h.age_component(comment_history.created_on,
1789 time_is_local=True),
1801 time_is_local=True),
1790 }
1802 }
1791 else:
1803 else:
1792 log.warning('No permissions for user %s to edit comment_id: %s',
1804 log.warning('No permissions for user %s to edit comment_id: %s',
1793 self._rhodecode_db_user, comment_id)
1805 self._rhodecode_db_user, comment_id)
1794 raise HTTPNotFound()
1806 raise HTTPNotFound()
@@ -1,267 +1,371 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import hashlib
22 import hashlib
23 import itsdangerous
23 import itsdangerous
24 import logging
24 import logging
25 import requests
25 import requests
26 import datetime
26 import datetime
27
27
28 from dogpile.core import ReadWriteMutex
28 from dogpile.core import ReadWriteMutex
29 from pyramid.threadlocal import get_current_registry
29 from pyramid.threadlocal import get_current_registry
30
30
31 import rhodecode.lib.helpers as h
31 import rhodecode.lib.helpers as h
32 from rhodecode.lib.auth import HasRepoPermissionAny
32 from rhodecode.lib.auth import HasRepoPermissionAny
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.model.db import User
34 from rhodecode.model.db import User
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38 LOCK = ReadWriteMutex()
38 LOCK = ReadWriteMutex()
39
39
40 USER_STATE_PUBLIC_KEYS = [
40 USER_STATE_PUBLIC_KEYS = [
41 'id', 'username', 'first_name', 'last_name',
41 'id', 'username', 'first_name', 'last_name',
42 'icon_link', 'display_name', 'display_link']
42 'icon_link', 'display_name', 'display_link']
43
43
44
44
45 class ChannelstreamException(Exception):
45 class ChannelstreamException(Exception):
46 pass
46 pass
47
47
48
48
49 class ChannelstreamConnectionException(ChannelstreamException):
49 class ChannelstreamConnectionException(ChannelstreamException):
50 pass
50 pass
51
51
52
52
53 class ChannelstreamPermissionException(ChannelstreamException):
53 class ChannelstreamPermissionException(ChannelstreamException):
54 pass
54 pass
55
55
56
56
57 def get_channelstream_server_url(config, endpoint):
57 def get_channelstream_server_url(config, endpoint):
58 return 'http://{}{}'.format(config['server'], endpoint)
58 return 'http://{}{}'.format(config['server'], endpoint)
59
59
60
60
61 def channelstream_request(config, payload, endpoint, raise_exc=True):
61 def channelstream_request(config, payload, endpoint, raise_exc=True):
62 signer = itsdangerous.TimestampSigner(config['secret'])
62 signer = itsdangerous.TimestampSigner(config['secret'])
63 sig_for_server = signer.sign(endpoint)
63 sig_for_server = signer.sign(endpoint)
64 secret_headers = {'x-channelstream-secret': sig_for_server,
64 secret_headers = {'x-channelstream-secret': sig_for_server,
65 'x-channelstream-endpoint': endpoint,
65 'x-channelstream-endpoint': endpoint,
66 'Content-Type': 'application/json'}
66 'Content-Type': 'application/json'}
67 req_url = get_channelstream_server_url(config, endpoint)
67 req_url = get_channelstream_server_url(config, endpoint)
68
68
69 log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
69 log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
70 response = None
70 response = None
71 try:
71 try:
72 response = requests.post(req_url, data=json.dumps(payload),
72 response = requests.post(req_url, data=json.dumps(payload),
73 headers=secret_headers).json()
73 headers=secret_headers).json()
74 except requests.ConnectionError:
74 except requests.ConnectionError:
75 log.exception('ConnectionError occurred for endpoint %s', req_url)
75 log.exception('ConnectionError occurred for endpoint %s', req_url)
76 if raise_exc:
76 if raise_exc:
77 raise ChannelstreamConnectionException(req_url)
77 raise ChannelstreamConnectionException(req_url)
78 except Exception:
78 except Exception:
79 log.exception('Exception related to Channelstream happened')
79 log.exception('Exception related to Channelstream happened')
80 if raise_exc:
80 if raise_exc:
81 raise ChannelstreamConnectionException()
81 raise ChannelstreamConnectionException()
82 log.debug('Got channelstream response: %s', response)
82 log.debug('Got channelstream response: %s', response)
83 return response
83 return response
84
84
85
85
86 def get_user_data(user_id):
86 def get_user_data(user_id):
87 user = User.get(user_id)
87 user = User.get(user_id)
88 return {
88 return {
89 'id': user.user_id,
89 'id': user.user_id,
90 'username': user.username,
90 'username': user.username,
91 'first_name': user.first_name,
91 'first_name': user.first_name,
92 'last_name': user.last_name,
92 'last_name': user.last_name,
93 'icon_link': h.gravatar_url(user.email, 60),
93 'icon_link': h.gravatar_url(user.email, 60),
94 'display_name': h.person(user, 'username_or_name_or_email'),
94 'display_name': h.person(user, 'username_or_name_or_email'),
95 'display_link': h.link_to_user(user),
95 'display_link': h.link_to_user(user),
96 'notifications': user.user_data.get('notification_status', True)
96 'notifications': user.user_data.get('notification_status', True)
97 }
97 }
98
98
99
99
100 def broadcast_validator(channel_name):
100 def broadcast_validator(channel_name):
101 """ checks if user can access the broadcast channel """
101 """ checks if user can access the broadcast channel """
102 if channel_name == 'broadcast':
102 if channel_name == 'broadcast':
103 return True
103 return True
104
104
105
105
106 def repo_validator(channel_name):
106 def repo_validator(channel_name):
107 """ checks if user can access the broadcast channel """
107 """ checks if user can access the broadcast channel """
108 channel_prefix = '/repo$'
108 channel_prefix = '/repo$'
109 if channel_name.startswith(channel_prefix):
109 if channel_name.startswith(channel_prefix):
110 elements = channel_name[len(channel_prefix):].split('$')
110 elements = channel_name[len(channel_prefix):].split('$')
111 repo_name = elements[0]
111 repo_name = elements[0]
112 can_access = HasRepoPermissionAny(
112 can_access = HasRepoPermissionAny(
113 'repository.read',
113 'repository.read',
114 'repository.write',
114 'repository.write',
115 'repository.admin')(repo_name)
115 'repository.admin')(repo_name)
116 log.debug(
116 log.debug(
117 'permission check for %s channel resulted in %s',
117 'permission check for %s channel resulted in %s',
118 repo_name, can_access)
118 repo_name, can_access)
119 if can_access:
119 if can_access:
120 return True
120 return True
121 return False
121 return False
122
122
123
123
124 def check_channel_permissions(channels, plugin_validators, should_raise=True):
124 def check_channel_permissions(channels, plugin_validators, should_raise=True):
125 valid_channels = []
125 valid_channels = []
126
126
127 validators = [broadcast_validator, repo_validator]
127 validators = [broadcast_validator, repo_validator]
128 if plugin_validators:
128 if plugin_validators:
129 validators.extend(plugin_validators)
129 validators.extend(plugin_validators)
130 for channel_name in channels:
130 for channel_name in channels:
131 is_valid = False
131 is_valid = False
132 for validator in validators:
132 for validator in validators:
133 if validator(channel_name):
133 if validator(channel_name):
134 is_valid = True
134 is_valid = True
135 break
135 break
136 if is_valid:
136 if is_valid:
137 valid_channels.append(channel_name)
137 valid_channels.append(channel_name)
138 else:
138 else:
139 if should_raise:
139 if should_raise:
140 raise ChannelstreamPermissionException()
140 raise ChannelstreamPermissionException()
141 return valid_channels
141 return valid_channels
142
142
143
143
144 def get_channels_info(self, channels):
144 def get_channels_info(self, channels):
145 payload = {'channels': channels}
145 payload = {'channels': channels}
146 # gather persistence info
146 # gather persistence info
147 return channelstream_request(self._config(), payload, '/info')
147 return channelstream_request(self._config(), payload, '/info')
148
148
149
149
150 def parse_channels_info(info_result, include_channel_info=None):
150 def parse_channels_info(info_result, include_channel_info=None):
151 """
151 """
152 Returns data that contains only secure information that can be
152 Returns data that contains only secure information that can be
153 presented to clients
153 presented to clients
154 """
154 """
155 include_channel_info = include_channel_info or []
155 include_channel_info = include_channel_info or []
156
156
157 user_state_dict = {}
157 user_state_dict = {}
158 for userinfo in info_result['users']:
158 for userinfo in info_result['users']:
159 user_state_dict[userinfo['user']] = {
159 user_state_dict[userinfo['user']] = {
160 k: v for k, v in userinfo['state'].items()
160 k: v for k, v in userinfo['state'].items()
161 if k in USER_STATE_PUBLIC_KEYS
161 if k in USER_STATE_PUBLIC_KEYS
162 }
162 }
163
163
164 channels_info = {}
164 channels_info = {}
165
165
166 for c_name, c_info in info_result['channels'].items():
166 for c_name, c_info in info_result['channels'].items():
167 if c_name not in include_channel_info:
167 if c_name not in include_channel_info:
168 continue
168 continue
169 connected_list = []
169 connected_list = []
170 for username in c_info['users']:
170 for username in c_info['users']:
171 connected_list.append({
171 connected_list.append({
172 'user': username,
172 'user': username,
173 'state': user_state_dict[username]
173 'state': user_state_dict[username]
174 })
174 })
175 channels_info[c_name] = {'users': connected_list,
175 channels_info[c_name] = {'users': connected_list,
176 'history': c_info['history']}
176 'history': c_info['history']}
177
177
178 return channels_info
178 return channels_info
179
179
180
180
181 def log_filepath(history_location, channel_name):
181 def log_filepath(history_location, channel_name):
182 hasher = hashlib.sha256()
182 hasher = hashlib.sha256()
183 hasher.update(channel_name.encode('utf8'))
183 hasher.update(channel_name.encode('utf8'))
184 filename = '{}.log'.format(hasher.hexdigest())
184 filename = '{}.log'.format(hasher.hexdigest())
185 filepath = os.path.join(history_location, filename)
185 filepath = os.path.join(history_location, filename)
186 return filepath
186 return filepath
187
187
188
188
189 def read_history(history_location, channel_name):
189 def read_history(history_location, channel_name):
190 filepath = log_filepath(history_location, channel_name)
190 filepath = log_filepath(history_location, channel_name)
191 if not os.path.exists(filepath):
191 if not os.path.exists(filepath):
192 return []
192 return []
193 history_lines_limit = -100
193 history_lines_limit = -100
194 history = []
194 history = []
195 with open(filepath, 'rb') as f:
195 with open(filepath, 'rb') as f:
196 for line in f.readlines()[history_lines_limit:]:
196 for line in f.readlines()[history_lines_limit:]:
197 try:
197 try:
198 history.append(json.loads(line))
198 history.append(json.loads(line))
199 except Exception:
199 except Exception:
200 log.exception('Failed to load history')
200 log.exception('Failed to load history')
201 return history
201 return history
202
202
203
203
204 def update_history_from_logs(config, channels, payload):
204 def update_history_from_logs(config, channels, payload):
205 history_location = config.get('history.location')
205 history_location = config.get('history.location')
206 for channel in channels:
206 for channel in channels:
207 history = read_history(history_location, channel)
207 history = read_history(history_location, channel)
208 payload['channels_info'][channel]['history'] = history
208 payload['channels_info'][channel]['history'] = history
209
209
210
210
211 def write_history(config, message):
211 def write_history(config, message):
212 """ writes a messge to a base64encoded filename """
212 """ writes a messge to a base64encoded filename """
213 history_location = config.get('history.location')
213 history_location = config.get('history.location')
214 if not os.path.exists(history_location):
214 if not os.path.exists(history_location):
215 return
215 return
216 try:
216 try:
217 LOCK.acquire_write_lock()
217 LOCK.acquire_write_lock()
218 filepath = log_filepath(history_location, message['channel'])
218 filepath = log_filepath(history_location, message['channel'])
219 with open(filepath, 'ab') as f:
219 with open(filepath, 'ab') as f:
220 json.dump(message, f)
220 json.dump(message, f)
221 f.write('\n')
221 f.write('\n')
222 finally:
222 finally:
223 LOCK.release_write_lock()
223 LOCK.release_write_lock()
224
224
225
225
226 def get_connection_validators(registry):
226 def get_connection_validators(registry):
227 validators = []
227 validators = []
228 for k, config in registry.rhodecode_plugins.iteritems():
228 for k, config in registry.rhodecode_plugins.items():
229 validator = config.get('channelstream', {}).get('connect_validator')
229 validator = config.get('channelstream', {}).get('connect_validator')
230 if validator:
230 if validator:
231 validators.append(validator)
231 validators.append(validator)
232 return validators
232 return validators
233
233
234
234
235 def get_channelstream_config(registry=None):
236 if not registry:
237 registry = get_current_registry()
238
239 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
240 channelstream_config = rhodecode_plugins.get('channelstream', {})
241 return channelstream_config
242
243
235 def post_message(channel, message, username, registry=None):
244 def post_message(channel, message, username, registry=None):
245 channelstream_config = get_channelstream_config(registry)
246 if not channelstream_config.get('enabled'):
247 return
236
248
237 message_obj = message
249 message_obj = message
238 if isinstance(message, basestring):
250 if isinstance(message, basestring):
239 message_obj = {
251 message_obj = {
240 'message': message,
252 'message': message,
241 'level': 'success',
253 'level': 'success',
242 'topic': '/notifications'
254 'topic': '/notifications'
243 }
255 }
244
256
245 if not registry:
257 log.debug('Channelstream: sending notification to channel %s', channel)
246 registry = get_current_registry()
258 payload = {
259 'type': 'message',
260 'timestamp': datetime.datetime.utcnow(),
261 'user': 'system',
262 'exclude_users': [username],
263 'channel': channel,
264 'message': message_obj
265 }
266
267 try:
268 return channelstream_request(
269 channelstream_config, [payload], '/message',
270 raise_exc=False)
271 except ChannelstreamException:
272 log.exception('Failed to send channelstream data')
273 raise
274
275
276 def _reload_link(label):
277 return (
278 '<a onclick="window.location.reload()">'
279 '<strong>{}</strong>'
280 '</a>'.format(label)
281 )
282
283
284 def pr_channel(pull_request):
285 repo_name = pull_request.target_repo.repo_name
286 pull_request_id = pull_request.pull_request_id
287 channel = '/repo${}$/pr/{}'.format(repo_name, pull_request_id)
288 log.debug('Getting pull-request channelstream broadcast channel: %s', channel)
289 return channel
290
291
292 def comment_channel(repo_name, commit_obj=None, pull_request_obj=None):
293 channel = None
294 if commit_obj:
295 channel = u'/repo${}$/commit/{}'.format(
296 repo_name, commit_obj.raw_id
297 )
298 elif pull_request_obj:
299 channel = u'/repo${}$/pr/{}'.format(
300 repo_name, pull_request_obj.pull_request_id
301 )
302 log.debug('Getting comment channelstream broadcast channel: %s', channel)
303
304 return channel
305
306
307 def pr_update_channelstream_push(request, pr_broadcast_channel, user, msg, **kwargs):
308 """
309 Channel push on pull request update
310 """
311 if not pr_broadcast_channel:
312 return
247
313
248 log.debug('Channelstream: sending notification to channel %s', channel)
314 _ = request.translate
249 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
315
250 channelstream_config = rhodecode_plugins.get('channelstream', {})
316 message = '{} {}'.format(
251 if channelstream_config.get('enabled'):
317 msg,
252 payload = {
318 _reload_link(_(' Reload page to load changes')))
253 'type': 'message',
319
254 'timestamp': datetime.datetime.utcnow(),
320 message_obj = {
255 'user': 'system',
321 'message': message,
256 'exclude_users': [username],
322 'level': 'success',
257 'channel': channel,
323 'topic': '/notifications'
258 'message': message_obj
324 }
259 }
325
326 post_message(
327 pr_broadcast_channel, message_obj, user.username,
328 registry=request.registry)
329
330
331 def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **kwargs):
332 """
333 Channelstream push on comment action, on commit, or pull-request
334 """
335 if not comment_broadcast_channel:
336 return
337
338 _ = request.translate
260
339
261 try:
340 comment_data = kwargs.pop('comment_data', {})
262 return channelstream_request(
341 user_data = kwargs.pop('user_data', {})
263 channelstream_config, [payload], '/message',
342 comment_id = comment_data.get('comment_id')
264 raise_exc=False)
343
265 except ChannelstreamException:
344 message = '<strong>{}</strong> {} #{}, {}'.format(
266 log.exception('Failed to send channelstream data')
345 user.username,
267 raise
346 msg,
347 comment_id,
348 _reload_link(_('Reload page to see new comments')),
349 )
350
351 message_obj = {
352 'message': message,
353 'level': 'success',
354 'topic': '/notifications'
355 }
356
357 post_message(
358 comment_broadcast_channel, message_obj, user.username,
359 registry=request.registry)
360
361 message_obj = {
362 'message': None,
363 'user': user.username,
364 'comment_id': comment_id,
365 'comment_data': comment_data,
366 'user_data': user_data,
367 'topic': '/comment'
368 }
369 post_message(
370 comment_broadcast_channel, message_obj, user.username,
371 registry=request.registry)
@@ -1,862 +1,818 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95
95
96 def yield_comments(pos):
96 def yield_comments(pos):
97 for co in comment_groups[pos]:
97 for co in comment_groups[pos]:
98 yield co
98 yield co
99
99
100 comment_versions = collections.defaultdict(
100 comment_versions = collections.defaultdict(
101 lambda: collections.defaultdict(list))
101 lambda: collections.defaultdict(list))
102 prev_prvid = -1
102 prev_prvid = -1
103 # fake last entry with None, to aggregate on "latest" version which
103 # fake last entry with None, to aggregate on "latest" version which
104 # doesn't have an pull_request_version_id
104 # doesn't have an pull_request_version_id
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 prvid = ver.pull_request_version_id
106 prvid = ver.pull_request_version_id
107 if prev_prvid == -1:
107 if prev_prvid == -1:
108 prev_prvid = prvid
108 prev_prvid = prvid
109
109
110 for co in yield_comments(prvid):
110 for co in yield_comments(prvid):
111 comment_versions[prvid]['at'].append(co)
111 comment_versions[prvid]['at'].append(co)
112
112
113 # save until
113 # save until
114 current = comment_versions[prvid]['at']
114 current = comment_versions[prvid]['at']
115 prev_until = comment_versions[prev_prvid]['until']
115 prev_until = comment_versions[prev_prvid]['until']
116 cur_until = prev_until + current
116 cur_until = prev_until + current
117 comment_versions[prvid]['until'].extend(cur_until)
117 comment_versions[prvid]['until'].extend(cur_until)
118
118
119 # save outdated
119 # save outdated
120 if inline:
120 if inline:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.outdated_at_version(show_version)]
122 if x.outdated_at_version(show_version)]
123 else:
123 else:
124 outdated = [x for x in cur_until
124 outdated = [x for x in cur_until
125 if x.older_than_version(show_version)]
125 if x.older_than_version(show_version)]
126 display = [x for x in cur_until if x not in outdated]
126 display = [x for x in cur_until if x not in outdated]
127
127
128 comment_versions[prvid]['outdated'] = outdated
128 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['display'] = display
129 comment_versions[prvid]['display'] = display
130
130
131 prev_prvid = prvid
131 prev_prvid = prvid
132
132
133 return comment_versions
133 return comment_versions
134
134
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 qry = Session().query(ChangesetComment) \
136 qry = Session().query(ChangesetComment) \
137 .filter(ChangesetComment.repo == repo)
137 .filter(ChangesetComment.repo == repo)
138
138
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141
141
142 if user:
142 if user:
143 user = self._get_user(user)
143 user = self._get_user(user)
144 if user:
144 if user:
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146
146
147 if commit_id:
147 if commit_id:
148 qry = qry.filter(ChangesetComment.revision == commit_id)
148 qry = qry.filter(ChangesetComment.revision == commit_id)
149
149
150 qry = qry.order_by(ChangesetComment.created_on)
150 qry = qry.order_by(ChangesetComment.created_on)
151 return qry.all()
151 return qry.all()
152
152
153 def get_repository_unresolved_todos(self, repo):
153 def get_repository_unresolved_todos(self, repo):
154 todos = Session().query(ChangesetComment) \
154 todos = Session().query(ChangesetComment) \
155 .filter(ChangesetComment.repo == repo) \
155 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.resolved_by == None) \
156 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.comment_type
157 .filter(ChangesetComment.comment_type
158 == ChangesetComment.COMMENT_TYPE_TODO)
158 == ChangesetComment.COMMENT_TYPE_TODO)
159 todos = todos.all()
159 todos = todos.all()
160
160
161 return todos
161 return todos
162
162
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164
164
165 todos = Session().query(ChangesetComment) \
165 todos = Session().query(ChangesetComment) \
166 .filter(ChangesetComment.pull_request == pull_request) \
166 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.resolved_by == None) \
167 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.comment_type
168 .filter(ChangesetComment.comment_type
169 == ChangesetComment.COMMENT_TYPE_TODO)
169 == ChangesetComment.COMMENT_TYPE_TODO)
170
170
171 if not show_outdated:
171 if not show_outdated:
172 todos = todos.filter(
172 todos = todos.filter(
173 coalesce(ChangesetComment.display_state, '') !=
173 coalesce(ChangesetComment.display_state, '') !=
174 ChangesetComment.COMMENT_OUTDATED)
174 ChangesetComment.COMMENT_OUTDATED)
175
175
176 todos = todos.all()
176 todos = todos.all()
177
177
178 return todos
178 return todos
179
179
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181
181
182 todos = Session().query(ChangesetComment) \
182 todos = Session().query(ChangesetComment) \
183 .filter(ChangesetComment.pull_request == pull_request) \
183 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.resolved_by != None) \
184 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.comment_type
185 .filter(ChangesetComment.comment_type
186 == ChangesetComment.COMMENT_TYPE_TODO)
186 == ChangesetComment.COMMENT_TYPE_TODO)
187
187
188 if not show_outdated:
188 if not show_outdated:
189 todos = todos.filter(
189 todos = todos.filter(
190 coalesce(ChangesetComment.display_state, '') !=
190 coalesce(ChangesetComment.display_state, '') !=
191 ChangesetComment.COMMENT_OUTDATED)
191 ChangesetComment.COMMENT_OUTDATED)
192
192
193 todos = todos.all()
193 todos = todos.all()
194
194
195 return todos
195 return todos
196
196
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198
198
199 todos = Session().query(ChangesetComment) \
199 todos = Session().query(ChangesetComment) \
200 .filter(ChangesetComment.revision == commit_id) \
200 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.resolved_by == None) \
201 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.comment_type
202 .filter(ChangesetComment.comment_type
203 == ChangesetComment.COMMENT_TYPE_TODO)
203 == ChangesetComment.COMMENT_TYPE_TODO)
204
204
205 if not show_outdated:
205 if not show_outdated:
206 todos = todos.filter(
206 todos = todos.filter(
207 coalesce(ChangesetComment.display_state, '') !=
207 coalesce(ChangesetComment.display_state, '') !=
208 ChangesetComment.COMMENT_OUTDATED)
208 ChangesetComment.COMMENT_OUTDATED)
209
209
210 todos = todos.all()
210 todos = todos.all()
211
211
212 return todos
212 return todos
213
213
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215
215
216 todos = Session().query(ChangesetComment) \
216 todos = Session().query(ChangesetComment) \
217 .filter(ChangesetComment.revision == commit_id) \
217 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.resolved_by != None) \
218 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.comment_type
219 .filter(ChangesetComment.comment_type
220 == ChangesetComment.COMMENT_TYPE_TODO)
220 == ChangesetComment.COMMENT_TYPE_TODO)
221
221
222 if not show_outdated:
222 if not show_outdated:
223 todos = todos.filter(
223 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
225 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
226
226
227 todos = todos.all()
227 todos = todos.all()
228
228
229 return todos
229 return todos
230
230
231 def get_commit_inline_comments(self, commit_id):
231 def get_commit_inline_comments(self, commit_id):
232 inline_comments = Session().query(ChangesetComment) \
232 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
233 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
234 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
235 .filter(ChangesetComment.revision == commit_id)
236 inline_comments = inline_comments.all()
236 inline_comments = inline_comments.all()
237 return inline_comments
237 return inline_comments
238
238
239 def _log_audit_action(self, action, action_data, auth_user, comment):
239 def _log_audit_action(self, action, action_data, auth_user, comment):
240 audit_logger.store(
240 audit_logger.store(
241 action=action,
241 action=action,
242 action_data=action_data,
242 action_data=action_data,
243 user=auth_user,
243 user=auth_user,
244 repo=comment.repo)
244 repo=comment.repo)
245
245
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
247 f_path=None, line_no=None, status_change=None,
247 f_path=None, line_no=None, status_change=None,
248 status_change_type=None, comment_type=None,
248 status_change_type=None, comment_type=None,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
250 renderer=None, auth_user=None, extra_recipients=None):
250 renderer=None, auth_user=None, extra_recipients=None):
251 """
251 """
252 Creates new comment for commit or pull request.
252 Creates new comment for commit or pull request.
253 IF status_change is not none this comment is associated with a
253 IF status_change is not none this comment is associated with a
254 status change of commit or commit associated with pull request
254 status change of commit or commit associated with pull request
255
255
256 :param text:
256 :param text:
257 :param repo:
257 :param repo:
258 :param user:
258 :param user:
259 :param commit_id:
259 :param commit_id:
260 :param pull_request:
260 :param pull_request:
261 :param f_path:
261 :param f_path:
262 :param line_no:
262 :param line_no:
263 :param status_change: Label for status change
263 :param status_change: Label for status change
264 :param comment_type: Type of comment
264 :param comment_type: Type of comment
265 :param resolves_comment_id: id of comment which this one will resolve
265 :param resolves_comment_id: id of comment which this one will resolve
266 :param status_change_type: type of status change
266 :param status_change_type: type of status change
267 :param closing_pr:
267 :param closing_pr:
268 :param send_email:
268 :param send_email:
269 :param renderer: pick renderer for this comment
269 :param renderer: pick renderer for this comment
270 :param auth_user: current authenticated user calling this method
270 :param auth_user: current authenticated user calling this method
271 :param extra_recipients: list of extra users to be added to recipients
271 :param extra_recipients: list of extra users to be added to recipients
272 """
272 """
273
273
274 if not text:
274 if not text:
275 log.warning('Missing text for comment, skipping...')
275 log.warning('Missing text for comment, skipping...')
276 return
276 return
277 request = get_current_request()
277 request = get_current_request()
278 _ = request.translate
278 _ = request.translate
279
279
280 if not renderer:
280 if not renderer:
281 renderer = self._get_renderer(request=request)
281 renderer = self._get_renderer(request=request)
282
282
283 repo = self._get_repo(repo)
283 repo = self._get_repo(repo)
284 user = self._get_user(user)
284 user = self._get_user(user)
285 auth_user = auth_user or user
285 auth_user = auth_user or user
286
286
287 schema = comment_schema.CommentSchema()
287 schema = comment_schema.CommentSchema()
288 validated_kwargs = schema.deserialize(dict(
288 validated_kwargs = schema.deserialize(dict(
289 comment_body=text,
289 comment_body=text,
290 comment_type=comment_type,
290 comment_type=comment_type,
291 comment_file=f_path,
291 comment_file=f_path,
292 comment_line=line_no,
292 comment_line=line_no,
293 renderer_type=renderer,
293 renderer_type=renderer,
294 status_change=status_change_type,
294 status_change=status_change_type,
295 resolves_comment_id=resolves_comment_id,
295 resolves_comment_id=resolves_comment_id,
296 repo=repo.repo_id,
296 repo=repo.repo_id,
297 user=user.user_id,
297 user=user.user_id,
298 ))
298 ))
299
299
300 comment = ChangesetComment()
300 comment = ChangesetComment()
301 comment.renderer = validated_kwargs['renderer_type']
301 comment.renderer = validated_kwargs['renderer_type']
302 comment.text = validated_kwargs['comment_body']
302 comment.text = validated_kwargs['comment_body']
303 comment.f_path = validated_kwargs['comment_file']
303 comment.f_path = validated_kwargs['comment_file']
304 comment.line_no = validated_kwargs['comment_line']
304 comment.line_no = validated_kwargs['comment_line']
305 comment.comment_type = validated_kwargs['comment_type']
305 comment.comment_type = validated_kwargs['comment_type']
306
306
307 comment.repo = repo
307 comment.repo = repo
308 comment.author = user
308 comment.author = user
309 resolved_comment = self.__get_commit_comment(
309 resolved_comment = self.__get_commit_comment(
310 validated_kwargs['resolves_comment_id'])
310 validated_kwargs['resolves_comment_id'])
311 # check if the comment actually belongs to this PR
311 # check if the comment actually belongs to this PR
312 if resolved_comment and resolved_comment.pull_request and \
312 if resolved_comment and resolved_comment.pull_request and \
313 resolved_comment.pull_request != pull_request:
313 resolved_comment.pull_request != pull_request:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
315 resolved_comment)
316 # comment not bound to this pull request, forbid
316 # comment not bound to this pull request, forbid
317 resolved_comment = None
317 resolved_comment = None
318
318
319 elif resolved_comment and resolved_comment.repo and \
319 elif resolved_comment and resolved_comment.repo and \
320 resolved_comment.repo != repo:
320 resolved_comment.repo != repo:
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
322 resolved_comment)
322 resolved_comment)
323 # comment not bound to this repo, forbid
323 # comment not bound to this repo, forbid
324 resolved_comment = None
324 resolved_comment = None
325
325
326 comment.resolved_comment = resolved_comment
326 comment.resolved_comment = resolved_comment
327
327
328 pull_request_id = pull_request
328 pull_request_id = pull_request
329
329
330 commit_obj = None
330 commit_obj = None
331 pull_request_obj = None
331 pull_request_obj = None
332
332
333 if commit_id:
333 if commit_id:
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
335 # do a lookup, so we don't pass something bad here
335 # do a lookup, so we don't pass something bad here
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
337 comment.revision = commit_obj.raw_id
337 comment.revision = commit_obj.raw_id
338
338
339 elif pull_request_id:
339 elif pull_request_id:
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
341 pull_request_obj = self.__get_pull_request(pull_request_id)
341 pull_request_obj = self.__get_pull_request(pull_request_id)
342 comment.pull_request = pull_request_obj
342 comment.pull_request = pull_request_obj
343 else:
343 else:
344 raise Exception('Please specify commit or pull_request_id')
344 raise Exception('Please specify commit or pull_request_id')
345
345
346 Session().add(comment)
346 Session().add(comment)
347 Session().flush()
347 Session().flush()
348 kwargs = {
348 kwargs = {
349 'user': user,
349 'user': user,
350 'renderer_type': renderer,
350 'renderer_type': renderer,
351 'repo_name': repo.repo_name,
351 'repo_name': repo.repo_name,
352 'status_change': status_change,
352 'status_change': status_change,
353 'status_change_type': status_change_type,
353 'status_change_type': status_change_type,
354 'comment_body': text,
354 'comment_body': text,
355 'comment_file': f_path,
355 'comment_file': f_path,
356 'comment_line': line_no,
356 'comment_line': line_no,
357 'comment_type': comment_type or 'note',
357 'comment_type': comment_type or 'note',
358 'comment_id': comment.comment_id
358 'comment_id': comment.comment_id
359 }
359 }
360
360
361 if commit_obj:
361 if commit_obj:
362 recipients = ChangesetComment.get_users(
362 recipients = ChangesetComment.get_users(
363 revision=commit_obj.raw_id)
363 revision=commit_obj.raw_id)
364 # add commit author if it's in RhodeCode system
364 # add commit author if it's in RhodeCode system
365 cs_author = User.get_from_cs_author(commit_obj.author)
365 cs_author = User.get_from_cs_author(commit_obj.author)
366 if not cs_author:
366 if not cs_author:
367 # use repo owner if we cannot extract the author correctly
367 # use repo owner if we cannot extract the author correctly
368 cs_author = repo.user
368 cs_author = repo.user
369 recipients += [cs_author]
369 recipients += [cs_author]
370
370
371 commit_comment_url = self.get_url(comment, request=request)
371 commit_comment_url = self.get_url(comment, request=request)
372 commit_comment_reply_url = self.get_url(
372 commit_comment_reply_url = self.get_url(
373 comment, request=request,
373 comment, request=request,
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
375
375
376 target_repo_url = h.link_to(
376 target_repo_url = h.link_to(
377 repo.repo_name,
377 repo.repo_name,
378 h.route_url('repo_summary', repo_name=repo.repo_name))
378 h.route_url('repo_summary', repo_name=repo.repo_name))
379
379
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
381 commit_id=commit_id)
381 commit_id=commit_id)
382
382
383 # commit specifics
383 # commit specifics
384 kwargs.update({
384 kwargs.update({
385 'commit': commit_obj,
385 'commit': commit_obj,
386 'commit_message': commit_obj.message,
386 'commit_message': commit_obj.message,
387 'commit_target_repo_url': target_repo_url,
387 'commit_target_repo_url': target_repo_url,
388 'commit_comment_url': commit_comment_url,
388 'commit_comment_url': commit_comment_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
390 'commit_url': commit_url,
390 'commit_url': commit_url,
391 'thread_ids': [commit_url, commit_comment_url],
391 'thread_ids': [commit_url, commit_comment_url],
392 })
392 })
393
393
394 elif pull_request_obj:
394 elif pull_request_obj:
395 # get the current participants of this pull request
395 # get the current participants of this pull request
396 recipients = ChangesetComment.get_users(
396 recipients = ChangesetComment.get_users(
397 pull_request_id=pull_request_obj.pull_request_id)
397 pull_request_id=pull_request_obj.pull_request_id)
398 # add pull request author
398 # add pull request author
399 recipients += [pull_request_obj.author]
399 recipients += [pull_request_obj.author]
400
400
401 # add the reviewers to notification
401 # add the reviewers to notification
402 recipients += [x.user for x in pull_request_obj.reviewers]
402 recipients += [x.user for x in pull_request_obj.reviewers]
403
403
404 pr_target_repo = pull_request_obj.target_repo
404 pr_target_repo = pull_request_obj.target_repo
405 pr_source_repo = pull_request_obj.source_repo
405 pr_source_repo = pull_request_obj.source_repo
406
406
407 pr_comment_url = self.get_url(comment, request=request)
407 pr_comment_url = self.get_url(comment, request=request)
408 pr_comment_reply_url = self.get_url(
408 pr_comment_reply_url = self.get_url(
409 comment, request=request,
409 comment, request=request,
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
411
411
412 pr_url = h.route_url(
412 pr_url = h.route_url(
413 'pullrequest_show',
413 'pullrequest_show',
414 repo_name=pr_target_repo.repo_name,
414 repo_name=pr_target_repo.repo_name,
415 pull_request_id=pull_request_obj.pull_request_id, )
415 pull_request_id=pull_request_obj.pull_request_id, )
416
416
417 # set some variables for email notification
417 # set some variables for email notification
418 pr_target_repo_url = h.route_url(
418 pr_target_repo_url = h.route_url(
419 'repo_summary', repo_name=pr_target_repo.repo_name)
419 'repo_summary', repo_name=pr_target_repo.repo_name)
420
420
421 pr_source_repo_url = h.route_url(
421 pr_source_repo_url = h.route_url(
422 'repo_summary', repo_name=pr_source_repo.repo_name)
422 'repo_summary', repo_name=pr_source_repo.repo_name)
423
423
424 # pull request specifics
424 # pull request specifics
425 kwargs.update({
425 kwargs.update({
426 'pull_request': pull_request_obj,
426 'pull_request': pull_request_obj,
427 'pr_id': pull_request_obj.pull_request_id,
427 'pr_id': pull_request_obj.pull_request_id,
428 'pull_request_url': pr_url,
428 'pull_request_url': pr_url,
429 'pull_request_target_repo': pr_target_repo,
429 'pull_request_target_repo': pr_target_repo,
430 'pull_request_target_repo_url': pr_target_repo_url,
430 'pull_request_target_repo_url': pr_target_repo_url,
431 'pull_request_source_repo': pr_source_repo,
431 'pull_request_source_repo': pr_source_repo,
432 'pull_request_source_repo_url': pr_source_repo_url,
432 'pull_request_source_repo_url': pr_source_repo_url,
433 'pr_comment_url': pr_comment_url,
433 'pr_comment_url': pr_comment_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
435 'pr_closing': closing_pr,
435 'pr_closing': closing_pr,
436 'thread_ids': [pr_url, pr_comment_url],
436 'thread_ids': [pr_url, pr_comment_url],
437 })
437 })
438
438
439 if send_email:
439 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
441 # pre-generate the subject for notification itself
441 # pre-generate the subject for notification itself
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
443 notification_type, **kwargs)
443 notification_type, **kwargs)
444
444
445 mention_recipients = set(
445 mention_recipients = set(
446 self._extract_mentions(text)).difference(recipients)
446 self._extract_mentions(text)).difference(recipients)
447
447
448 # create notification objects, and emails
448 # create notification objects, and emails
449 NotificationModel().create(
449 NotificationModel().create(
450 created_by=user,
450 created_by=user,
451 notification_subject=subject,
451 notification_subject=subject,
452 notification_body=body_plaintext,
452 notification_body=body_plaintext,
453 notification_type=notification_type,
453 notification_type=notification_type,
454 recipients=recipients,
454 recipients=recipients,
455 mention_recipients=mention_recipients,
455 mention_recipients=mention_recipients,
456 email_kwargs=kwargs,
456 email_kwargs=kwargs,
457 )
457 )
458
458
459 Session().flush()
459 Session().flush()
460 if comment.pull_request:
460 if comment.pull_request:
461 action = 'repo.pull_request.comment.create'
461 action = 'repo.pull_request.comment.create'
462 else:
462 else:
463 action = 'repo.commit.comment.create'
463 action = 'repo.commit.comment.create'
464
464
465 comment_id = comment.comment_id
466 comment_data = comment.get_api_data()
465 comment_data = comment.get_api_data()
467
466
468 self._log_audit_action(
467 self._log_audit_action(
469 action, {'data': comment_data}, auth_user, comment)
468 action, {'data': comment_data}, auth_user, comment)
470
469
471 channel = None
472 if commit_obj:
473 repo_name = repo.repo_name
474 channel = u'/repo${}$/commit/{}'.format(
475 repo_name,
476 commit_obj.raw_id
477 )
478 elif pull_request_obj:
479 repo_name = pr_target_repo.repo_name
480 channel = u'/repo${}$/pr/{}'.format(
481 repo_name,
482 pull_request_obj.pull_request_id
483 )
484
485 if channel:
486 username = user.username
487 message = '<strong>{}</strong> {} #{}, {}'
488 message = message.format(
489 username,
490 _('posted a new comment'),
491 comment_id,
492 _('Refresh the page to see new comments.'))
493
494 message_obj = {
495 'message': message,
496 'level': 'success',
497 'topic': '/notifications'
498 }
499
500 channelstream.post_message(
501 channel, message_obj, user.username,
502 registry=get_current_registry())
503
504 message_obj = {
505 'message': None,
506 'user': username,
507 'comment_id': comment_id,
508 'topic': '/comment'
509 }
510 channelstream.post_message(
511 channel, message_obj, user.username,
512 registry=get_current_registry())
513
514 return comment
470 return comment
515
471
516 def edit(self, comment_id, text, auth_user, version):
472 def edit(self, comment_id, text, auth_user, version):
517 """
473 """
518 Change existing comment for commit or pull request.
474 Change existing comment for commit or pull request.
519
475
520 :param comment_id:
476 :param comment_id:
521 :param text:
477 :param text:
522 :param auth_user: current authenticated user calling this method
478 :param auth_user: current authenticated user calling this method
523 :param version: last comment version
479 :param version: last comment version
524 """
480 """
525 if not text:
481 if not text:
526 log.warning('Missing text for comment, skipping...')
482 log.warning('Missing text for comment, skipping...')
527 return
483 return
528
484
529 comment = ChangesetComment.get(comment_id)
485 comment = ChangesetComment.get(comment_id)
530 old_comment_text = comment.text
486 old_comment_text = comment.text
531 comment.text = text
487 comment.text = text
532 comment.modified_at = datetime.datetime.now()
488 comment.modified_at = datetime.datetime.now()
533 version = safe_int(version)
489 version = safe_int(version)
534
490
535 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
491 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
536 # would return 3 here
492 # would return 3 here
537 comment_version = ChangesetCommentHistory.get_version(comment_id)
493 comment_version = ChangesetCommentHistory.get_version(comment_id)
538
494
539 if isinstance(version, (int, long)) and (comment_version - version) != 1:
495 if isinstance(version, (int, long)) and (comment_version - version) != 1:
540 log.warning(
496 log.warning(
541 'Version mismatch comment_version {} submitted {}, skipping'.format(
497 'Version mismatch comment_version {} submitted {}, skipping'.format(
542 comment_version-1, # -1 since note above
498 comment_version-1, # -1 since note above
543 version
499 version
544 )
500 )
545 )
501 )
546 raise CommentVersionMismatch()
502 raise CommentVersionMismatch()
547
503
548 comment_history = ChangesetCommentHistory()
504 comment_history = ChangesetCommentHistory()
549 comment_history.comment_id = comment_id
505 comment_history.comment_id = comment_id
550 comment_history.version = comment_version
506 comment_history.version = comment_version
551 comment_history.created_by_user_id = auth_user.user_id
507 comment_history.created_by_user_id = auth_user.user_id
552 comment_history.text = old_comment_text
508 comment_history.text = old_comment_text
553 # TODO add email notification
509 # TODO add email notification
554 Session().add(comment_history)
510 Session().add(comment_history)
555 Session().add(comment)
511 Session().add(comment)
556 Session().flush()
512 Session().flush()
557
513
558 if comment.pull_request:
514 if comment.pull_request:
559 action = 'repo.pull_request.comment.edit'
515 action = 'repo.pull_request.comment.edit'
560 else:
516 else:
561 action = 'repo.commit.comment.edit'
517 action = 'repo.commit.comment.edit'
562
518
563 comment_data = comment.get_api_data()
519 comment_data = comment.get_api_data()
564 comment_data['old_comment_text'] = old_comment_text
520 comment_data['old_comment_text'] = old_comment_text
565 self._log_audit_action(
521 self._log_audit_action(
566 action, {'data': comment_data}, auth_user, comment)
522 action, {'data': comment_data}, auth_user, comment)
567
523
568 return comment_history
524 return comment_history
569
525
570 def delete(self, comment, auth_user):
526 def delete(self, comment, auth_user):
571 """
527 """
572 Deletes given comment
528 Deletes given comment
573 """
529 """
574 comment = self.__get_commit_comment(comment)
530 comment = self.__get_commit_comment(comment)
575 old_data = comment.get_api_data()
531 old_data = comment.get_api_data()
576 Session().delete(comment)
532 Session().delete(comment)
577
533
578 if comment.pull_request:
534 if comment.pull_request:
579 action = 'repo.pull_request.comment.delete'
535 action = 'repo.pull_request.comment.delete'
580 else:
536 else:
581 action = 'repo.commit.comment.delete'
537 action = 'repo.commit.comment.delete'
582
538
583 self._log_audit_action(
539 self._log_audit_action(
584 action, {'old_data': old_data}, auth_user, comment)
540 action, {'old_data': old_data}, auth_user, comment)
585
541
586 return comment
542 return comment
587
543
588 def get_all_comments(self, repo_id, revision=None, pull_request=None):
544 def get_all_comments(self, repo_id, revision=None, pull_request=None):
589 q = ChangesetComment.query()\
545 q = ChangesetComment.query()\
590 .filter(ChangesetComment.repo_id == repo_id)
546 .filter(ChangesetComment.repo_id == repo_id)
591 if revision:
547 if revision:
592 q = q.filter(ChangesetComment.revision == revision)
548 q = q.filter(ChangesetComment.revision == revision)
593 elif pull_request:
549 elif pull_request:
594 pull_request = self.__get_pull_request(pull_request)
550 pull_request = self.__get_pull_request(pull_request)
595 q = q.filter(ChangesetComment.pull_request == pull_request)
551 q = q.filter(ChangesetComment.pull_request == pull_request)
596 else:
552 else:
597 raise Exception('Please specify commit or pull_request')
553 raise Exception('Please specify commit or pull_request')
598 q = q.order_by(ChangesetComment.created_on)
554 q = q.order_by(ChangesetComment.created_on)
599 return q.all()
555 return q.all()
600
556
601 def get_url(self, comment, request=None, permalink=False, anchor=None):
557 def get_url(self, comment, request=None, permalink=False, anchor=None):
602 if not request:
558 if not request:
603 request = get_current_request()
559 request = get_current_request()
604
560
605 comment = self.__get_commit_comment(comment)
561 comment = self.__get_commit_comment(comment)
606 if anchor is None:
562 if anchor is None:
607 anchor = 'comment-{}'.format(comment.comment_id)
563 anchor = 'comment-{}'.format(comment.comment_id)
608
564
609 if comment.pull_request:
565 if comment.pull_request:
610 pull_request = comment.pull_request
566 pull_request = comment.pull_request
611 if permalink:
567 if permalink:
612 return request.route_url(
568 return request.route_url(
613 'pull_requests_global',
569 'pull_requests_global',
614 pull_request_id=pull_request.pull_request_id,
570 pull_request_id=pull_request.pull_request_id,
615 _anchor=anchor)
571 _anchor=anchor)
616 else:
572 else:
617 return request.route_url(
573 return request.route_url(
618 'pullrequest_show',
574 'pullrequest_show',
619 repo_name=safe_str(pull_request.target_repo.repo_name),
575 repo_name=safe_str(pull_request.target_repo.repo_name),
620 pull_request_id=pull_request.pull_request_id,
576 pull_request_id=pull_request.pull_request_id,
621 _anchor=anchor)
577 _anchor=anchor)
622
578
623 else:
579 else:
624 repo = comment.repo
580 repo = comment.repo
625 commit_id = comment.revision
581 commit_id = comment.revision
626
582
627 if permalink:
583 if permalink:
628 return request.route_url(
584 return request.route_url(
629 'repo_commit', repo_name=safe_str(repo.repo_id),
585 'repo_commit', repo_name=safe_str(repo.repo_id),
630 commit_id=commit_id,
586 commit_id=commit_id,
631 _anchor=anchor)
587 _anchor=anchor)
632
588
633 else:
589 else:
634 return request.route_url(
590 return request.route_url(
635 'repo_commit', repo_name=safe_str(repo.repo_name),
591 'repo_commit', repo_name=safe_str(repo.repo_name),
636 commit_id=commit_id,
592 commit_id=commit_id,
637 _anchor=anchor)
593 _anchor=anchor)
638
594
639 def get_comments(self, repo_id, revision=None, pull_request=None):
595 def get_comments(self, repo_id, revision=None, pull_request=None):
640 """
596 """
641 Gets main comments based on revision or pull_request_id
597 Gets main comments based on revision or pull_request_id
642
598
643 :param repo_id:
599 :param repo_id:
644 :param revision:
600 :param revision:
645 :param pull_request:
601 :param pull_request:
646 """
602 """
647
603
648 q = ChangesetComment.query()\
604 q = ChangesetComment.query()\
649 .filter(ChangesetComment.repo_id == repo_id)\
605 .filter(ChangesetComment.repo_id == repo_id)\
650 .filter(ChangesetComment.line_no == None)\
606 .filter(ChangesetComment.line_no == None)\
651 .filter(ChangesetComment.f_path == None)
607 .filter(ChangesetComment.f_path == None)
652 if revision:
608 if revision:
653 q = q.filter(ChangesetComment.revision == revision)
609 q = q.filter(ChangesetComment.revision == revision)
654 elif pull_request:
610 elif pull_request:
655 pull_request = self.__get_pull_request(pull_request)
611 pull_request = self.__get_pull_request(pull_request)
656 q = q.filter(ChangesetComment.pull_request == pull_request)
612 q = q.filter(ChangesetComment.pull_request == pull_request)
657 else:
613 else:
658 raise Exception('Please specify commit or pull_request')
614 raise Exception('Please specify commit or pull_request')
659 q = q.order_by(ChangesetComment.created_on)
615 q = q.order_by(ChangesetComment.created_on)
660 return q.all()
616 return q.all()
661
617
662 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
618 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
663 q = self._get_inline_comments_query(repo_id, revision, pull_request)
619 q = self._get_inline_comments_query(repo_id, revision, pull_request)
664 return self._group_comments_by_path_and_line_number(q)
620 return self._group_comments_by_path_and_line_number(q)
665
621
666 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
622 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
667 version=None):
623 version=None):
668 inline_comms = []
624 inline_comms = []
669 for fname, per_line_comments in inline_comments.iteritems():
625 for fname, per_line_comments in inline_comments.iteritems():
670 for lno, comments in per_line_comments.iteritems():
626 for lno, comments in per_line_comments.iteritems():
671 for comm in comments:
627 for comm in comments:
672 if not comm.outdated_at_version(version) and skip_outdated:
628 if not comm.outdated_at_version(version) and skip_outdated:
673 inline_comms.append(comm)
629 inline_comms.append(comm)
674
630
675 return inline_comms
631 return inline_comms
676
632
677 def get_outdated_comments(self, repo_id, pull_request):
633 def get_outdated_comments(self, repo_id, pull_request):
678 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
634 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
679 # of a pull request.
635 # of a pull request.
680 q = self._all_inline_comments_of_pull_request(pull_request)
636 q = self._all_inline_comments_of_pull_request(pull_request)
681 q = q.filter(
637 q = q.filter(
682 ChangesetComment.display_state ==
638 ChangesetComment.display_state ==
683 ChangesetComment.COMMENT_OUTDATED
639 ChangesetComment.COMMENT_OUTDATED
684 ).order_by(ChangesetComment.comment_id.asc())
640 ).order_by(ChangesetComment.comment_id.asc())
685
641
686 return self._group_comments_by_path_and_line_number(q)
642 return self._group_comments_by_path_and_line_number(q)
687
643
688 def _get_inline_comments_query(self, repo_id, revision, pull_request):
644 def _get_inline_comments_query(self, repo_id, revision, pull_request):
689 # TODO: johbo: Split this into two methods: One for PR and one for
645 # TODO: johbo: Split this into two methods: One for PR and one for
690 # commit.
646 # commit.
691 if revision:
647 if revision:
692 q = Session().query(ChangesetComment).filter(
648 q = Session().query(ChangesetComment).filter(
693 ChangesetComment.repo_id == repo_id,
649 ChangesetComment.repo_id == repo_id,
694 ChangesetComment.line_no != null(),
650 ChangesetComment.line_no != null(),
695 ChangesetComment.f_path != null(),
651 ChangesetComment.f_path != null(),
696 ChangesetComment.revision == revision)
652 ChangesetComment.revision == revision)
697
653
698 elif pull_request:
654 elif pull_request:
699 pull_request = self.__get_pull_request(pull_request)
655 pull_request = self.__get_pull_request(pull_request)
700 if not CommentsModel.use_outdated_comments(pull_request):
656 if not CommentsModel.use_outdated_comments(pull_request):
701 q = self._visible_inline_comments_of_pull_request(pull_request)
657 q = self._visible_inline_comments_of_pull_request(pull_request)
702 else:
658 else:
703 q = self._all_inline_comments_of_pull_request(pull_request)
659 q = self._all_inline_comments_of_pull_request(pull_request)
704
660
705 else:
661 else:
706 raise Exception('Please specify commit or pull_request_id')
662 raise Exception('Please specify commit or pull_request_id')
707 q = q.order_by(ChangesetComment.comment_id.asc())
663 q = q.order_by(ChangesetComment.comment_id.asc())
708 return q
664 return q
709
665
710 def _group_comments_by_path_and_line_number(self, q):
666 def _group_comments_by_path_and_line_number(self, q):
711 comments = q.all()
667 comments = q.all()
712 paths = collections.defaultdict(lambda: collections.defaultdict(list))
668 paths = collections.defaultdict(lambda: collections.defaultdict(list))
713 for co in comments:
669 for co in comments:
714 paths[co.f_path][co.line_no].append(co)
670 paths[co.f_path][co.line_no].append(co)
715 return paths
671 return paths
716
672
717 @classmethod
673 @classmethod
718 def needed_extra_diff_context(cls):
674 def needed_extra_diff_context(cls):
719 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
675 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
720
676
721 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
677 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
722 if not CommentsModel.use_outdated_comments(pull_request):
678 if not CommentsModel.use_outdated_comments(pull_request):
723 return
679 return
724
680
725 comments = self._visible_inline_comments_of_pull_request(pull_request)
681 comments = self._visible_inline_comments_of_pull_request(pull_request)
726 comments_to_outdate = comments.all()
682 comments_to_outdate = comments.all()
727
683
728 for comment in comments_to_outdate:
684 for comment in comments_to_outdate:
729 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
685 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
730
686
731 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
687 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
732 diff_line = _parse_comment_line_number(comment.line_no)
688 diff_line = _parse_comment_line_number(comment.line_no)
733
689
734 try:
690 try:
735 old_context = old_diff_proc.get_context_of_line(
691 old_context = old_diff_proc.get_context_of_line(
736 path=comment.f_path, diff_line=diff_line)
692 path=comment.f_path, diff_line=diff_line)
737 new_context = new_diff_proc.get_context_of_line(
693 new_context = new_diff_proc.get_context_of_line(
738 path=comment.f_path, diff_line=diff_line)
694 path=comment.f_path, diff_line=diff_line)
739 except (diffs.LineNotInDiffException,
695 except (diffs.LineNotInDiffException,
740 diffs.FileNotInDiffException):
696 diffs.FileNotInDiffException):
741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
697 comment.display_state = ChangesetComment.COMMENT_OUTDATED
742 return
698 return
743
699
744 if old_context == new_context:
700 if old_context == new_context:
745 return
701 return
746
702
747 if self._should_relocate_diff_line(diff_line):
703 if self._should_relocate_diff_line(diff_line):
748 new_diff_lines = new_diff_proc.find_context(
704 new_diff_lines = new_diff_proc.find_context(
749 path=comment.f_path, context=old_context,
705 path=comment.f_path, context=old_context,
750 offset=self.DIFF_CONTEXT_BEFORE)
706 offset=self.DIFF_CONTEXT_BEFORE)
751 if not new_diff_lines:
707 if not new_diff_lines:
752 comment.display_state = ChangesetComment.COMMENT_OUTDATED
708 comment.display_state = ChangesetComment.COMMENT_OUTDATED
753 else:
709 else:
754 new_diff_line = self._choose_closest_diff_line(
710 new_diff_line = self._choose_closest_diff_line(
755 diff_line, new_diff_lines)
711 diff_line, new_diff_lines)
756 comment.line_no = _diff_to_comment_line_number(new_diff_line)
712 comment.line_no = _diff_to_comment_line_number(new_diff_line)
757 else:
713 else:
758 comment.display_state = ChangesetComment.COMMENT_OUTDATED
714 comment.display_state = ChangesetComment.COMMENT_OUTDATED
759
715
760 def _should_relocate_diff_line(self, diff_line):
716 def _should_relocate_diff_line(self, diff_line):
761 """
717 """
762 Checks if relocation shall be tried for the given `diff_line`.
718 Checks if relocation shall be tried for the given `diff_line`.
763
719
764 If a comment points into the first lines, then we can have a situation
720 If a comment points into the first lines, then we can have a situation
765 that after an update another line has been added on top. In this case
721 that after an update another line has been added on top. In this case
766 we would find the context still and move the comment around. This
722 we would find the context still and move the comment around. This
767 would be wrong.
723 would be wrong.
768 """
724 """
769 should_relocate = (
725 should_relocate = (
770 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
726 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
771 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
727 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
772 return should_relocate
728 return should_relocate
773
729
774 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
730 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
775 candidate = new_diff_lines[0]
731 candidate = new_diff_lines[0]
776 best_delta = _diff_line_delta(diff_line, candidate)
732 best_delta = _diff_line_delta(diff_line, candidate)
777 for new_diff_line in new_diff_lines[1:]:
733 for new_diff_line in new_diff_lines[1:]:
778 delta = _diff_line_delta(diff_line, new_diff_line)
734 delta = _diff_line_delta(diff_line, new_diff_line)
779 if delta < best_delta:
735 if delta < best_delta:
780 candidate = new_diff_line
736 candidate = new_diff_line
781 best_delta = delta
737 best_delta = delta
782 return candidate
738 return candidate
783
739
784 def _visible_inline_comments_of_pull_request(self, pull_request):
740 def _visible_inline_comments_of_pull_request(self, pull_request):
785 comments = self._all_inline_comments_of_pull_request(pull_request)
741 comments = self._all_inline_comments_of_pull_request(pull_request)
786 comments = comments.filter(
742 comments = comments.filter(
787 coalesce(ChangesetComment.display_state, '') !=
743 coalesce(ChangesetComment.display_state, '') !=
788 ChangesetComment.COMMENT_OUTDATED)
744 ChangesetComment.COMMENT_OUTDATED)
789 return comments
745 return comments
790
746
791 def _all_inline_comments_of_pull_request(self, pull_request):
747 def _all_inline_comments_of_pull_request(self, pull_request):
792 comments = Session().query(ChangesetComment)\
748 comments = Session().query(ChangesetComment)\
793 .filter(ChangesetComment.line_no != None)\
749 .filter(ChangesetComment.line_no != None)\
794 .filter(ChangesetComment.f_path != None)\
750 .filter(ChangesetComment.f_path != None)\
795 .filter(ChangesetComment.pull_request == pull_request)
751 .filter(ChangesetComment.pull_request == pull_request)
796 return comments
752 return comments
797
753
798 def _all_general_comments_of_pull_request(self, pull_request):
754 def _all_general_comments_of_pull_request(self, pull_request):
799 comments = Session().query(ChangesetComment)\
755 comments = Session().query(ChangesetComment)\
800 .filter(ChangesetComment.line_no == None)\
756 .filter(ChangesetComment.line_no == None)\
801 .filter(ChangesetComment.f_path == None)\
757 .filter(ChangesetComment.f_path == None)\
802 .filter(ChangesetComment.pull_request == pull_request)
758 .filter(ChangesetComment.pull_request == pull_request)
803
759
804 return comments
760 return comments
805
761
806 @staticmethod
762 @staticmethod
807 def use_outdated_comments(pull_request):
763 def use_outdated_comments(pull_request):
808 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
764 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
809 settings = settings_model.get_general_settings()
765 settings = settings_model.get_general_settings()
810 return settings.get('rhodecode_use_outdated_comments', False)
766 return settings.get('rhodecode_use_outdated_comments', False)
811
767
812 def trigger_commit_comment_hook(self, repo, user, action, data=None):
768 def trigger_commit_comment_hook(self, repo, user, action, data=None):
813 repo = self._get_repo(repo)
769 repo = self._get_repo(repo)
814 target_scm = repo.scm_instance()
770 target_scm = repo.scm_instance()
815 if action == 'create':
771 if action == 'create':
816 trigger_hook = hooks_utils.trigger_comment_commit_hooks
772 trigger_hook = hooks_utils.trigger_comment_commit_hooks
817 elif action == 'edit':
773 elif action == 'edit':
818 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
774 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
819 else:
775 else:
820 return
776 return
821
777
822 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
778 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
823 repo, action, trigger_hook)
779 repo, action, trigger_hook)
824 trigger_hook(
780 trigger_hook(
825 username=user.username,
781 username=user.username,
826 repo_name=repo.repo_name,
782 repo_name=repo.repo_name,
827 repo_type=target_scm.alias,
783 repo_type=target_scm.alias,
828 repo=repo,
784 repo=repo,
829 data=data)
785 data=data)
830
786
831
787
832 def _parse_comment_line_number(line_no):
788 def _parse_comment_line_number(line_no):
833 """
789 """
834 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
790 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
835 """
791 """
836 old_line = None
792 old_line = None
837 new_line = None
793 new_line = None
838 if line_no.startswith('o'):
794 if line_no.startswith('o'):
839 old_line = int(line_no[1:])
795 old_line = int(line_no[1:])
840 elif line_no.startswith('n'):
796 elif line_no.startswith('n'):
841 new_line = int(line_no[1:])
797 new_line = int(line_no[1:])
842 else:
798 else:
843 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
799 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
844 return diffs.DiffLineNumber(old_line, new_line)
800 return diffs.DiffLineNumber(old_line, new_line)
845
801
846
802
847 def _diff_to_comment_line_number(diff_line):
803 def _diff_to_comment_line_number(diff_line):
848 if diff_line.new is not None:
804 if diff_line.new is not None:
849 return u'n{}'.format(diff_line.new)
805 return u'n{}'.format(diff_line.new)
850 elif diff_line.old is not None:
806 elif diff_line.old is not None:
851 return u'o{}'.format(diff_line.old)
807 return u'o{}'.format(diff_line.old)
852 return u''
808 return u''
853
809
854
810
855 def _diff_line_delta(a, b):
811 def _diff_line_delta(a, b):
856 if None not in (a.new, b.new):
812 if None not in (a.new, b.new):
857 return abs(a.new - b.new)
813 return abs(a.new - b.new)
858 elif None not in (a.old, b.old):
814 elif None not in (a.old, b.old):
859 return abs(a.old - b.old)
815 return abs(a.old - b.old)
860 else:
816 else:
861 raise ValueError(
817 raise ValueError(
862 "Cannot compute delta between {} and {}".format(a, b))
818 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now