##// END OF EJS Templates
emails: added new tags to status sent...
marcink -
r548:1e26c289 default
parent child Browse files
Show More
@@ -1,633 +1,635 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 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
24 from rhodecode.api import jsonrpc_method, JSONRPCError
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 has_repo_permissions, resolve_ref_or_error)
28 has_repo_permissions, resolve_ref_or_error)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import ChangesetCommentsModel
33 from rhodecode.model.comment import ChangesetCommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus
34 from rhodecode.model.db import Session, ChangesetStatus
35 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.pull_request import PullRequestModel
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 @jsonrpc_method()
41 @jsonrpc_method()
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 """
43 """
44 Get a pull request based on the given ID.
44 Get a pull request based on the given ID.
45
45
46 :param apiuser: This is filled automatically from the |authtoken|.
46 :param apiuser: This is filled automatically from the |authtoken|.
47 :type apiuser: AuthUser
47 :type apiuser: AuthUser
48 :param repoid: Repository name or repository ID from where the pull
48 :param repoid: Repository name or repository ID from where the pull
49 request was opened.
49 request was opened.
50 :type repoid: str or int
50 :type repoid: str or int
51 :param pullrequestid: ID of the requested pull request.
51 :param pullrequestid: ID of the requested pull request.
52 :type pullrequestid: int
52 :type pullrequestid: int
53
53
54 Example output:
54 Example output:
55
55
56 .. code-block:: bash
56 .. code-block:: bash
57
57
58 "id": <id_given_in_input>,
58 "id": <id_given_in_input>,
59 "result":
59 "result":
60 {
60 {
61 "pull_request_id": "<pull_request_id>",
61 "pull_request_id": "<pull_request_id>",
62 "url": "<url>",
62 "url": "<url>",
63 "title": "<title>",
63 "title": "<title>",
64 "description": "<description>",
64 "description": "<description>",
65 "status" : "<status>",
65 "status" : "<status>",
66 "created_on": "<date_time_created>",
66 "created_on": "<date_time_created>",
67 "updated_on": "<date_time_updated>",
67 "updated_on": "<date_time_updated>",
68 "commit_ids": [
68 "commit_ids": [
69 ...
69 ...
70 "<commit_id>",
70 "<commit_id>",
71 "<commit_id>",
71 "<commit_id>",
72 ...
72 ...
73 ],
73 ],
74 "review_status": "<review_status>",
74 "review_status": "<review_status>",
75 "mergeable": {
75 "mergeable": {
76 "status": "<bool>",
76 "status": "<bool>",
77 "message": "<message>",
77 "message": "<message>",
78 },
78 },
79 "source": {
79 "source": {
80 "clone_url": "<clone_url>",
80 "clone_url": "<clone_url>",
81 "repository": "<repository_name>",
81 "repository": "<repository_name>",
82 "reference":
82 "reference":
83 {
83 {
84 "name": "<name>",
84 "name": "<name>",
85 "type": "<type>",
85 "type": "<type>",
86 "commit_id": "<commit_id>",
86 "commit_id": "<commit_id>",
87 }
87 }
88 },
88 },
89 "target": {
89 "target": {
90 "clone_url": "<clone_url>",
90 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
91 "repository": "<repository_name>",
92 "reference":
92 "reference":
93 {
93 {
94 "name": "<name>",
94 "name": "<name>",
95 "type": "<type>",
95 "type": "<type>",
96 "commit_id": "<commit_id>",
96 "commit_id": "<commit_id>",
97 }
97 }
98 },
98 },
99 "author": <user_obj>,
99 "author": <user_obj>,
100 "reviewers": [
100 "reviewers": [
101 ...
101 ...
102 {
102 {
103 "user": "<user_obj>",
103 "user": "<user_obj>",
104 "review_status": "<review_status>",
104 "review_status": "<review_status>",
105 }
105 }
106 ...
106 ...
107 ]
107 ]
108 },
108 },
109 "error": null
109 "error": null
110 """
110 """
111 get_repo_or_error(repoid)
111 get_repo_or_error(repoid)
112 pull_request = get_pull_request_or_error(pullrequestid)
112 pull_request = get_pull_request_or_error(pullrequestid)
113 if not PullRequestModel().check_user_read(
113 if not PullRequestModel().check_user_read(
114 pull_request, apiuser, api=True):
114 pull_request, apiuser, api=True):
115 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
115 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
116 data = pull_request.get_api_data()
116 data = pull_request.get_api_data()
117 return data
117 return data
118
118
119
119
120 @jsonrpc_method()
120 @jsonrpc_method()
121 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
121 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
122 """
122 """
123 Get all pull requests from the repository specified in `repoid`.
123 Get all pull requests from the repository specified in `repoid`.
124
124
125 :param apiuser: This is filled automatically from the |authtoken|.
125 :param apiuser: This is filled automatically from the |authtoken|.
126 :type apiuser: AuthUser
126 :type apiuser: AuthUser
127 :param repoid: Repository name or repository ID.
127 :param repoid: Repository name or repository ID.
128 :type repoid: str or int
128 :type repoid: str or int
129 :param status: Only return pull requests with the specified status.
129 :param status: Only return pull requests with the specified status.
130 Valid options are.
130 Valid options are.
131 * ``new`` (default)
131 * ``new`` (default)
132 * ``open``
132 * ``open``
133 * ``closed``
133 * ``closed``
134 :type status: str
134 :type status: str
135
135
136 Example output:
136 Example output:
137
137
138 .. code-block:: bash
138 .. code-block:: bash
139
139
140 "id": <id_given_in_input>,
140 "id": <id_given_in_input>,
141 "result":
141 "result":
142 [
142 [
143 ...
143 ...
144 {
144 {
145 "pull_request_id": "<pull_request_id>",
145 "pull_request_id": "<pull_request_id>",
146 "url": "<url>",
146 "url": "<url>",
147 "title" : "<title>",
147 "title" : "<title>",
148 "description": "<description>",
148 "description": "<description>",
149 "status": "<status>",
149 "status": "<status>",
150 "created_on": "<date_time_created>",
150 "created_on": "<date_time_created>",
151 "updated_on": "<date_time_updated>",
151 "updated_on": "<date_time_updated>",
152 "commit_ids": [
152 "commit_ids": [
153 ...
153 ...
154 "<commit_id>",
154 "<commit_id>",
155 "<commit_id>",
155 "<commit_id>",
156 ...
156 ...
157 ],
157 ],
158 "review_status": "<review_status>",
158 "review_status": "<review_status>",
159 "mergeable": {
159 "mergeable": {
160 "status": "<bool>",
160 "status": "<bool>",
161 "message: "<message>",
161 "message: "<message>",
162 },
162 },
163 "source": {
163 "source": {
164 "clone_url": "<clone_url>",
164 "clone_url": "<clone_url>",
165 "reference":
165 "reference":
166 {
166 {
167 "name": "<name>",
167 "name": "<name>",
168 "type": "<type>",
168 "type": "<type>",
169 "commit_id": "<commit_id>",
169 "commit_id": "<commit_id>",
170 }
170 }
171 },
171 },
172 "target": {
172 "target": {
173 "clone_url": "<clone_url>",
173 "clone_url": "<clone_url>",
174 "reference":
174 "reference":
175 {
175 {
176 "name": "<name>",
176 "name": "<name>",
177 "type": "<type>",
177 "type": "<type>",
178 "commit_id": "<commit_id>",
178 "commit_id": "<commit_id>",
179 }
179 }
180 },
180 },
181 "author": <user_obj>,
181 "author": <user_obj>,
182 "reviewers": [
182 "reviewers": [
183 ...
183 ...
184 {
184 {
185 "user": "<user_obj>",
185 "user": "<user_obj>",
186 "review_status": "<review_status>",
186 "review_status": "<review_status>",
187 }
187 }
188 ...
188 ...
189 ]
189 ]
190 }
190 }
191 ...
191 ...
192 ],
192 ],
193 "error": null
193 "error": null
194
194
195 """
195 """
196 repo = get_repo_or_error(repoid)
196 repo = get_repo_or_error(repoid)
197 if not has_superadmin_permission(apiuser):
197 if not has_superadmin_permission(apiuser):
198 _perms = (
198 _perms = (
199 'repository.admin', 'repository.write', 'repository.read',)
199 'repository.admin', 'repository.write', 'repository.read',)
200 has_repo_permissions(apiuser, repoid, repo, _perms)
200 has_repo_permissions(apiuser, repoid, repo, _perms)
201
201
202 status = Optional.extract(status)
202 status = Optional.extract(status)
203 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
203 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
204 data = [pr.get_api_data() for pr in pull_requests]
204 data = [pr.get_api_data() for pr in pull_requests]
205 return data
205 return data
206
206
207
207
208 @jsonrpc_method()
208 @jsonrpc_method()
209 def merge_pull_request(request, apiuser, repoid, pullrequestid,
209 def merge_pull_request(request, apiuser, repoid, pullrequestid,
210 userid=Optional(OAttr('apiuser'))):
210 userid=Optional(OAttr('apiuser'))):
211 """
211 """
212 Merge the pull request specified by `pullrequestid` into its target
212 Merge the pull request specified by `pullrequestid` into its target
213 repository.
213 repository.
214
214
215 :param apiuser: This is filled automatically from the |authtoken|.
215 :param apiuser: This is filled automatically from the |authtoken|.
216 :type apiuser: AuthUser
216 :type apiuser: AuthUser
217 :param repoid: The Repository name or repository ID of the
217 :param repoid: The Repository name or repository ID of the
218 target repository to which the |pr| is to be merged.
218 target repository to which the |pr| is to be merged.
219 :type repoid: str or int
219 :type repoid: str or int
220 :param pullrequestid: ID of the pull request which shall be merged.
220 :param pullrequestid: ID of the pull request which shall be merged.
221 :type pullrequestid: int
221 :type pullrequestid: int
222 :param userid: Merge the pull request as this user.
222 :param userid: Merge the pull request as this user.
223 :type userid: Optional(str or int)
223 :type userid: Optional(str or int)
224
224
225 Example output:
225 Example output:
226
226
227 .. code-block:: bash
227 .. code-block:: bash
228
228
229 "id": <id_given_in_input>,
229 "id": <id_given_in_input>,
230 "result":
230 "result":
231 {
231 {
232 "executed": "<bool>",
232 "executed": "<bool>",
233 "failure_reason": "<int>",
233 "failure_reason": "<int>",
234 "merge_commit_id": "<merge_commit_id>",
234 "merge_commit_id": "<merge_commit_id>",
235 "possible": "<bool>"
235 "possible": "<bool>"
236 },
236 },
237 "error": null
237 "error": null
238
238
239 """
239 """
240 repo = get_repo_or_error(repoid)
240 repo = get_repo_or_error(repoid)
241 if not isinstance(userid, Optional):
241 if not isinstance(userid, Optional):
242 if (has_superadmin_permission(apiuser) or
242 if (has_superadmin_permission(apiuser) or
243 HasRepoPermissionAnyApi('repository.admin')(
243 HasRepoPermissionAnyApi('repository.admin')(
244 user=apiuser, repo_name=repo.repo_name)):
244 user=apiuser, repo_name=repo.repo_name)):
245 apiuser = get_user_or_error(userid)
245 apiuser = get_user_or_error(userid)
246 else:
246 else:
247 raise JSONRPCError('userid is not the same as your user')
247 raise JSONRPCError('userid is not the same as your user')
248
248
249 pull_request = get_pull_request_or_error(pullrequestid)
249 pull_request = get_pull_request_or_error(pullrequestid)
250 if not PullRequestModel().check_user_merge(
250 if not PullRequestModel().check_user_merge(
251 pull_request, apiuser, api=True):
251 pull_request, apiuser, api=True):
252 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
252 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
253 if pull_request.is_closed():
253 if pull_request.is_closed():
254 raise JSONRPCError(
254 raise JSONRPCError(
255 'pull request `%s` merge failed, pull request is closed' % (
255 'pull request `%s` merge failed, pull request is closed' % (
256 pullrequestid,))
256 pullrequestid,))
257
257
258 target_repo = pull_request.target_repo
258 target_repo = pull_request.target_repo
259 extras = vcs_operation_context(
259 extras = vcs_operation_context(
260 request.environ, repo_name=target_repo.repo_name,
260 request.environ, repo_name=target_repo.repo_name,
261 username=apiuser.username, action='push',
261 username=apiuser.username, action='push',
262 scm=target_repo.repo_type)
262 scm=target_repo.repo_type)
263 data = PullRequestModel().merge(pull_request, apiuser, extras=extras)
263 data = PullRequestModel().merge(pull_request, apiuser, extras=extras)
264 if data.executed:
264 if data.executed:
265 PullRequestModel().close_pull_request(
265 PullRequestModel().close_pull_request(
266 pull_request.pull_request_id, apiuser)
266 pull_request.pull_request_id, apiuser)
267
267
268 Session().commit()
268 Session().commit()
269 return data
269 return data
270
270
271
271
272 @jsonrpc_method()
272 @jsonrpc_method()
273 def close_pull_request(request, apiuser, repoid, pullrequestid,
273 def close_pull_request(request, apiuser, repoid, pullrequestid,
274 userid=Optional(OAttr('apiuser'))):
274 userid=Optional(OAttr('apiuser'))):
275 """
275 """
276 Close the pull request specified by `pullrequestid`.
276 Close the pull request specified by `pullrequestid`.
277
277
278 :param apiuser: This is filled automatically from the |authtoken|.
278 :param apiuser: This is filled automatically from the |authtoken|.
279 :type apiuser: AuthUser
279 :type apiuser: AuthUser
280 :param repoid: Repository name or repository ID to which the pull
280 :param repoid: Repository name or repository ID to which the pull
281 request belongs.
281 request belongs.
282 :type repoid: str or int
282 :type repoid: str or int
283 :param pullrequestid: ID of the pull request to be closed.
283 :param pullrequestid: ID of the pull request to be closed.
284 :type pullrequestid: int
284 :type pullrequestid: int
285 :param userid: Close the pull request as this user.
285 :param userid: Close the pull request as this user.
286 :type userid: Optional(str or int)
286 :type userid: Optional(str or int)
287
287
288 Example output:
288 Example output:
289
289
290 .. code-block:: bash
290 .. code-block:: bash
291
291
292 "id": <id_given_in_input>,
292 "id": <id_given_in_input>,
293 "result":
293 "result":
294 {
294 {
295 "pull_request_id": "<int>",
295 "pull_request_id": "<int>",
296 "closed": "<bool>"
296 "closed": "<bool>"
297 },
297 },
298 "error": null
298 "error": null
299
299
300 """
300 """
301 repo = get_repo_or_error(repoid)
301 repo = get_repo_or_error(repoid)
302 if not isinstance(userid, Optional):
302 if not isinstance(userid, Optional):
303 if (has_superadmin_permission(apiuser) or
303 if (has_superadmin_permission(apiuser) or
304 HasRepoPermissionAnyApi('repository.admin')(
304 HasRepoPermissionAnyApi('repository.admin')(
305 user=apiuser, repo_name=repo.repo_name)):
305 user=apiuser, repo_name=repo.repo_name)):
306 apiuser = get_user_or_error(userid)
306 apiuser = get_user_or_error(userid)
307 else:
307 else:
308 raise JSONRPCError('userid is not the same as your user')
308 raise JSONRPCError('userid is not the same as your user')
309
309
310 pull_request = get_pull_request_or_error(pullrequestid)
310 pull_request = get_pull_request_or_error(pullrequestid)
311 if not PullRequestModel().check_user_update(
311 if not PullRequestModel().check_user_update(
312 pull_request, apiuser, api=True):
312 pull_request, apiuser, api=True):
313 raise JSONRPCError(
313 raise JSONRPCError(
314 'pull request `%s` close failed, no permission to close.' % (
314 'pull request `%s` close failed, no permission to close.' % (
315 pullrequestid,))
315 pullrequestid,))
316 if pull_request.is_closed():
316 if pull_request.is_closed():
317 raise JSONRPCError(
317 raise JSONRPCError(
318 'pull request `%s` is already closed' % (pullrequestid,))
318 'pull request `%s` is already closed' % (pullrequestid,))
319
319
320 PullRequestModel().close_pull_request(
320 PullRequestModel().close_pull_request(
321 pull_request.pull_request_id, apiuser)
321 pull_request.pull_request_id, apiuser)
322 Session().commit()
322 Session().commit()
323 data = {
323 data = {
324 'pull_request_id': pull_request.pull_request_id,
324 'pull_request_id': pull_request.pull_request_id,
325 'closed': True,
325 'closed': True,
326 }
326 }
327 return data
327 return data
328
328
329
329
330 @jsonrpc_method()
330 @jsonrpc_method()
331 def comment_pull_request(request, apiuser, repoid, pullrequestid,
331 def comment_pull_request(request, apiuser, repoid, pullrequestid,
332 message=Optional(None), status=Optional(None),
332 message=Optional(None), status=Optional(None),
333 userid=Optional(OAttr('apiuser'))):
333 userid=Optional(OAttr('apiuser'))):
334 """
334 """
335 Comment on the pull request specified with the `pullrequestid`,
335 Comment on the pull request specified with the `pullrequestid`,
336 in the |repo| specified by the `repoid`, and optionally change the
336 in the |repo| specified by the `repoid`, and optionally change the
337 review status.
337 review status.
338
338
339 :param apiuser: This is filled automatically from the |authtoken|.
339 :param apiuser: This is filled automatically from the |authtoken|.
340 :type apiuser: AuthUser
340 :type apiuser: AuthUser
341 :param repoid: The repository name or repository ID.
341 :param repoid: The repository name or repository ID.
342 :type repoid: str or int
342 :type repoid: str or int
343 :param pullrequestid: The pull request ID.
343 :param pullrequestid: The pull request ID.
344 :type pullrequestid: int
344 :type pullrequestid: int
345 :param message: The text content of the comment.
345 :param message: The text content of the comment.
346 :type message: str
346 :type message: str
347 :param status: (**Optional**) Set the approval status of the pull
347 :param status: (**Optional**) Set the approval status of the pull
348 request. Valid options are:
348 request. Valid options are:
349 * not_reviewed
349 * not_reviewed
350 * approved
350 * approved
351 * rejected
351 * rejected
352 * under_review
352 * under_review
353 :type status: str
353 :type status: str
354 :param userid: Comment on the pull request as this user
354 :param userid: Comment on the pull request as this user
355 :type userid: Optional(str or int)
355 :type userid: Optional(str or int)
356
356
357 Example output:
357 Example output:
358
358
359 .. code-block:: bash
359 .. code-block:: bash
360
360
361 id : <id_given_in_input>
361 id : <id_given_in_input>
362 result :
362 result :
363 {
363 {
364 "pull_request_id": "<Integer>",
364 "pull_request_id": "<Integer>",
365 "comment_id": "<Integer>"
365 "comment_id": "<Integer>"
366 }
366 }
367 error : null
367 error : null
368 """
368 """
369 repo = get_repo_or_error(repoid)
369 repo = get_repo_or_error(repoid)
370 if not isinstance(userid, Optional):
370 if not isinstance(userid, Optional):
371 if (has_superadmin_permission(apiuser) or
371 if (has_superadmin_permission(apiuser) or
372 HasRepoPermissionAnyApi('repository.admin')(
372 HasRepoPermissionAnyApi('repository.admin')(
373 user=apiuser, repo_name=repo.repo_name)):
373 user=apiuser, repo_name=repo.repo_name)):
374 apiuser = get_user_or_error(userid)
374 apiuser = get_user_or_error(userid)
375 else:
375 else:
376 raise JSONRPCError('userid is not the same as your user')
376 raise JSONRPCError('userid is not the same as your user')
377
377
378 pull_request = get_pull_request_or_error(pullrequestid)
378 pull_request = get_pull_request_or_error(pullrequestid)
379 if not PullRequestModel().check_user_read(
379 if not PullRequestModel().check_user_read(
380 pull_request, apiuser, api=True):
380 pull_request, apiuser, api=True):
381 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
381 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
382 message = Optional.extract(message)
382 message = Optional.extract(message)
383 status = Optional.extract(status)
383 status = Optional.extract(status)
384 if not message and not status:
384 if not message and not status:
385 raise JSONRPCError('message and status parameter missing')
385 raise JSONRPCError('message and status parameter missing')
386
386
387 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
387 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
388 status is not None):
388 status is not None):
389 raise JSONRPCError('unknown comment status`%s`' % status)
389 raise JSONRPCError('unknown comment status`%s`' % status)
390
390
391 allowed_to_change_status = PullRequestModel().check_user_change_status(
391 allowed_to_change_status = PullRequestModel().check_user_change_status(
392 pull_request, apiuser)
392 pull_request, apiuser)
393 text = message
393 text = message
394 if status and allowed_to_change_status:
394 if status and allowed_to_change_status:
395 st_message = (('Status change %(transition_icon)s %(status)s')
395 st_message = (('Status change %(transition_icon)s %(status)s')
396 % {'transition_icon': '>',
396 % {'transition_icon': '>',
397 'status': ChangesetStatus.get_status_lbl(status)})
397 'status': ChangesetStatus.get_status_lbl(status)})
398 text = message or st_message
398 text = message or st_message
399
399
400 rc_config = SettingsModel().get_all_settings()
400 rc_config = SettingsModel().get_all_settings()
401 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
401 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
402 comment = ChangesetCommentsModel().create(
402 comment = ChangesetCommentsModel().create(
403 text=text,
403 text=text,
404 repo=pull_request.target_repo.repo_id,
404 repo=pull_request.target_repo.repo_id,
405 user=apiuser.user_id,
405 user=apiuser.user_id,
406 pull_request=pull_request.pull_request_id,
406 pull_request=pull_request.pull_request_id,
407 f_path=None,
407 f_path=None,
408 line_no=None,
408 line_no=None,
409 status_change=(ChangesetStatus.get_status_lbl(status)
409 status_change=(ChangesetStatus.get_status_lbl(status)
410 if status and allowed_to_change_status else None),
410 if status and allowed_to_change_status else None),
411 status_change_type=(status
412 if status and allowed_to_change_status else None),
411 closing_pr=False,
413 closing_pr=False,
412 renderer=renderer
414 renderer=renderer
413 )
415 )
414
416
415 if allowed_to_change_status and status:
417 if allowed_to_change_status and status:
416 ChangesetStatusModel().set_status(
418 ChangesetStatusModel().set_status(
417 pull_request.target_repo.repo_id,
419 pull_request.target_repo.repo_id,
418 status,
420 status,
419 apiuser.user_id,
421 apiuser.user_id,
420 comment,
422 comment,
421 pull_request=pull_request.pull_request_id
423 pull_request=pull_request.pull_request_id
422 )
424 )
423 Session().flush()
425 Session().flush()
424
426
425 Session().commit()
427 Session().commit()
426 data = {
428 data = {
427 'pull_request_id': pull_request.pull_request_id,
429 'pull_request_id': pull_request.pull_request_id,
428 'comment_id': comment.comment_id,
430 'comment_id': comment.comment_id,
429 'status': status
431 'status': status
430 }
432 }
431 return data
433 return data
432
434
433
435
434 @jsonrpc_method()
436 @jsonrpc_method()
435 def create_pull_request(
437 def create_pull_request(
436 request, apiuser, source_repo, target_repo, source_ref, target_ref,
438 request, apiuser, source_repo, target_repo, source_ref, target_ref,
437 title, description=Optional(''), reviewers=Optional(None)):
439 title, description=Optional(''), reviewers=Optional(None)):
438 """
440 """
439 Creates a new pull request.
441 Creates a new pull request.
440
442
441 Accepts refs in the following formats:
443 Accepts refs in the following formats:
442
444
443 * branch:<branch_name>:<sha>
445 * branch:<branch_name>:<sha>
444 * branch:<branch_name>
446 * branch:<branch_name>
445 * bookmark:<bookmark_name>:<sha> (Mercurial only)
447 * bookmark:<bookmark_name>:<sha> (Mercurial only)
446 * bookmark:<bookmark_name> (Mercurial only)
448 * bookmark:<bookmark_name> (Mercurial only)
447
449
448 :param apiuser: This is filled automatically from the |authtoken|.
450 :param apiuser: This is filled automatically from the |authtoken|.
449 :type apiuser: AuthUser
451 :type apiuser: AuthUser
450 :param source_repo: Set the source repository name.
452 :param source_repo: Set the source repository name.
451 :type source_repo: str
453 :type source_repo: str
452 :param target_repo: Set the target repository name.
454 :param target_repo: Set the target repository name.
453 :type target_repo: str
455 :type target_repo: str
454 :param source_ref: Set the source ref name.
456 :param source_ref: Set the source ref name.
455 :type source_ref: str
457 :type source_ref: str
456 :param target_ref: Set the target ref name.
458 :param target_ref: Set the target ref name.
457 :type target_ref: str
459 :type target_ref: str
458 :param title: Set the pull request title.
460 :param title: Set the pull request title.
459 :type title: str
461 :type title: str
460 :param description: Set the pull request description.
462 :param description: Set the pull request description.
461 :type description: Optional(str)
463 :type description: Optional(str)
462 :param reviewers: Set the new pull request reviewers list.
464 :param reviewers: Set the new pull request reviewers list.
463 :type reviewers: Optional(list)
465 :type reviewers: Optional(list)
464 """
466 """
465 source = get_repo_or_error(source_repo)
467 source = get_repo_or_error(source_repo)
466 target = get_repo_or_error(target_repo)
468 target = get_repo_or_error(target_repo)
467 if not has_superadmin_permission(apiuser):
469 if not has_superadmin_permission(apiuser):
468 _perms = ('repository.admin', 'repository.write', 'repository.read',)
470 _perms = ('repository.admin', 'repository.write', 'repository.read',)
469 has_repo_permissions(apiuser, source_repo, source, _perms)
471 has_repo_permissions(apiuser, source_repo, source, _perms)
470
472
471 full_source_ref = resolve_ref_or_error(source_ref, source)
473 full_source_ref = resolve_ref_or_error(source_ref, source)
472 full_target_ref = resolve_ref_or_error(target_ref, target)
474 full_target_ref = resolve_ref_or_error(target_ref, target)
473 source_commit = get_commit_or_error(full_source_ref, source)
475 source_commit = get_commit_or_error(full_source_ref, source)
474 target_commit = get_commit_or_error(full_target_ref, target)
476 target_commit = get_commit_or_error(full_target_ref, target)
475 source_scm = source.scm_instance()
477 source_scm = source.scm_instance()
476 target_scm = target.scm_instance()
478 target_scm = target.scm_instance()
477
479
478 commit_ranges = target_scm.compare(
480 commit_ranges = target_scm.compare(
479 target_commit.raw_id, source_commit.raw_id, source_scm,
481 target_commit.raw_id, source_commit.raw_id, source_scm,
480 merge=True, pre_load=[])
482 merge=True, pre_load=[])
481
483
482 ancestor = target_scm.get_common_ancestor(
484 ancestor = target_scm.get_common_ancestor(
483 target_commit.raw_id, source_commit.raw_id, source_scm)
485 target_commit.raw_id, source_commit.raw_id, source_scm)
484
486
485 if not commit_ranges:
487 if not commit_ranges:
486 raise JSONRPCError('no commits found')
488 raise JSONRPCError('no commits found')
487
489
488 if not ancestor:
490 if not ancestor:
489 raise JSONRPCError('no common ancestor found')
491 raise JSONRPCError('no common ancestor found')
490
492
491 reviewer_names = Optional.extract(reviewers) or []
493 reviewer_names = Optional.extract(reviewers) or []
492 if not isinstance(reviewer_names, list):
494 if not isinstance(reviewer_names, list):
493 raise JSONRPCError('reviewers should be specified as a list')
495 raise JSONRPCError('reviewers should be specified as a list')
494
496
495 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
497 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
496 reviewer_ids = [u.user_id for u in reviewer_users]
498 reviewer_ids = [u.user_id for u in reviewer_users]
497
499
498 pull_request_model = PullRequestModel()
500 pull_request_model = PullRequestModel()
499 pull_request = pull_request_model.create(
501 pull_request = pull_request_model.create(
500 created_by=apiuser.user_id,
502 created_by=apiuser.user_id,
501 source_repo=source_repo,
503 source_repo=source_repo,
502 source_ref=full_source_ref,
504 source_ref=full_source_ref,
503 target_repo=target_repo,
505 target_repo=target_repo,
504 target_ref=full_target_ref,
506 target_ref=full_target_ref,
505 revisions=reversed(
507 revisions=reversed(
506 [commit.raw_id for commit in reversed(commit_ranges)]),
508 [commit.raw_id for commit in reversed(commit_ranges)]),
507 reviewers=reviewer_ids,
509 reviewers=reviewer_ids,
508 title=title,
510 title=title,
509 description=Optional.extract(description)
511 description=Optional.extract(description)
510 )
512 )
511
513
512 Session().commit()
514 Session().commit()
513 data = {
515 data = {
514 'msg': 'Created new pull request `{}`'.format(title),
516 'msg': 'Created new pull request `{}`'.format(title),
515 'pull_request_id': pull_request.pull_request_id,
517 'pull_request_id': pull_request.pull_request_id,
516 }
518 }
517 return data
519 return data
518
520
519
521
520 @jsonrpc_method()
522 @jsonrpc_method()
521 def update_pull_request(
523 def update_pull_request(
522 request, apiuser, repoid, pullrequestid, title=Optional(''),
524 request, apiuser, repoid, pullrequestid, title=Optional(''),
523 description=Optional(''), reviewers=Optional(None),
525 description=Optional(''), reviewers=Optional(None),
524 update_commits=Optional(None), close_pull_request=Optional(None)):
526 update_commits=Optional(None), close_pull_request=Optional(None)):
525 """
527 """
526 Updates a pull request.
528 Updates a pull request.
527
529
528 :param apiuser: This is filled automatically from the |authtoken|.
530 :param apiuser: This is filled automatically from the |authtoken|.
529 :type apiuser: AuthUser
531 :type apiuser: AuthUser
530 :param repoid: The repository name or repository ID.
532 :param repoid: The repository name or repository ID.
531 :type repoid: str or int
533 :type repoid: str or int
532 :param pullrequestid: The pull request ID.
534 :param pullrequestid: The pull request ID.
533 :type pullrequestid: int
535 :type pullrequestid: int
534 :param title: Set the pull request title.
536 :param title: Set the pull request title.
535 :type title: str
537 :type title: str
536 :param description: Update pull request description.
538 :param description: Update pull request description.
537 :type description: Optional(str)
539 :type description: Optional(str)
538 :param reviewers: Update pull request reviewers list with new value.
540 :param reviewers: Update pull request reviewers list with new value.
539 :type reviewers: Optional(list)
541 :type reviewers: Optional(list)
540 :param update_commits: Trigger update of commits for this pull request
542 :param update_commits: Trigger update of commits for this pull request
541 :type: update_commits: Optional(bool)
543 :type: update_commits: Optional(bool)
542 :param close_pull_request: Close this pull request with rejected state
544 :param close_pull_request: Close this pull request with rejected state
543 :type: close_pull_request: Optional(bool)
545 :type: close_pull_request: Optional(bool)
544
546
545 Example output:
547 Example output:
546
548
547 .. code-block:: bash
549 .. code-block:: bash
548
550
549 id : <id_given_in_input>
551 id : <id_given_in_input>
550 result :
552 result :
551 {
553 {
552 "msg": "Updated pull request `63`",
554 "msg": "Updated pull request `63`",
553 "pull_request": <pull_request_object>,
555 "pull_request": <pull_request_object>,
554 "updated_reviewers": {
556 "updated_reviewers": {
555 "added": [
557 "added": [
556 "username"
558 "username"
557 ],
559 ],
558 "removed": []
560 "removed": []
559 },
561 },
560 "updated_commits": {
562 "updated_commits": {
561 "added": [
563 "added": [
562 "<sha1_hash>"
564 "<sha1_hash>"
563 ],
565 ],
564 "common": [
566 "common": [
565 "<sha1_hash>",
567 "<sha1_hash>",
566 "<sha1_hash>",
568 "<sha1_hash>",
567 ],
569 ],
568 "removed": []
570 "removed": []
569 }
571 }
570 }
572 }
571 error : null
573 error : null
572 """
574 """
573
575
574 repo = get_repo_or_error(repoid)
576 repo = get_repo_or_error(repoid)
575 pull_request = get_pull_request_or_error(pullrequestid)
577 pull_request = get_pull_request_or_error(pullrequestid)
576 if not PullRequestModel().check_user_update(
578 if not PullRequestModel().check_user_update(
577 pull_request, apiuser, api=True):
579 pull_request, apiuser, api=True):
578 raise JSONRPCError(
580 raise JSONRPCError(
579 'pull request `%s` update failed, no permission to update.' % (
581 'pull request `%s` update failed, no permission to update.' % (
580 pullrequestid,))
582 pullrequestid,))
581 if pull_request.is_closed():
583 if pull_request.is_closed():
582 raise JSONRPCError(
584 raise JSONRPCError(
583 'pull request `%s` update failed, pull request is closed' % (
585 'pull request `%s` update failed, pull request is closed' % (
584 pullrequestid,))
586 pullrequestid,))
585
587
586 reviewer_names = Optional.extract(reviewers) or []
588 reviewer_names = Optional.extract(reviewers) or []
587 if not isinstance(reviewer_names, list):
589 if not isinstance(reviewer_names, list):
588 raise JSONRPCError('reviewers should be specified as a list')
590 raise JSONRPCError('reviewers should be specified as a list')
589
591
590 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
592 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
591 reviewer_ids = [u.user_id for u in reviewer_users]
593 reviewer_ids = [u.user_id for u in reviewer_users]
592
594
593 title = Optional.extract(title)
595 title = Optional.extract(title)
594 description = Optional.extract(description)
596 description = Optional.extract(description)
595 if title or description:
597 if title or description:
596 PullRequestModel().edit(
598 PullRequestModel().edit(
597 pull_request, title or pull_request.title,
599 pull_request, title or pull_request.title,
598 description or pull_request.description)
600 description or pull_request.description)
599 Session().commit()
601 Session().commit()
600
602
601 commit_changes = {"added": [], "common": [], "removed": []}
603 commit_changes = {"added": [], "common": [], "removed": []}
602 if str2bool(Optional.extract(update_commits)):
604 if str2bool(Optional.extract(update_commits)):
603 if PullRequestModel().has_valid_update_type(pull_request):
605 if PullRequestModel().has_valid_update_type(pull_request):
604 _version, _commit_changes = PullRequestModel().update_commits(
606 _version, _commit_changes = PullRequestModel().update_commits(
605 pull_request)
607 pull_request)
606 commit_changes = _commit_changes or commit_changes
608 commit_changes = _commit_changes or commit_changes
607 Session().commit()
609 Session().commit()
608
610
609 reviewers_changes = {"added": [], "removed": []}
611 reviewers_changes = {"added": [], "removed": []}
610 if reviewer_ids:
612 if reviewer_ids:
611 added_reviewers, removed_reviewers = \
613 added_reviewers, removed_reviewers = \
612 PullRequestModel().update_reviewers(pull_request, reviewer_ids)
614 PullRequestModel().update_reviewers(pull_request, reviewer_ids)
613
615
614 reviewers_changes['added'] = sorted(
616 reviewers_changes['added'] = sorted(
615 [get_user_or_error(n).username for n in added_reviewers])
617 [get_user_or_error(n).username for n in added_reviewers])
616 reviewers_changes['removed'] = sorted(
618 reviewers_changes['removed'] = sorted(
617 [get_user_or_error(n).username for n in removed_reviewers])
619 [get_user_or_error(n).username for n in removed_reviewers])
618 Session().commit()
620 Session().commit()
619
621
620 if str2bool(Optional.extract(close_pull_request)):
622 if str2bool(Optional.extract(close_pull_request)):
621 PullRequestModel().close_pull_request_with_comment(
623 PullRequestModel().close_pull_request_with_comment(
622 pull_request, apiuser, repo)
624 pull_request, apiuser, repo)
623 Session().commit()
625 Session().commit()
624
626
625 data = {
627 data = {
626 'msg': 'Updated pull request `{}`'.format(
628 'msg': 'Updated pull request `{}`'.format(
627 pull_request.pull_request_id),
629 pull_request.pull_request_id),
628 'pull_request': pull_request.get_api_data(),
630 'pull_request': pull_request.get_api_data(),
629 'updated_commits': commit_changes,
631 'updated_commits': commit_changes,
630 'updated_reviewers': reviewers_changes
632 'updated_reviewers': reviewers_changes
631 }
633 }
632 return data
634 return data
633
635
@@ -1,1886 +1,1888 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 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 colander
24 import colander
25
25
26 from rhodecode import BACKENDS
26 from rhodecode import BACKENDS
27 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden, json
27 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden, json
28 from rhodecode.api.utils import (
28 from rhodecode.api.utils import (
29 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
30 get_user_group_or_error, get_user_or_error, has_repo_permissions,
30 get_user_group_or_error, get_user_or_error, has_repo_permissions,
31 get_perm_or_error, store_update, get_repo_group_or_error, parse_args,
31 get_perm_or_error, store_update, get_repo_group_or_error, parse_args,
32 get_origin, build_commit_data)
32 get_origin, build_commit_data)
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
34 HasPermissionAnyApi, HasRepoGroupPermissionAnyApi,
35 HasUserGroupPermissionAnyApi)
35 HasUserGroupPermissionAnyApi)
36 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
36 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
37 from rhodecode.lib.utils import map_groups
37 from rhodecode.lib.utils import map_groups
38 from rhodecode.lib.utils2 import str2bool, time_to_datetime
38 from rhodecode.lib.utils2 import str2bool, time_to_datetime
39 from rhodecode.model.changeset_status import ChangesetStatusModel
39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 from rhodecode.model.comment import ChangesetCommentsModel
40 from rhodecode.model.comment import ChangesetCommentsModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 Session, ChangesetStatus, RepositoryField, Repository)
42 Session, ChangesetStatus, RepositoryField, Repository)
43 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.repo_group import RepoGroupModel
45 from rhodecode.model.scm import ScmModel, RepoList
45 from rhodecode.model.scm import ScmModel, RepoList
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 from rhodecode.model.validation_schema.schemas import repo_schema
47 from rhodecode.model.validation_schema.schemas import repo_schema
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 @jsonrpc_method()
52 @jsonrpc_method()
53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 """
54 """
55 Gets an existing repository by its name or repository_id.
55 Gets an existing repository by its name or repository_id.
56
56
57 The members section so the output returns users groups or users
57 The members section so the output returns users groups or users
58 associated with that repository.
58 associated with that repository.
59
59
60 This command can only be run using an |authtoken| with admin rights,
60 This command can only be run using an |authtoken| with admin rights,
61 or users with at least read rights to the |repo|.
61 or users with at least read rights to the |repo|.
62
62
63 :param apiuser: This is filled automatically from the |authtoken|.
63 :param apiuser: This is filled automatically from the |authtoken|.
64 :type apiuser: AuthUser
64 :type apiuser: AuthUser
65 :param repoid: The repository name or repository id.
65 :param repoid: The repository name or repository id.
66 :type repoid: str or int
66 :type repoid: str or int
67 :param cache: use the cached value for last changeset
67 :param cache: use the cached value for last changeset
68 :type: cache: Optional(bool)
68 :type: cache: Optional(bool)
69
69
70 Example output:
70 Example output:
71
71
72 .. code-block:: bash
72 .. code-block:: bash
73
73
74 {
74 {
75 "error": null,
75 "error": null,
76 "id": <repo_id>,
76 "id": <repo_id>,
77 "result": {
77 "result": {
78 "clone_uri": null,
78 "clone_uri": null,
79 "created_on": "timestamp",
79 "created_on": "timestamp",
80 "description": "repo description",
80 "description": "repo description",
81 "enable_downloads": false,
81 "enable_downloads": false,
82 "enable_locking": false,
82 "enable_locking": false,
83 "enable_statistics": false,
83 "enable_statistics": false,
84 "followers": [
84 "followers": [
85 {
85 {
86 "active": true,
86 "active": true,
87 "admin": false,
87 "admin": false,
88 "api_key": "****************************************",
88 "api_key": "****************************************",
89 "api_keys": [
89 "api_keys": [
90 "****************************************"
90 "****************************************"
91 ],
91 ],
92 "email": "user@example.com",
92 "email": "user@example.com",
93 "emails": [
93 "emails": [
94 "user@example.com"
94 "user@example.com"
95 ],
95 ],
96 "extern_name": "rhodecode",
96 "extern_name": "rhodecode",
97 "extern_type": "rhodecode",
97 "extern_type": "rhodecode",
98 "firstname": "username",
98 "firstname": "username",
99 "ip_addresses": [],
99 "ip_addresses": [],
100 "language": null,
100 "language": null,
101 "last_login": "2015-09-16T17:16:35.854",
101 "last_login": "2015-09-16T17:16:35.854",
102 "lastname": "surname",
102 "lastname": "surname",
103 "user_id": <user_id>,
103 "user_id": <user_id>,
104 "username": "name"
104 "username": "name"
105 }
105 }
106 ],
106 ],
107 "fork_of": "parent-repo",
107 "fork_of": "parent-repo",
108 "landing_rev": [
108 "landing_rev": [
109 "rev",
109 "rev",
110 "tip"
110 "tip"
111 ],
111 ],
112 "last_changeset": {
112 "last_changeset": {
113 "author": "User <user@example.com>",
113 "author": "User <user@example.com>",
114 "branch": "default",
114 "branch": "default",
115 "date": "timestamp",
115 "date": "timestamp",
116 "message": "last commit message",
116 "message": "last commit message",
117 "parents": [
117 "parents": [
118 {
118 {
119 "raw_id": "commit-id"
119 "raw_id": "commit-id"
120 }
120 }
121 ],
121 ],
122 "raw_id": "commit-id",
122 "raw_id": "commit-id",
123 "revision": <revision number>,
123 "revision": <revision number>,
124 "short_id": "short id"
124 "short_id": "short id"
125 },
125 },
126 "lock_reason": null,
126 "lock_reason": null,
127 "locked_by": null,
127 "locked_by": null,
128 "locked_date": null,
128 "locked_date": null,
129 "members": [
129 "members": [
130 {
130 {
131 "name": "super-admin-name",
131 "name": "super-admin-name",
132 "origin": "super-admin",
132 "origin": "super-admin",
133 "permission": "repository.admin",
133 "permission": "repository.admin",
134 "type": "user"
134 "type": "user"
135 },
135 },
136 {
136 {
137 "name": "owner-name",
137 "name": "owner-name",
138 "origin": "owner",
138 "origin": "owner",
139 "permission": "repository.admin",
139 "permission": "repository.admin",
140 "type": "user"
140 "type": "user"
141 },
141 },
142 {
142 {
143 "name": "user-group-name",
143 "name": "user-group-name",
144 "origin": "permission",
144 "origin": "permission",
145 "permission": "repository.write",
145 "permission": "repository.write",
146 "type": "user_group"
146 "type": "user_group"
147 }
147 }
148 ],
148 ],
149 "owner": "owner-name",
149 "owner": "owner-name",
150 "permissions": [
150 "permissions": [
151 {
151 {
152 "name": "super-admin-name",
152 "name": "super-admin-name",
153 "origin": "super-admin",
153 "origin": "super-admin",
154 "permission": "repository.admin",
154 "permission": "repository.admin",
155 "type": "user"
155 "type": "user"
156 },
156 },
157 {
157 {
158 "name": "owner-name",
158 "name": "owner-name",
159 "origin": "owner",
159 "origin": "owner",
160 "permission": "repository.admin",
160 "permission": "repository.admin",
161 "type": "user"
161 "type": "user"
162 },
162 },
163 {
163 {
164 "name": "user-group-name",
164 "name": "user-group-name",
165 "origin": "permission",
165 "origin": "permission",
166 "permission": "repository.write",
166 "permission": "repository.write",
167 "type": "user_group"
167 "type": "user_group"
168 }
168 }
169 ],
169 ],
170 "private": true,
170 "private": true,
171 "repo_id": 676,
171 "repo_id": 676,
172 "repo_name": "user-group/repo-name",
172 "repo_name": "user-group/repo-name",
173 "repo_type": "hg"
173 "repo_type": "hg"
174 }
174 }
175 }
175 }
176 """
176 """
177
177
178 repo = get_repo_or_error(repoid)
178 repo = get_repo_or_error(repoid)
179 cache = Optional.extract(cache)
179 cache = Optional.extract(cache)
180 include_secrets = False
180 include_secrets = False
181 if has_superadmin_permission(apiuser):
181 if has_superadmin_permission(apiuser):
182 include_secrets = True
182 include_secrets = True
183 else:
183 else:
184 # check if we have at least read permission for this repo !
184 # check if we have at least read permission for this repo !
185 _perms = (
185 _perms = (
186 'repository.admin', 'repository.write', 'repository.read',)
186 'repository.admin', 'repository.write', 'repository.read',)
187 has_repo_permissions(apiuser, repoid, repo, _perms)
187 has_repo_permissions(apiuser, repoid, repo, _perms)
188
188
189 permissions = []
189 permissions = []
190 for _user in repo.permissions():
190 for _user in repo.permissions():
191 user_data = {
191 user_data = {
192 'name': _user.username,
192 'name': _user.username,
193 'permission': _user.permission,
193 'permission': _user.permission,
194 'origin': get_origin(_user),
194 'origin': get_origin(_user),
195 'type': "user",
195 'type': "user",
196 }
196 }
197 permissions.append(user_data)
197 permissions.append(user_data)
198
198
199 for _user_group in repo.permission_user_groups():
199 for _user_group in repo.permission_user_groups():
200 user_group_data = {
200 user_group_data = {
201 'name': _user_group.users_group_name,
201 'name': _user_group.users_group_name,
202 'permission': _user_group.permission,
202 'permission': _user_group.permission,
203 'origin': get_origin(_user_group),
203 'origin': get_origin(_user_group),
204 'type': "user_group",
204 'type': "user_group",
205 }
205 }
206 permissions.append(user_group_data)
206 permissions.append(user_group_data)
207
207
208 following_users = [
208 following_users = [
209 user.user.get_api_data(include_secrets=include_secrets)
209 user.user.get_api_data(include_secrets=include_secrets)
210 for user in repo.followers]
210 for user in repo.followers]
211
211
212 if not cache:
212 if not cache:
213 repo.update_commit_cache()
213 repo.update_commit_cache()
214 data = repo.get_api_data(include_secrets=include_secrets)
214 data = repo.get_api_data(include_secrets=include_secrets)
215 data['members'] = permissions # TODO: this should be deprecated soon
215 data['members'] = permissions # TODO: this should be deprecated soon
216 data['permissions'] = permissions
216 data['permissions'] = permissions
217 data['followers'] = following_users
217 data['followers'] = following_users
218 return data
218 return data
219
219
220
220
221 @jsonrpc_method()
221 @jsonrpc_method()
222 def get_repos(request, apiuser):
222 def get_repos(request, apiuser):
223 """
223 """
224 Lists all existing repositories.
224 Lists all existing repositories.
225
225
226 This command can only be run using an |authtoken| with admin rights,
226 This command can only be run using an |authtoken| with admin rights,
227 or users with at least read rights to |repos|.
227 or users with at least read rights to |repos|.
228
228
229 :param apiuser: This is filled automatically from the |authtoken|.
229 :param apiuser: This is filled automatically from the |authtoken|.
230 :type apiuser: AuthUser
230 :type apiuser: AuthUser
231
231
232 Example output:
232 Example output:
233
233
234 .. code-block:: bash
234 .. code-block:: bash
235
235
236 id : <id_given_in_input>
236 id : <id_given_in_input>
237 result: [
237 result: [
238 {
238 {
239 "repo_id" : "<repo_id>",
239 "repo_id" : "<repo_id>",
240 "repo_name" : "<reponame>"
240 "repo_name" : "<reponame>"
241 "repo_type" : "<repo_type>",
241 "repo_type" : "<repo_type>",
242 "clone_uri" : "<clone_uri>",
242 "clone_uri" : "<clone_uri>",
243 "private": : "<bool>",
243 "private": : "<bool>",
244 "created_on" : "<datetimecreated>",
244 "created_on" : "<datetimecreated>",
245 "description" : "<description>",
245 "description" : "<description>",
246 "landing_rev": "<landing_rev>",
246 "landing_rev": "<landing_rev>",
247 "owner": "<repo_owner>",
247 "owner": "<repo_owner>",
248 "fork_of": "<name_of_fork_parent>",
248 "fork_of": "<name_of_fork_parent>",
249 "enable_downloads": "<bool>",
249 "enable_downloads": "<bool>",
250 "enable_locking": "<bool>",
250 "enable_locking": "<bool>",
251 "enable_statistics": "<bool>",
251 "enable_statistics": "<bool>",
252 },
252 },
253 ...
253 ...
254 ]
254 ]
255 error: null
255 error: null
256 """
256 """
257
257
258 include_secrets = has_superadmin_permission(apiuser)
258 include_secrets = has_superadmin_permission(apiuser)
259 _perms = ('repository.read', 'repository.write', 'repository.admin',)
259 _perms = ('repository.read', 'repository.write', 'repository.admin',)
260 extras = {'user': apiuser}
260 extras = {'user': apiuser}
261
261
262 repo_list = RepoList(
262 repo_list = RepoList(
263 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
263 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
264 return [repo.get_api_data(include_secrets=include_secrets)
264 return [repo.get_api_data(include_secrets=include_secrets)
265 for repo in repo_list]
265 for repo in repo_list]
266
266
267
267
268 @jsonrpc_method()
268 @jsonrpc_method()
269 def get_repo_changeset(request, apiuser, repoid, revision,
269 def get_repo_changeset(request, apiuser, repoid, revision,
270 details=Optional('basic')):
270 details=Optional('basic')):
271 """
271 """
272 Returns information about a changeset.
272 Returns information about a changeset.
273
273
274 Additionally parameters define the amount of details returned by
274 Additionally parameters define the amount of details returned by
275 this function.
275 this function.
276
276
277 This command can only be run using an |authtoken| with admin rights,
277 This command can only be run using an |authtoken| with admin rights,
278 or users with at least read rights to the |repo|.
278 or users with at least read rights to the |repo|.
279
279
280 :param apiuser: This is filled automatically from the |authtoken|.
280 :param apiuser: This is filled automatically from the |authtoken|.
281 :type apiuser: AuthUser
281 :type apiuser: AuthUser
282 :param repoid: The repository name or repository id
282 :param repoid: The repository name or repository id
283 :type repoid: str or int
283 :type repoid: str or int
284 :param revision: revision for which listing should be done
284 :param revision: revision for which listing should be done
285 :type revision: str
285 :type revision: str
286 :param details: details can be 'basic|extended|full' full gives diff
286 :param details: details can be 'basic|extended|full' full gives diff
287 info details like the diff itself, and number of changed files etc.
287 info details like the diff itself, and number of changed files etc.
288 :type details: Optional(str)
288 :type details: Optional(str)
289
289
290 """
290 """
291 repo = get_repo_or_error(repoid)
291 repo = get_repo_or_error(repoid)
292 if not has_superadmin_permission(apiuser):
292 if not has_superadmin_permission(apiuser):
293 _perms = (
293 _perms = (
294 'repository.admin', 'repository.write', 'repository.read',)
294 'repository.admin', 'repository.write', 'repository.read',)
295 has_repo_permissions(apiuser, repoid, repo, _perms)
295 has_repo_permissions(apiuser, repoid, repo, _perms)
296
296
297 changes_details = Optional.extract(details)
297 changes_details = Optional.extract(details)
298 _changes_details_types = ['basic', 'extended', 'full']
298 _changes_details_types = ['basic', 'extended', 'full']
299 if changes_details not in _changes_details_types:
299 if changes_details not in _changes_details_types:
300 raise JSONRPCError(
300 raise JSONRPCError(
301 'ret_type must be one of %s' % (
301 'ret_type must be one of %s' % (
302 ','.join(_changes_details_types)))
302 ','.join(_changes_details_types)))
303
303
304 pre_load = ['author', 'branch', 'date', 'message', 'parents',
304 pre_load = ['author', 'branch', 'date', 'message', 'parents',
305 'status', '_commit', '_file_paths']
305 'status', '_commit', '_file_paths']
306
306
307 try:
307 try:
308 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
308 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
309 except TypeError as e:
309 except TypeError as e:
310 raise JSONRPCError(e.message)
310 raise JSONRPCError(e.message)
311 _cs_json = cs.__json__()
311 _cs_json = cs.__json__()
312 _cs_json['diff'] = build_commit_data(cs, changes_details)
312 _cs_json['diff'] = build_commit_data(cs, changes_details)
313 if changes_details == 'full':
313 if changes_details == 'full':
314 _cs_json['refs'] = {
314 _cs_json['refs'] = {
315 'branches': [cs.branch],
315 'branches': [cs.branch],
316 'bookmarks': getattr(cs, 'bookmarks', []),
316 'bookmarks': getattr(cs, 'bookmarks', []),
317 'tags': cs.tags
317 'tags': cs.tags
318 }
318 }
319 return _cs_json
319 return _cs_json
320
320
321
321
322 @jsonrpc_method()
322 @jsonrpc_method()
323 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
323 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
324 details=Optional('basic')):
324 details=Optional('basic')):
325 """
325 """
326 Returns a set of commits limited by the number starting
326 Returns a set of commits limited by the number starting
327 from the `start_rev` option.
327 from the `start_rev` option.
328
328
329 Additional parameters define the amount of details returned by this
329 Additional parameters define the amount of details returned by this
330 function.
330 function.
331
331
332 This command can only be run using an |authtoken| with admin rights,
332 This command can only be run using an |authtoken| with admin rights,
333 or users with at least read rights to |repos|.
333 or users with at least read rights to |repos|.
334
334
335 :param apiuser: This is filled automatically from the |authtoken|.
335 :param apiuser: This is filled automatically from the |authtoken|.
336 :type apiuser: AuthUser
336 :type apiuser: AuthUser
337 :param repoid: The repository name or repository ID.
337 :param repoid: The repository name or repository ID.
338 :type repoid: str or int
338 :type repoid: str or int
339 :param start_rev: The starting revision from where to get changesets.
339 :param start_rev: The starting revision from where to get changesets.
340 :type start_rev: str
340 :type start_rev: str
341 :param limit: Limit the number of commits to this amount
341 :param limit: Limit the number of commits to this amount
342 :type limit: str or int
342 :type limit: str or int
343 :param details: Set the level of detail returned. Valid option are:
343 :param details: Set the level of detail returned. Valid option are:
344 ``basic``, ``extended`` and ``full``.
344 ``basic``, ``extended`` and ``full``.
345 :type details: Optional(str)
345 :type details: Optional(str)
346
346
347 .. note::
347 .. note::
348
348
349 Setting the parameter `details` to the value ``full`` is extensive
349 Setting the parameter `details` to the value ``full`` is extensive
350 and returns details like the diff itself, and the number
350 and returns details like the diff itself, and the number
351 of changed files.
351 of changed files.
352
352
353 """
353 """
354 repo = get_repo_or_error(repoid)
354 repo = get_repo_or_error(repoid)
355 if not has_superadmin_permission(apiuser):
355 if not has_superadmin_permission(apiuser):
356 _perms = (
356 _perms = (
357 'repository.admin', 'repository.write', 'repository.read',)
357 'repository.admin', 'repository.write', 'repository.read',)
358 has_repo_permissions(apiuser, repoid, repo, _perms)
358 has_repo_permissions(apiuser, repoid, repo, _perms)
359
359
360 changes_details = Optional.extract(details)
360 changes_details = Optional.extract(details)
361 _changes_details_types = ['basic', 'extended', 'full']
361 _changes_details_types = ['basic', 'extended', 'full']
362 if changes_details not in _changes_details_types:
362 if changes_details not in _changes_details_types:
363 raise JSONRPCError(
363 raise JSONRPCError(
364 'ret_type must be one of %s' % (
364 'ret_type must be one of %s' % (
365 ','.join(_changes_details_types)))
365 ','.join(_changes_details_types)))
366
366
367 limit = int(limit)
367 limit = int(limit)
368 pre_load = ['author', 'branch', 'date', 'message', 'parents',
368 pre_load = ['author', 'branch', 'date', 'message', 'parents',
369 'status', '_commit', '_file_paths']
369 'status', '_commit', '_file_paths']
370
370
371 vcs_repo = repo.scm_instance()
371 vcs_repo = repo.scm_instance()
372 # SVN needs a special case to distinguish its index and commit id
372 # SVN needs a special case to distinguish its index and commit id
373 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
373 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
374 start_rev = vcs_repo.commit_ids[0]
374 start_rev = vcs_repo.commit_ids[0]
375
375
376 try:
376 try:
377 commits = vcs_repo.get_commits(
377 commits = vcs_repo.get_commits(
378 start_id=start_rev, pre_load=pre_load)
378 start_id=start_rev, pre_load=pre_load)
379 except TypeError as e:
379 except TypeError as e:
380 raise JSONRPCError(e.message)
380 raise JSONRPCError(e.message)
381 except Exception:
381 except Exception:
382 log.exception('Fetching of commits failed')
382 log.exception('Fetching of commits failed')
383 raise JSONRPCError('Error occurred during commit fetching')
383 raise JSONRPCError('Error occurred during commit fetching')
384
384
385 ret = []
385 ret = []
386 for cnt, commit in enumerate(commits):
386 for cnt, commit in enumerate(commits):
387 if cnt >= limit != -1:
387 if cnt >= limit != -1:
388 break
388 break
389 _cs_json = commit.__json__()
389 _cs_json = commit.__json__()
390 _cs_json['diff'] = build_commit_data(commit, changes_details)
390 _cs_json['diff'] = build_commit_data(commit, changes_details)
391 if changes_details == 'full':
391 if changes_details == 'full':
392 _cs_json['refs'] = {
392 _cs_json['refs'] = {
393 'branches': [commit.branch],
393 'branches': [commit.branch],
394 'bookmarks': getattr(commit, 'bookmarks', []),
394 'bookmarks': getattr(commit, 'bookmarks', []),
395 'tags': commit.tags
395 'tags': commit.tags
396 }
396 }
397 ret.append(_cs_json)
397 ret.append(_cs_json)
398 return ret
398 return ret
399
399
400
400
401 @jsonrpc_method()
401 @jsonrpc_method()
402 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
402 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
403 ret_type=Optional('all'), details=Optional('basic'),
403 ret_type=Optional('all'), details=Optional('basic'),
404 max_file_bytes=Optional(None)):
404 max_file_bytes=Optional(None)):
405 """
405 """
406 Returns a list of nodes and children in a flat list for a given
406 Returns a list of nodes and children in a flat list for a given
407 path at given revision.
407 path at given revision.
408
408
409 It's possible to specify ret_type to show only `files` or `dirs`.
409 It's possible to specify ret_type to show only `files` or `dirs`.
410
410
411 This command can only be run using an |authtoken| with admin rights,
411 This command can only be run using an |authtoken| with admin rights,
412 or users with at least read rights to |repos|.
412 or users with at least read rights to |repos|.
413
413
414 :param apiuser: This is filled automatically from the |authtoken|.
414 :param apiuser: This is filled automatically from the |authtoken|.
415 :type apiuser: AuthUser
415 :type apiuser: AuthUser
416 :param repoid: The repository name or repository ID.
416 :param repoid: The repository name or repository ID.
417 :type repoid: str or int
417 :type repoid: str or int
418 :param revision: The revision for which listing should be done.
418 :param revision: The revision for which listing should be done.
419 :type revision: str
419 :type revision: str
420 :param root_path: The path from which to start displaying.
420 :param root_path: The path from which to start displaying.
421 :type root_path: str
421 :type root_path: str
422 :param ret_type: Set the return type. Valid options are
422 :param ret_type: Set the return type. Valid options are
423 ``all`` (default), ``files`` and ``dirs``.
423 ``all`` (default), ``files`` and ``dirs``.
424 :type ret_type: Optional(str)
424 :type ret_type: Optional(str)
425 :param details: Returns extended information about nodes, such as
425 :param details: Returns extended information about nodes, such as
426 md5, binary, and or content. The valid options are ``basic`` and
426 md5, binary, and or content. The valid options are ``basic`` and
427 ``full``.
427 ``full``.
428 :type details: Optional(str)
428 :type details: Optional(str)
429 :param max_file_bytes: Only return file content under this file size bytes
429 :param max_file_bytes: Only return file content under this file size bytes
430 :type details: Optional(int)
430 :type details: Optional(int)
431
431
432 Example output:
432 Example output:
433
433
434 .. code-block:: bash
434 .. code-block:: bash
435
435
436 id : <id_given_in_input>
436 id : <id_given_in_input>
437 result: [
437 result: [
438 {
438 {
439 "name" : "<name>"
439 "name" : "<name>"
440 "type" : "<type>",
440 "type" : "<type>",
441 "binary": "<true|false>" (only in extended mode)
441 "binary": "<true|false>" (only in extended mode)
442 "md5" : "<md5 of file content>" (only in extended mode)
442 "md5" : "<md5 of file content>" (only in extended mode)
443 },
443 },
444 ...
444 ...
445 ]
445 ]
446 error: null
446 error: null
447 """
447 """
448
448
449 repo = get_repo_or_error(repoid)
449 repo = get_repo_or_error(repoid)
450 if not has_superadmin_permission(apiuser):
450 if not has_superadmin_permission(apiuser):
451 _perms = (
451 _perms = (
452 'repository.admin', 'repository.write', 'repository.read',)
452 'repository.admin', 'repository.write', 'repository.read',)
453 has_repo_permissions(apiuser, repoid, repo, _perms)
453 has_repo_permissions(apiuser, repoid, repo, _perms)
454
454
455 ret_type = Optional.extract(ret_type)
455 ret_type = Optional.extract(ret_type)
456 details = Optional.extract(details)
456 details = Optional.extract(details)
457 _extended_types = ['basic', 'full']
457 _extended_types = ['basic', 'full']
458 if details not in _extended_types:
458 if details not in _extended_types:
459 raise JSONRPCError(
459 raise JSONRPCError(
460 'ret_type must be one of %s' % (','.join(_extended_types)))
460 'ret_type must be one of %s' % (','.join(_extended_types)))
461 extended_info = False
461 extended_info = False
462 content = False
462 content = False
463 if details == 'basic':
463 if details == 'basic':
464 extended_info = True
464 extended_info = True
465
465
466 if details == 'full':
466 if details == 'full':
467 extended_info = content = True
467 extended_info = content = True
468
468
469 _map = {}
469 _map = {}
470 try:
470 try:
471 # check if repo is not empty by any chance, skip quicker if it is.
471 # check if repo is not empty by any chance, skip quicker if it is.
472 _scm = repo.scm_instance()
472 _scm = repo.scm_instance()
473 if _scm.is_empty():
473 if _scm.is_empty():
474 return []
474 return []
475
475
476 _d, _f = ScmModel().get_nodes(
476 _d, _f = ScmModel().get_nodes(
477 repo, revision, root_path, flat=False,
477 repo, revision, root_path, flat=False,
478 extended_info=extended_info, content=content,
478 extended_info=extended_info, content=content,
479 max_file_bytes=max_file_bytes)
479 max_file_bytes=max_file_bytes)
480 _map = {
480 _map = {
481 'all': _d + _f,
481 'all': _d + _f,
482 'files': _f,
482 'files': _f,
483 'dirs': _d,
483 'dirs': _d,
484 }
484 }
485 return _map[ret_type]
485 return _map[ret_type]
486 except KeyError:
486 except KeyError:
487 raise JSONRPCError(
487 raise JSONRPCError(
488 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
488 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
489 except Exception:
489 except Exception:
490 log.exception("Exception occurred while trying to get repo nodes")
490 log.exception("Exception occurred while trying to get repo nodes")
491 raise JSONRPCError(
491 raise JSONRPCError(
492 'failed to get repo: `%s` nodes' % repo.repo_name
492 'failed to get repo: `%s` nodes' % repo.repo_name
493 )
493 )
494
494
495
495
496 @jsonrpc_method()
496 @jsonrpc_method()
497 def get_repo_refs(request, apiuser, repoid):
497 def get_repo_refs(request, apiuser, repoid):
498 """
498 """
499 Returns a dictionary of current references. It returns
499 Returns a dictionary of current references. It returns
500 bookmarks, branches, closed_branches, and tags for given repository
500 bookmarks, branches, closed_branches, and tags for given repository
501
501
502 It's possible to specify ret_type to show only `files` or `dirs`.
502 It's possible to specify ret_type to show only `files` or `dirs`.
503
503
504 This command can only be run using an |authtoken| with admin rights,
504 This command can only be run using an |authtoken| with admin rights,
505 or users with at least read rights to |repos|.
505 or users with at least read rights to |repos|.
506
506
507 :param apiuser: This is filled automatically from the |authtoken|.
507 :param apiuser: This is filled automatically from the |authtoken|.
508 :type apiuser: AuthUser
508 :type apiuser: AuthUser
509 :param repoid: The repository name or repository ID.
509 :param repoid: The repository name or repository ID.
510 :type repoid: str or int
510 :type repoid: str or int
511
511
512 Example output:
512 Example output:
513
513
514 .. code-block:: bash
514 .. code-block:: bash
515
515
516 id : <id_given_in_input>
516 id : <id_given_in_input>
517 result: [
517 result: [
518 TODO...
518 TODO...
519 ]
519 ]
520 error: null
520 error: null
521 """
521 """
522
522
523 repo = get_repo_or_error(repoid)
523 repo = get_repo_or_error(repoid)
524 if not has_superadmin_permission(apiuser):
524 if not has_superadmin_permission(apiuser):
525 _perms = ('repository.admin', 'repository.write', 'repository.read',)
525 _perms = ('repository.admin', 'repository.write', 'repository.read',)
526 has_repo_permissions(apiuser, repoid, repo, _perms)
526 has_repo_permissions(apiuser, repoid, repo, _perms)
527
527
528 try:
528 try:
529 # check if repo is not empty by any chance, skip quicker if it is.
529 # check if repo is not empty by any chance, skip quicker if it is.
530 vcs_instance = repo.scm_instance()
530 vcs_instance = repo.scm_instance()
531 refs = vcs_instance.refs()
531 refs = vcs_instance.refs()
532 return refs
532 return refs
533 except Exception:
533 except Exception:
534 log.exception("Exception occurred while trying to get repo refs")
534 log.exception("Exception occurred while trying to get repo refs")
535 raise JSONRPCError(
535 raise JSONRPCError(
536 'failed to get repo: `%s` references' % repo.repo_name
536 'failed to get repo: `%s` references' % repo.repo_name
537 )
537 )
538
538
539
539
540 @jsonrpc_method()
540 @jsonrpc_method()
541 def create_repo(request, apiuser, repo_name, repo_type,
541 def create_repo(request, apiuser, repo_name, repo_type,
542 owner=Optional(OAttr('apiuser')), description=Optional(''),
542 owner=Optional(OAttr('apiuser')), description=Optional(''),
543 private=Optional(False), clone_uri=Optional(None),
543 private=Optional(False), clone_uri=Optional(None),
544 landing_rev=Optional('rev:tip'),
544 landing_rev=Optional('rev:tip'),
545 enable_statistics=Optional(False),
545 enable_statistics=Optional(False),
546 enable_locking=Optional(False),
546 enable_locking=Optional(False),
547 enable_downloads=Optional(False),
547 enable_downloads=Optional(False),
548 copy_permissions=Optional(False)):
548 copy_permissions=Optional(False)):
549 """
549 """
550 Creates a repository.
550 Creates a repository.
551
551
552 * If the repository name contains "/", all the required repository
552 * If the repository name contains "/", all the required repository
553 groups will be created.
553 groups will be created.
554
554
555 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
555 For example "foo/bar/baz" will create |repo| groups "foo" and "bar"
556 (with "foo" as parent). It will also create the "baz" repository
556 (with "foo" as parent). It will also create the "baz" repository
557 with "bar" as |repo| group.
557 with "bar" as |repo| group.
558
558
559 This command can only be run using an |authtoken| with at least
559 This command can only be run using an |authtoken| with at least
560 write permissions to the |repo|.
560 write permissions to the |repo|.
561
561
562 :param apiuser: This is filled automatically from the |authtoken|.
562 :param apiuser: This is filled automatically from the |authtoken|.
563 :type apiuser: AuthUser
563 :type apiuser: AuthUser
564 :param repo_name: Set the repository name.
564 :param repo_name: Set the repository name.
565 :type repo_name: str
565 :type repo_name: str
566 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
566 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
567 :type repo_type: str
567 :type repo_type: str
568 :param owner: user_id or username
568 :param owner: user_id or username
569 :type owner: Optional(str)
569 :type owner: Optional(str)
570 :param description: Set the repository description.
570 :param description: Set the repository description.
571 :type description: Optional(str)
571 :type description: Optional(str)
572 :param private:
572 :param private:
573 :type private: bool
573 :type private: bool
574 :param clone_uri:
574 :param clone_uri:
575 :type clone_uri: str
575 :type clone_uri: str
576 :param landing_rev: <rev_type>:<rev>
576 :param landing_rev: <rev_type>:<rev>
577 :type landing_rev: str
577 :type landing_rev: str
578 :param enable_locking:
578 :param enable_locking:
579 :type enable_locking: bool
579 :type enable_locking: bool
580 :param enable_downloads:
580 :param enable_downloads:
581 :type enable_downloads: bool
581 :type enable_downloads: bool
582 :param enable_statistics:
582 :param enable_statistics:
583 :type enable_statistics: bool
583 :type enable_statistics: bool
584 :param copy_permissions: Copy permission from group in which the
584 :param copy_permissions: Copy permission from group in which the
585 repository is being created.
585 repository is being created.
586 :type copy_permissions: bool
586 :type copy_permissions: bool
587
587
588
588
589 Example output:
589 Example output:
590
590
591 .. code-block:: bash
591 .. code-block:: bash
592
592
593 id : <id_given_in_input>
593 id : <id_given_in_input>
594 result: {
594 result: {
595 "msg": "Created new repository `<reponame>`",
595 "msg": "Created new repository `<reponame>`",
596 "success": true,
596 "success": true,
597 "task": "<celery task id or None if done sync>"
597 "task": "<celery task id or None if done sync>"
598 }
598 }
599 error: null
599 error: null
600
600
601
601
602 Example error output:
602 Example error output:
603
603
604 .. code-block:: bash
604 .. code-block:: bash
605
605
606 id : <id_given_in_input>
606 id : <id_given_in_input>
607 result : null
607 result : null
608 error : {
608 error : {
609 'failed to create repository `<repo_name>`
609 'failed to create repository `<repo_name>`
610 }
610 }
611
611
612 """
612 """
613 schema = repo_schema.RepoSchema()
613 schema = repo_schema.RepoSchema()
614 try:
614 try:
615 data = schema.deserialize({
615 data = schema.deserialize({
616 'repo_name': repo_name
616 'repo_name': repo_name
617 })
617 })
618 except colander.Invalid as e:
618 except colander.Invalid as e:
619 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
619 raise JSONRPCError("Validation failed: %s" % (e.asdict(),))
620 repo_name = data['repo_name']
620 repo_name = data['repo_name']
621
621
622 (repo_name_cleaned,
622 (repo_name_cleaned,
623 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
623 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
624 repo_name)
624 repo_name)
625
625
626 if not HasPermissionAnyApi(
626 if not HasPermissionAnyApi(
627 'hg.admin', 'hg.create.repository')(user=apiuser):
627 'hg.admin', 'hg.create.repository')(user=apiuser):
628 # check if we have admin permission for this repo group if given !
628 # check if we have admin permission for this repo group if given !
629
629
630 if parent_group_name:
630 if parent_group_name:
631 repogroupid = parent_group_name
631 repogroupid = parent_group_name
632 repo_group = get_repo_group_or_error(parent_group_name)
632 repo_group = get_repo_group_or_error(parent_group_name)
633
633
634 _perms = ('group.admin',)
634 _perms = ('group.admin',)
635 if not HasRepoGroupPermissionAnyApi(*_perms)(
635 if not HasRepoGroupPermissionAnyApi(*_perms)(
636 user=apiuser, group_name=repo_group.group_name):
636 user=apiuser, group_name=repo_group.group_name):
637 raise JSONRPCError(
637 raise JSONRPCError(
638 'repository group `%s` does not exist' % (
638 'repository group `%s` does not exist' % (
639 repogroupid,))
639 repogroupid,))
640 else:
640 else:
641 raise JSONRPCForbidden()
641 raise JSONRPCForbidden()
642
642
643 if not has_superadmin_permission(apiuser):
643 if not has_superadmin_permission(apiuser):
644 if not isinstance(owner, Optional):
644 if not isinstance(owner, Optional):
645 # forbid setting owner for non-admins
645 # forbid setting owner for non-admins
646 raise JSONRPCError(
646 raise JSONRPCError(
647 'Only RhodeCode admin can specify `owner` param')
647 'Only RhodeCode admin can specify `owner` param')
648
648
649 if isinstance(owner, Optional):
649 if isinstance(owner, Optional):
650 owner = apiuser.user_id
650 owner = apiuser.user_id
651
651
652 owner = get_user_or_error(owner)
652 owner = get_user_or_error(owner)
653
653
654 if RepoModel().get_by_repo_name(repo_name):
654 if RepoModel().get_by_repo_name(repo_name):
655 raise JSONRPCError("repo `%s` already exist" % repo_name)
655 raise JSONRPCError("repo `%s` already exist" % repo_name)
656
656
657 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
657 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
658 if isinstance(private, Optional):
658 if isinstance(private, Optional):
659 private = defs.get('repo_private') or Optional.extract(private)
659 private = defs.get('repo_private') or Optional.extract(private)
660 if isinstance(repo_type, Optional):
660 if isinstance(repo_type, Optional):
661 repo_type = defs.get('repo_type')
661 repo_type = defs.get('repo_type')
662 if isinstance(enable_statistics, Optional):
662 if isinstance(enable_statistics, Optional):
663 enable_statistics = defs.get('repo_enable_statistics')
663 enable_statistics = defs.get('repo_enable_statistics')
664 if isinstance(enable_locking, Optional):
664 if isinstance(enable_locking, Optional):
665 enable_locking = defs.get('repo_enable_locking')
665 enable_locking = defs.get('repo_enable_locking')
666 if isinstance(enable_downloads, Optional):
666 if isinstance(enable_downloads, Optional):
667 enable_downloads = defs.get('repo_enable_downloads')
667 enable_downloads = defs.get('repo_enable_downloads')
668
668
669 clone_uri = Optional.extract(clone_uri)
669 clone_uri = Optional.extract(clone_uri)
670 description = Optional.extract(description)
670 description = Optional.extract(description)
671 landing_rev = Optional.extract(landing_rev)
671 landing_rev = Optional.extract(landing_rev)
672 copy_permissions = Optional.extract(copy_permissions)
672 copy_permissions = Optional.extract(copy_permissions)
673
673
674 try:
674 try:
675 # create structure of groups and return the last group
675 # create structure of groups and return the last group
676 repo_group = map_groups(repo_name)
676 repo_group = map_groups(repo_name)
677 data = {
677 data = {
678 'repo_name': repo_name_cleaned,
678 'repo_name': repo_name_cleaned,
679 'repo_name_full': repo_name,
679 'repo_name_full': repo_name,
680 'repo_type': repo_type,
680 'repo_type': repo_type,
681 'repo_description': description,
681 'repo_description': description,
682 'owner': owner,
682 'owner': owner,
683 'repo_private': private,
683 'repo_private': private,
684 'clone_uri': clone_uri,
684 'clone_uri': clone_uri,
685 'repo_group': repo_group.group_id if repo_group else None,
685 'repo_group': repo_group.group_id if repo_group else None,
686 'repo_landing_rev': landing_rev,
686 'repo_landing_rev': landing_rev,
687 'enable_statistics': enable_statistics,
687 'enable_statistics': enable_statistics,
688 'enable_locking': enable_locking,
688 'enable_locking': enable_locking,
689 'enable_downloads': enable_downloads,
689 'enable_downloads': enable_downloads,
690 'repo_copy_permissions': copy_permissions,
690 'repo_copy_permissions': copy_permissions,
691 }
691 }
692
692
693 if repo_type not in BACKENDS.keys():
693 if repo_type not in BACKENDS.keys():
694 raise Exception("Invalid backend type %s" % repo_type)
694 raise Exception("Invalid backend type %s" % repo_type)
695 task = RepoModel().create(form_data=data, cur_user=owner)
695 task = RepoModel().create(form_data=data, cur_user=owner)
696 from celery.result import BaseAsyncResult
696 from celery.result import BaseAsyncResult
697 task_id = None
697 task_id = None
698 if isinstance(task, BaseAsyncResult):
698 if isinstance(task, BaseAsyncResult):
699 task_id = task.task_id
699 task_id = task.task_id
700 # no commit, it's done in RepoModel, or async via celery
700 # no commit, it's done in RepoModel, or async via celery
701 return {
701 return {
702 'msg': "Created new repository `%s`" % (repo_name,),
702 'msg': "Created new repository `%s`" % (repo_name,),
703 'success': True, # cannot return the repo data here since fork
703 'success': True, # cannot return the repo data here since fork
704 # cann be done async
704 # cann be done async
705 'task': task_id
705 'task': task_id
706 }
706 }
707 except Exception:
707 except Exception:
708 log.exception(
708 log.exception(
709 u"Exception while trying to create the repository %s",
709 u"Exception while trying to create the repository %s",
710 repo_name)
710 repo_name)
711 raise JSONRPCError(
711 raise JSONRPCError(
712 'failed to create repository `%s`' % (repo_name,))
712 'failed to create repository `%s`' % (repo_name,))
713
713
714
714
715 @jsonrpc_method()
715 @jsonrpc_method()
716 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
716 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
717 description=Optional('')):
717 description=Optional('')):
718 """
718 """
719 Adds an extra field to a repository.
719 Adds an extra field to a repository.
720
720
721 This command can only be run using an |authtoken| with at least
721 This command can only be run using an |authtoken| with at least
722 write permissions to the |repo|.
722 write permissions to the |repo|.
723
723
724 :param apiuser: This is filled automatically from the |authtoken|.
724 :param apiuser: This is filled automatically from the |authtoken|.
725 :type apiuser: AuthUser
725 :type apiuser: AuthUser
726 :param repoid: Set the repository name or repository id.
726 :param repoid: Set the repository name or repository id.
727 :type repoid: str or int
727 :type repoid: str or int
728 :param key: Create a unique field key for this repository.
728 :param key: Create a unique field key for this repository.
729 :type key: str
729 :type key: str
730 :param label:
730 :param label:
731 :type label: Optional(str)
731 :type label: Optional(str)
732 :param description:
732 :param description:
733 :type description: Optional(str)
733 :type description: Optional(str)
734 """
734 """
735 repo = get_repo_or_error(repoid)
735 repo = get_repo_or_error(repoid)
736 if not has_superadmin_permission(apiuser):
736 if not has_superadmin_permission(apiuser):
737 _perms = ('repository.admin',)
737 _perms = ('repository.admin',)
738 has_repo_permissions(apiuser, repoid, repo, _perms)
738 has_repo_permissions(apiuser, repoid, repo, _perms)
739
739
740 label = Optional.extract(label) or key
740 label = Optional.extract(label) or key
741 description = Optional.extract(description)
741 description = Optional.extract(description)
742
742
743 field = RepositoryField.get_by_key_name(key, repo)
743 field = RepositoryField.get_by_key_name(key, repo)
744 if field:
744 if field:
745 raise JSONRPCError('Field with key '
745 raise JSONRPCError('Field with key '
746 '`%s` exists for repo `%s`' % (key, repoid))
746 '`%s` exists for repo `%s`' % (key, repoid))
747
747
748 try:
748 try:
749 RepoModel().add_repo_field(repo, key, field_label=label,
749 RepoModel().add_repo_field(repo, key, field_label=label,
750 field_desc=description)
750 field_desc=description)
751 Session().commit()
751 Session().commit()
752 return {
752 return {
753 'msg': "Added new repository field `%s`" % (key,),
753 'msg': "Added new repository field `%s`" % (key,),
754 'success': True,
754 'success': True,
755 }
755 }
756 except Exception:
756 except Exception:
757 log.exception("Exception occurred while trying to add field to repo")
757 log.exception("Exception occurred while trying to add field to repo")
758 raise JSONRPCError(
758 raise JSONRPCError(
759 'failed to create new field for repository `%s`' % (repoid,))
759 'failed to create new field for repository `%s`' % (repoid,))
760
760
761
761
762 @jsonrpc_method()
762 @jsonrpc_method()
763 def remove_field_from_repo(request, apiuser, repoid, key):
763 def remove_field_from_repo(request, apiuser, repoid, key):
764 """
764 """
765 Removes an extra field from a repository.
765 Removes an extra field from a repository.
766
766
767 This command can only be run using an |authtoken| with at least
767 This command can only be run using an |authtoken| with at least
768 write permissions to the |repo|.
768 write permissions to the |repo|.
769
769
770 :param apiuser: This is filled automatically from the |authtoken|.
770 :param apiuser: This is filled automatically from the |authtoken|.
771 :type apiuser: AuthUser
771 :type apiuser: AuthUser
772 :param repoid: Set the repository name or repository ID.
772 :param repoid: Set the repository name or repository ID.
773 :type repoid: str or int
773 :type repoid: str or int
774 :param key: Set the unique field key for this repository.
774 :param key: Set the unique field key for this repository.
775 :type key: str
775 :type key: str
776 """
776 """
777
777
778 repo = get_repo_or_error(repoid)
778 repo = get_repo_or_error(repoid)
779 if not has_superadmin_permission(apiuser):
779 if not has_superadmin_permission(apiuser):
780 _perms = ('repository.admin',)
780 _perms = ('repository.admin',)
781 has_repo_permissions(apiuser, repoid, repo, _perms)
781 has_repo_permissions(apiuser, repoid, repo, _perms)
782
782
783 field = RepositoryField.get_by_key_name(key, repo)
783 field = RepositoryField.get_by_key_name(key, repo)
784 if not field:
784 if not field:
785 raise JSONRPCError('Field with key `%s` does not '
785 raise JSONRPCError('Field with key `%s` does not '
786 'exists for repo `%s`' % (key, repoid))
786 'exists for repo `%s`' % (key, repoid))
787
787
788 try:
788 try:
789 RepoModel().delete_repo_field(repo, field_key=key)
789 RepoModel().delete_repo_field(repo, field_key=key)
790 Session().commit()
790 Session().commit()
791 return {
791 return {
792 'msg': "Deleted repository field `%s`" % (key,),
792 'msg': "Deleted repository field `%s`" % (key,),
793 'success': True,
793 'success': True,
794 }
794 }
795 except Exception:
795 except Exception:
796 log.exception(
796 log.exception(
797 "Exception occurred while trying to delete field from repo")
797 "Exception occurred while trying to delete field from repo")
798 raise JSONRPCError(
798 raise JSONRPCError(
799 'failed to delete field for repository `%s`' % (repoid,))
799 'failed to delete field for repository `%s`' % (repoid,))
800
800
801
801
802 @jsonrpc_method()
802 @jsonrpc_method()
803 def update_repo(request, apiuser, repoid, name=Optional(None),
803 def update_repo(request, apiuser, repoid, name=Optional(None),
804 owner=Optional(OAttr('apiuser')),
804 owner=Optional(OAttr('apiuser')),
805 group=Optional(None),
805 group=Optional(None),
806 fork_of=Optional(None),
806 fork_of=Optional(None),
807 description=Optional(''), private=Optional(False),
807 description=Optional(''), private=Optional(False),
808 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
808 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
809 enable_statistics=Optional(False),
809 enable_statistics=Optional(False),
810 enable_locking=Optional(False),
810 enable_locking=Optional(False),
811 enable_downloads=Optional(False),
811 enable_downloads=Optional(False),
812 fields=Optional('')):
812 fields=Optional('')):
813 """
813 """
814 Updates a repository with the given information.
814 Updates a repository with the given information.
815
815
816 This command can only be run using an |authtoken| with at least
816 This command can only be run using an |authtoken| with at least
817 write permissions to the |repo|.
817 write permissions to the |repo|.
818
818
819 :param apiuser: This is filled automatically from the |authtoken|.
819 :param apiuser: This is filled automatically from the |authtoken|.
820 :type apiuser: AuthUser
820 :type apiuser: AuthUser
821 :param repoid: repository name or repository ID.
821 :param repoid: repository name or repository ID.
822 :type repoid: str or int
822 :type repoid: str or int
823 :param name: Update the |repo| name.
823 :param name: Update the |repo| name.
824 :type name: str
824 :type name: str
825 :param owner: Set the |repo| owner.
825 :param owner: Set the |repo| owner.
826 :type owner: str
826 :type owner: str
827 :param group: Set the |repo| group the |repo| belongs to.
827 :param group: Set the |repo| group the |repo| belongs to.
828 :type group: str
828 :type group: str
829 :param fork_of: Set the master |repo| name.
829 :param fork_of: Set the master |repo| name.
830 :type fork_of: str
830 :type fork_of: str
831 :param description: Update the |repo| description.
831 :param description: Update the |repo| description.
832 :type description: str
832 :type description: str
833 :param private: Set the |repo| as private. (True | False)
833 :param private: Set the |repo| as private. (True | False)
834 :type private: bool
834 :type private: bool
835 :param clone_uri: Update the |repo| clone URI.
835 :param clone_uri: Update the |repo| clone URI.
836 :type clone_uri: str
836 :type clone_uri: str
837 :param landing_rev: Set the |repo| landing revision. Default is
837 :param landing_rev: Set the |repo| landing revision. Default is
838 ``tip``.
838 ``tip``.
839 :type landing_rev: str
839 :type landing_rev: str
840 :param enable_statistics: Enable statistics on the |repo|,
840 :param enable_statistics: Enable statistics on the |repo|,
841 (True | False).
841 (True | False).
842 :type enable_statistics: bool
842 :type enable_statistics: bool
843 :param enable_locking: Enable |repo| locking.
843 :param enable_locking: Enable |repo| locking.
844 :type enable_locking: bool
844 :type enable_locking: bool
845 :param enable_downloads: Enable downloads from the |repo|,
845 :param enable_downloads: Enable downloads from the |repo|,
846 (True | False).
846 (True | False).
847 :type enable_downloads: bool
847 :type enable_downloads: bool
848 :param fields: Add extra fields to the |repo|. Use the following
848 :param fields: Add extra fields to the |repo|. Use the following
849 example format: ``field_key=field_val,field_key2=fieldval2``.
849 example format: ``field_key=field_val,field_key2=fieldval2``.
850 Escape ', ' with \,
850 Escape ', ' with \,
851 :type fields: str
851 :type fields: str
852 """
852 """
853 repo = get_repo_or_error(repoid)
853 repo = get_repo_or_error(repoid)
854 include_secrets = False
854 include_secrets = False
855 if has_superadmin_permission(apiuser):
855 if has_superadmin_permission(apiuser):
856 include_secrets = True
856 include_secrets = True
857 else:
857 else:
858 _perms = ('repository.admin',)
858 _perms = ('repository.admin',)
859 has_repo_permissions(apiuser, repoid, repo, _perms)
859 has_repo_permissions(apiuser, repoid, repo, _perms)
860
860
861 updates = {
861 updates = {
862 # update function requires this.
862 # update function requires this.
863 'repo_name': repo.just_name
863 'repo_name': repo.just_name
864 }
864 }
865 repo_group = group
865 repo_group = group
866 if not isinstance(repo_group, Optional):
866 if not isinstance(repo_group, Optional):
867 repo_group = get_repo_group_or_error(repo_group)
867 repo_group = get_repo_group_or_error(repo_group)
868 repo_group = repo_group.group_id
868 repo_group = repo_group.group_id
869
869
870 repo_fork_of = fork_of
870 repo_fork_of = fork_of
871 if not isinstance(repo_fork_of, Optional):
871 if not isinstance(repo_fork_of, Optional):
872 repo_fork_of = get_repo_or_error(repo_fork_of)
872 repo_fork_of = get_repo_or_error(repo_fork_of)
873 repo_fork_of = repo_fork_of.repo_id
873 repo_fork_of = repo_fork_of.repo_id
874
874
875 try:
875 try:
876 store_update(updates, name, 'repo_name')
876 store_update(updates, name, 'repo_name')
877 store_update(updates, repo_group, 'repo_group')
877 store_update(updates, repo_group, 'repo_group')
878 store_update(updates, repo_fork_of, 'fork_id')
878 store_update(updates, repo_fork_of, 'fork_id')
879 store_update(updates, owner, 'user')
879 store_update(updates, owner, 'user')
880 store_update(updates, description, 'repo_description')
880 store_update(updates, description, 'repo_description')
881 store_update(updates, private, 'repo_private')
881 store_update(updates, private, 'repo_private')
882 store_update(updates, clone_uri, 'clone_uri')
882 store_update(updates, clone_uri, 'clone_uri')
883 store_update(updates, landing_rev, 'repo_landing_rev')
883 store_update(updates, landing_rev, 'repo_landing_rev')
884 store_update(updates, enable_statistics, 'repo_enable_statistics')
884 store_update(updates, enable_statistics, 'repo_enable_statistics')
885 store_update(updates, enable_locking, 'repo_enable_locking')
885 store_update(updates, enable_locking, 'repo_enable_locking')
886 store_update(updates, enable_downloads, 'repo_enable_downloads')
886 store_update(updates, enable_downloads, 'repo_enable_downloads')
887
887
888 # extra fields
888 # extra fields
889 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
889 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
890 if fields:
890 if fields:
891 updates.update(fields)
891 updates.update(fields)
892
892
893 RepoModel().update(repo, **updates)
893 RepoModel().update(repo, **updates)
894 Session().commit()
894 Session().commit()
895 return {
895 return {
896 'msg': 'updated repo ID:%s %s' % (
896 'msg': 'updated repo ID:%s %s' % (
897 repo.repo_id, repo.repo_name),
897 repo.repo_id, repo.repo_name),
898 'repository': repo.get_api_data(
898 'repository': repo.get_api_data(
899 include_secrets=include_secrets)
899 include_secrets=include_secrets)
900 }
900 }
901 except Exception:
901 except Exception:
902 log.exception(
902 log.exception(
903 u"Exception while trying to update the repository %s",
903 u"Exception while trying to update the repository %s",
904 repoid)
904 repoid)
905 raise JSONRPCError('failed to update repo `%s`' % repoid)
905 raise JSONRPCError('failed to update repo `%s`' % repoid)
906
906
907
907
908 @jsonrpc_method()
908 @jsonrpc_method()
909 def fork_repo(request, apiuser, repoid, fork_name,
909 def fork_repo(request, apiuser, repoid, fork_name,
910 owner=Optional(OAttr('apiuser')),
910 owner=Optional(OAttr('apiuser')),
911 description=Optional(''), copy_permissions=Optional(False),
911 description=Optional(''), copy_permissions=Optional(False),
912 private=Optional(False), landing_rev=Optional('rev:tip')):
912 private=Optional(False), landing_rev=Optional('rev:tip')):
913 """
913 """
914 Creates a fork of the specified |repo|.
914 Creates a fork of the specified |repo|.
915
915
916 * If using |RCE| with Celery this will immediately return a success
916 * If using |RCE| with Celery this will immediately return a success
917 message, even though the fork will be created asynchronously.
917 message, even though the fork will be created asynchronously.
918
918
919 This command can only be run using an |authtoken| with fork
919 This command can only be run using an |authtoken| with fork
920 permissions on the |repo|.
920 permissions on the |repo|.
921
921
922 :param apiuser: This is filled automatically from the |authtoken|.
922 :param apiuser: This is filled automatically from the |authtoken|.
923 :type apiuser: AuthUser
923 :type apiuser: AuthUser
924 :param repoid: Set repository name or repository ID.
924 :param repoid: Set repository name or repository ID.
925 :type repoid: str or int
925 :type repoid: str or int
926 :param fork_name: Set the fork name.
926 :param fork_name: Set the fork name.
927 :type fork_name: str
927 :type fork_name: str
928 :param owner: Set the fork owner.
928 :param owner: Set the fork owner.
929 :type owner: str
929 :type owner: str
930 :param description: Set the fork descripton.
930 :param description: Set the fork descripton.
931 :type description: str
931 :type description: str
932 :param copy_permissions: Copy permissions from parent |repo|. The
932 :param copy_permissions: Copy permissions from parent |repo|. The
933 default is False.
933 default is False.
934 :type copy_permissions: bool
934 :type copy_permissions: bool
935 :param private: Make the fork private. The default is False.
935 :param private: Make the fork private. The default is False.
936 :type private: bool
936 :type private: bool
937 :param landing_rev: Set the landing revision. The default is tip.
937 :param landing_rev: Set the landing revision. The default is tip.
938
938
939 Example output:
939 Example output:
940
940
941 .. code-block:: bash
941 .. code-block:: bash
942
942
943 id : <id_for_response>
943 id : <id_for_response>
944 api_key : "<api_key>"
944 api_key : "<api_key>"
945 args: {
945 args: {
946 "repoid" : "<reponame or repo_id>",
946 "repoid" : "<reponame or repo_id>",
947 "fork_name": "<forkname>",
947 "fork_name": "<forkname>",
948 "owner": "<username or user_id = Optional(=apiuser)>",
948 "owner": "<username or user_id = Optional(=apiuser)>",
949 "description": "<description>",
949 "description": "<description>",
950 "copy_permissions": "<bool>",
950 "copy_permissions": "<bool>",
951 "private": "<bool>",
951 "private": "<bool>",
952 "landing_rev": "<landing_rev>"
952 "landing_rev": "<landing_rev>"
953 }
953 }
954
954
955 Example error output:
955 Example error output:
956
956
957 .. code-block:: bash
957 .. code-block:: bash
958
958
959 id : <id_given_in_input>
959 id : <id_given_in_input>
960 result: {
960 result: {
961 "msg": "Created fork of `<reponame>` as `<forkname>`",
961 "msg": "Created fork of `<reponame>` as `<forkname>`",
962 "success": true,
962 "success": true,
963 "task": "<celery task id or None if done sync>"
963 "task": "<celery task id or None if done sync>"
964 }
964 }
965 error: null
965 error: null
966
966
967 """
967 """
968 if not has_superadmin_permission(apiuser):
968 if not has_superadmin_permission(apiuser):
969 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
969 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
970 raise JSONRPCForbidden()
970 raise JSONRPCForbidden()
971
971
972 repo = get_repo_or_error(repoid)
972 repo = get_repo_or_error(repoid)
973 repo_name = repo.repo_name
973 repo_name = repo.repo_name
974
974
975 (fork_name_cleaned,
975 (fork_name_cleaned,
976 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
976 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(
977 fork_name)
977 fork_name)
978
978
979 if not has_superadmin_permission(apiuser):
979 if not has_superadmin_permission(apiuser):
980 # check if we have at least read permission for
980 # check if we have at least read permission for
981 # this repo that we fork !
981 # this repo that we fork !
982 _perms = (
982 _perms = (
983 'repository.admin', 'repository.write', 'repository.read')
983 'repository.admin', 'repository.write', 'repository.read')
984 has_repo_permissions(apiuser, repoid, repo, _perms)
984 has_repo_permissions(apiuser, repoid, repo, _perms)
985
985
986 if not isinstance(owner, Optional):
986 if not isinstance(owner, Optional):
987 # forbid setting owner for non super admins
987 # forbid setting owner for non super admins
988 raise JSONRPCError(
988 raise JSONRPCError(
989 'Only RhodeCode admin can specify `owner` param'
989 'Only RhodeCode admin can specify `owner` param'
990 )
990 )
991 # check if we have a create.repo permission if not maybe the parent
991 # check if we have a create.repo permission if not maybe the parent
992 # group permission
992 # group permission
993 if not HasPermissionAnyApi('hg.create.repository')(user=apiuser):
993 if not HasPermissionAnyApi('hg.create.repository')(user=apiuser):
994 if parent_group_name:
994 if parent_group_name:
995 repogroupid = parent_group_name
995 repogroupid = parent_group_name
996 repo_group = get_repo_group_or_error(parent_group_name)
996 repo_group = get_repo_group_or_error(parent_group_name)
997
997
998 _perms = ('group.admin',)
998 _perms = ('group.admin',)
999 if not HasRepoGroupPermissionAnyApi(*_perms)(
999 if not HasRepoGroupPermissionAnyApi(*_perms)(
1000 user=apiuser, group_name=repo_group.group_name):
1000 user=apiuser, group_name=repo_group.group_name):
1001 raise JSONRPCError(
1001 raise JSONRPCError(
1002 'repository group `%s` does not exist' % (
1002 'repository group `%s` does not exist' % (
1003 repogroupid,))
1003 repogroupid,))
1004 else:
1004 else:
1005 raise JSONRPCForbidden()
1005 raise JSONRPCForbidden()
1006
1006
1007 _repo = RepoModel().get_by_repo_name(fork_name)
1007 _repo = RepoModel().get_by_repo_name(fork_name)
1008 if _repo:
1008 if _repo:
1009 type_ = 'fork' if _repo.fork else 'repo'
1009 type_ = 'fork' if _repo.fork else 'repo'
1010 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1010 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1011
1011
1012 if isinstance(owner, Optional):
1012 if isinstance(owner, Optional):
1013 owner = apiuser.user_id
1013 owner = apiuser.user_id
1014
1014
1015 owner = get_user_or_error(owner)
1015 owner = get_user_or_error(owner)
1016
1016
1017 try:
1017 try:
1018 # create structure of groups and return the last group
1018 # create structure of groups and return the last group
1019 repo_group = map_groups(fork_name)
1019 repo_group = map_groups(fork_name)
1020 form_data = {
1020 form_data = {
1021 'repo_name': fork_name_cleaned,
1021 'repo_name': fork_name_cleaned,
1022 'repo_name_full': fork_name,
1022 'repo_name_full': fork_name,
1023 'repo_group': repo_group.group_id if repo_group else None,
1023 'repo_group': repo_group.group_id if repo_group else None,
1024 'repo_type': repo.repo_type,
1024 'repo_type': repo.repo_type,
1025 'description': Optional.extract(description),
1025 'description': Optional.extract(description),
1026 'private': Optional.extract(private),
1026 'private': Optional.extract(private),
1027 'copy_permissions': Optional.extract(copy_permissions),
1027 'copy_permissions': Optional.extract(copy_permissions),
1028 'landing_rev': Optional.extract(landing_rev),
1028 'landing_rev': Optional.extract(landing_rev),
1029 'fork_parent_id': repo.repo_id,
1029 'fork_parent_id': repo.repo_id,
1030 }
1030 }
1031
1031
1032 task = RepoModel().create_fork(form_data, cur_user=owner)
1032 task = RepoModel().create_fork(form_data, cur_user=owner)
1033 # no commit, it's done in RepoModel, or async via celery
1033 # no commit, it's done in RepoModel, or async via celery
1034 from celery.result import BaseAsyncResult
1034 from celery.result import BaseAsyncResult
1035 task_id = None
1035 task_id = None
1036 if isinstance(task, BaseAsyncResult):
1036 if isinstance(task, BaseAsyncResult):
1037 task_id = task.task_id
1037 task_id = task.task_id
1038 return {
1038 return {
1039 'msg': 'Created fork of `%s` as `%s`' % (
1039 'msg': 'Created fork of `%s` as `%s`' % (
1040 repo.repo_name, fork_name),
1040 repo.repo_name, fork_name),
1041 'success': True, # cannot return the repo data here since fork
1041 'success': True, # cannot return the repo data here since fork
1042 # can be done async
1042 # can be done async
1043 'task': task_id
1043 'task': task_id
1044 }
1044 }
1045 except Exception:
1045 except Exception:
1046 log.exception("Exception occurred while trying to fork a repo")
1046 log.exception("Exception occurred while trying to fork a repo")
1047 raise JSONRPCError(
1047 raise JSONRPCError(
1048 'failed to fork repository `%s` as `%s`' % (
1048 'failed to fork repository `%s` as `%s`' % (
1049 repo_name, fork_name))
1049 repo_name, fork_name))
1050
1050
1051
1051
1052 @jsonrpc_method()
1052 @jsonrpc_method()
1053 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1053 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1054 """
1054 """
1055 Deletes a repository.
1055 Deletes a repository.
1056
1056
1057 * When the `forks` parameter is set it's possible to detach or delete
1057 * When the `forks` parameter is set it's possible to detach or delete
1058 forks of deleted repository.
1058 forks of deleted repository.
1059
1059
1060 This command can only be run using an |authtoken| with admin
1060 This command can only be run using an |authtoken| with admin
1061 permissions on the |repo|.
1061 permissions on the |repo|.
1062
1062
1063 :param apiuser: This is filled automatically from the |authtoken|.
1063 :param apiuser: This is filled automatically from the |authtoken|.
1064 :type apiuser: AuthUser
1064 :type apiuser: AuthUser
1065 :param repoid: Set the repository name or repository ID.
1065 :param repoid: Set the repository name or repository ID.
1066 :type repoid: str or int
1066 :type repoid: str or int
1067 :param forks: Set to `detach` or `delete` forks from the |repo|.
1067 :param forks: Set to `detach` or `delete` forks from the |repo|.
1068 :type forks: Optional(str)
1068 :type forks: Optional(str)
1069
1069
1070 Example error output:
1070 Example error output:
1071
1071
1072 .. code-block:: bash
1072 .. code-block:: bash
1073
1073
1074 id : <id_given_in_input>
1074 id : <id_given_in_input>
1075 result: {
1075 result: {
1076 "msg": "Deleted repository `<reponame>`",
1076 "msg": "Deleted repository `<reponame>`",
1077 "success": true
1077 "success": true
1078 }
1078 }
1079 error: null
1079 error: null
1080 """
1080 """
1081
1081
1082 repo = get_repo_or_error(repoid)
1082 repo = get_repo_or_error(repoid)
1083 if not has_superadmin_permission(apiuser):
1083 if not has_superadmin_permission(apiuser):
1084 _perms = ('repository.admin',)
1084 _perms = ('repository.admin',)
1085 has_repo_permissions(apiuser, repoid, repo, _perms)
1085 has_repo_permissions(apiuser, repoid, repo, _perms)
1086
1086
1087 try:
1087 try:
1088 handle_forks = Optional.extract(forks)
1088 handle_forks = Optional.extract(forks)
1089 _forks_msg = ''
1089 _forks_msg = ''
1090 _forks = [f for f in repo.forks]
1090 _forks = [f for f in repo.forks]
1091 if handle_forks == 'detach':
1091 if handle_forks == 'detach':
1092 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1092 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1093 elif handle_forks == 'delete':
1093 elif handle_forks == 'delete':
1094 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1094 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1095 elif _forks:
1095 elif _forks:
1096 raise JSONRPCError(
1096 raise JSONRPCError(
1097 'Cannot delete `%s` it still contains attached forks' %
1097 'Cannot delete `%s` it still contains attached forks' %
1098 (repo.repo_name,)
1098 (repo.repo_name,)
1099 )
1099 )
1100
1100
1101 RepoModel().delete(repo, forks=forks)
1101 RepoModel().delete(repo, forks=forks)
1102 Session().commit()
1102 Session().commit()
1103 return {
1103 return {
1104 'msg': 'Deleted repository `%s`%s' % (
1104 'msg': 'Deleted repository `%s`%s' % (
1105 repo.repo_name, _forks_msg),
1105 repo.repo_name, _forks_msg),
1106 'success': True
1106 'success': True
1107 }
1107 }
1108 except Exception:
1108 except Exception:
1109 log.exception("Exception occurred while trying to delete repo")
1109 log.exception("Exception occurred while trying to delete repo")
1110 raise JSONRPCError(
1110 raise JSONRPCError(
1111 'failed to delete repository `%s`' % (repo.repo_name,)
1111 'failed to delete repository `%s`' % (repo.repo_name,)
1112 )
1112 )
1113
1113
1114
1114
1115 #TODO: marcink, change name ?
1115 #TODO: marcink, change name ?
1116 @jsonrpc_method()
1116 @jsonrpc_method()
1117 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1117 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1118 """
1118 """
1119 Invalidates the cache for the specified repository.
1119 Invalidates the cache for the specified repository.
1120
1120
1121 This command can only be run using an |authtoken| with admin rights to
1121 This command can only be run using an |authtoken| with admin rights to
1122 the specified repository.
1122 the specified repository.
1123
1123
1124 This command takes the following options:
1124 This command takes the following options:
1125
1125
1126 :param apiuser: This is filled automatically from |authtoken|.
1126 :param apiuser: This is filled automatically from |authtoken|.
1127 :type apiuser: AuthUser
1127 :type apiuser: AuthUser
1128 :param repoid: Sets the repository name or repository ID.
1128 :param repoid: Sets the repository name or repository ID.
1129 :type repoid: str or int
1129 :type repoid: str or int
1130 :param delete_keys: This deletes the invalidated keys instead of
1130 :param delete_keys: This deletes the invalidated keys instead of
1131 just flagging them.
1131 just flagging them.
1132 :type delete_keys: Optional(``True`` | ``False``)
1132 :type delete_keys: Optional(``True`` | ``False``)
1133
1133
1134 Example output:
1134 Example output:
1135
1135
1136 .. code-block:: bash
1136 .. code-block:: bash
1137
1137
1138 id : <id_given_in_input>
1138 id : <id_given_in_input>
1139 result : {
1139 result : {
1140 'msg': Cache for repository `<repository name>` was invalidated,
1140 'msg': Cache for repository `<repository name>` was invalidated,
1141 'repository': <repository name>
1141 'repository': <repository name>
1142 }
1142 }
1143 error : null
1143 error : null
1144
1144
1145 Example error output:
1145 Example error output:
1146
1146
1147 .. code-block:: bash
1147 .. code-block:: bash
1148
1148
1149 id : <id_given_in_input>
1149 id : <id_given_in_input>
1150 result : null
1150 result : null
1151 error : {
1151 error : {
1152 'Error occurred during cache invalidation action'
1152 'Error occurred during cache invalidation action'
1153 }
1153 }
1154
1154
1155 """
1155 """
1156
1156
1157 repo = get_repo_or_error(repoid)
1157 repo = get_repo_or_error(repoid)
1158 if not has_superadmin_permission(apiuser):
1158 if not has_superadmin_permission(apiuser):
1159 _perms = ('repository.admin', 'repository.write',)
1159 _perms = ('repository.admin', 'repository.write',)
1160 has_repo_permissions(apiuser, repoid, repo, _perms)
1160 has_repo_permissions(apiuser, repoid, repo, _perms)
1161
1161
1162 delete = Optional.extract(delete_keys)
1162 delete = Optional.extract(delete_keys)
1163 try:
1163 try:
1164 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1164 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1165 return {
1165 return {
1166 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1166 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1167 'repository': repo.repo_name
1167 'repository': repo.repo_name
1168 }
1168 }
1169 except Exception:
1169 except Exception:
1170 log.exception(
1170 log.exception(
1171 "Exception occurred while trying to invalidate repo cache")
1171 "Exception occurred while trying to invalidate repo cache")
1172 raise JSONRPCError(
1172 raise JSONRPCError(
1173 'Error occurred during cache invalidation action'
1173 'Error occurred during cache invalidation action'
1174 )
1174 )
1175
1175
1176
1176
1177 #TODO: marcink, change name ?
1177 #TODO: marcink, change name ?
1178 @jsonrpc_method()
1178 @jsonrpc_method()
1179 def lock(request, apiuser, repoid, locked=Optional(None),
1179 def lock(request, apiuser, repoid, locked=Optional(None),
1180 userid=Optional(OAttr('apiuser'))):
1180 userid=Optional(OAttr('apiuser'))):
1181 """
1181 """
1182 Sets the lock state of the specified |repo| by the given user.
1182 Sets the lock state of the specified |repo| by the given user.
1183 From more information, see :ref:`repo-locking`.
1183 From more information, see :ref:`repo-locking`.
1184
1184
1185 * If the ``userid`` option is not set, the repository is locked to the
1185 * If the ``userid`` option is not set, the repository is locked to the
1186 user who called the method.
1186 user who called the method.
1187 * If the ``locked`` parameter is not set, the current lock state of the
1187 * If the ``locked`` parameter is not set, the current lock state of the
1188 repository is displayed.
1188 repository is displayed.
1189
1189
1190 This command can only be run using an |authtoken| with admin rights to
1190 This command can only be run using an |authtoken| with admin rights to
1191 the specified repository.
1191 the specified repository.
1192
1192
1193 This command takes the following options:
1193 This command takes the following options:
1194
1194
1195 :param apiuser: This is filled automatically from the |authtoken|.
1195 :param apiuser: This is filled automatically from the |authtoken|.
1196 :type apiuser: AuthUser
1196 :type apiuser: AuthUser
1197 :param repoid: Sets the repository name or repository ID.
1197 :param repoid: Sets the repository name or repository ID.
1198 :type repoid: str or int
1198 :type repoid: str or int
1199 :param locked: Sets the lock state.
1199 :param locked: Sets the lock state.
1200 :type locked: Optional(``True`` | ``False``)
1200 :type locked: Optional(``True`` | ``False``)
1201 :param userid: Set the repository lock to this user.
1201 :param userid: Set the repository lock to this user.
1202 :type userid: Optional(str or int)
1202 :type userid: Optional(str or int)
1203
1203
1204 Example error output:
1204 Example error output:
1205
1205
1206 .. code-block:: bash
1206 .. code-block:: bash
1207
1207
1208 id : <id_given_in_input>
1208 id : <id_given_in_input>
1209 result : {
1209 result : {
1210 'repo': '<reponame>',
1210 'repo': '<reponame>',
1211 'locked': <bool: lock state>,
1211 'locked': <bool: lock state>,
1212 'locked_since': <int: lock timestamp>,
1212 'locked_since': <int: lock timestamp>,
1213 'locked_by': <username of person who made the lock>,
1213 'locked_by': <username of person who made the lock>,
1214 'lock_reason': <str: reason for locking>,
1214 'lock_reason': <str: reason for locking>,
1215 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1215 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1216 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1216 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1217 or
1217 or
1218 'msg': 'Repo `<repository name>` not locked.'
1218 'msg': 'Repo `<repository name>` not locked.'
1219 or
1219 or
1220 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1220 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1221 }
1221 }
1222 error : null
1222 error : null
1223
1223
1224 Example error output:
1224 Example error output:
1225
1225
1226 .. code-block:: bash
1226 .. code-block:: bash
1227
1227
1228 id : <id_given_in_input>
1228 id : <id_given_in_input>
1229 result : null
1229 result : null
1230 error : {
1230 error : {
1231 'Error occurred locking repository `<reponame>`
1231 'Error occurred locking repository `<reponame>`
1232 }
1232 }
1233 """
1233 """
1234
1234
1235 repo = get_repo_or_error(repoid)
1235 repo = get_repo_or_error(repoid)
1236 if not has_superadmin_permission(apiuser):
1236 if not has_superadmin_permission(apiuser):
1237 # check if we have at least write permission for this repo !
1237 # check if we have at least write permission for this repo !
1238 _perms = ('repository.admin', 'repository.write',)
1238 _perms = ('repository.admin', 'repository.write',)
1239 has_repo_permissions(apiuser, repoid, repo, _perms)
1239 has_repo_permissions(apiuser, repoid, repo, _perms)
1240
1240
1241 # make sure normal user does not pass someone else userid,
1241 # make sure normal user does not pass someone else userid,
1242 # he is not allowed to do that
1242 # he is not allowed to do that
1243 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1243 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1244 raise JSONRPCError('userid is not the same as your user')
1244 raise JSONRPCError('userid is not the same as your user')
1245
1245
1246 if isinstance(userid, Optional):
1246 if isinstance(userid, Optional):
1247 userid = apiuser.user_id
1247 userid = apiuser.user_id
1248
1248
1249 user = get_user_or_error(userid)
1249 user = get_user_or_error(userid)
1250
1250
1251 if isinstance(locked, Optional):
1251 if isinstance(locked, Optional):
1252 lockobj = repo.locked
1252 lockobj = repo.locked
1253
1253
1254 if lockobj[0] is None:
1254 if lockobj[0] is None:
1255 _d = {
1255 _d = {
1256 'repo': repo.repo_name,
1256 'repo': repo.repo_name,
1257 'locked': False,
1257 'locked': False,
1258 'locked_since': None,
1258 'locked_since': None,
1259 'locked_by': None,
1259 'locked_by': None,
1260 'lock_reason': None,
1260 'lock_reason': None,
1261 'lock_state_changed': False,
1261 'lock_state_changed': False,
1262 'msg': 'Repo `%s` not locked.' % repo.repo_name
1262 'msg': 'Repo `%s` not locked.' % repo.repo_name
1263 }
1263 }
1264 return _d
1264 return _d
1265 else:
1265 else:
1266 _user_id, _time, _reason = lockobj
1266 _user_id, _time, _reason = lockobj
1267 lock_user = get_user_or_error(userid)
1267 lock_user = get_user_or_error(userid)
1268 _d = {
1268 _d = {
1269 'repo': repo.repo_name,
1269 'repo': repo.repo_name,
1270 'locked': True,
1270 'locked': True,
1271 'locked_since': _time,
1271 'locked_since': _time,
1272 'locked_by': lock_user.username,
1272 'locked_by': lock_user.username,
1273 'lock_reason': _reason,
1273 'lock_reason': _reason,
1274 'lock_state_changed': False,
1274 'lock_state_changed': False,
1275 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1275 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1276 % (repo.repo_name, lock_user.username,
1276 % (repo.repo_name, lock_user.username,
1277 json.dumps(time_to_datetime(_time))))
1277 json.dumps(time_to_datetime(_time))))
1278 }
1278 }
1279 return _d
1279 return _d
1280
1280
1281 # force locked state through a flag
1281 # force locked state through a flag
1282 else:
1282 else:
1283 locked = str2bool(locked)
1283 locked = str2bool(locked)
1284 lock_reason = Repository.LOCK_API
1284 lock_reason = Repository.LOCK_API
1285 try:
1285 try:
1286 if locked:
1286 if locked:
1287 lock_time = time.time()
1287 lock_time = time.time()
1288 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1288 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1289 else:
1289 else:
1290 lock_time = None
1290 lock_time = None
1291 Repository.unlock(repo)
1291 Repository.unlock(repo)
1292 _d = {
1292 _d = {
1293 'repo': repo.repo_name,
1293 'repo': repo.repo_name,
1294 'locked': locked,
1294 'locked': locked,
1295 'locked_since': lock_time,
1295 'locked_since': lock_time,
1296 'locked_by': user.username,
1296 'locked_by': user.username,
1297 'lock_reason': lock_reason,
1297 'lock_reason': lock_reason,
1298 'lock_state_changed': True,
1298 'lock_state_changed': True,
1299 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1299 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1300 % (user.username, repo.repo_name, locked))
1300 % (user.username, repo.repo_name, locked))
1301 }
1301 }
1302 return _d
1302 return _d
1303 except Exception:
1303 except Exception:
1304 log.exception(
1304 log.exception(
1305 "Exception occurred while trying to lock repository")
1305 "Exception occurred while trying to lock repository")
1306 raise JSONRPCError(
1306 raise JSONRPCError(
1307 'Error occurred locking repository `%s`' % repo.repo_name
1307 'Error occurred locking repository `%s`' % repo.repo_name
1308 )
1308 )
1309
1309
1310
1310
1311 @jsonrpc_method()
1311 @jsonrpc_method()
1312 def comment_commit(
1312 def comment_commit(
1313 request, apiuser, repoid, commit_id, message,
1313 request, apiuser, repoid, commit_id, message,
1314 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1314 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1315 """
1315 """
1316 Set a commit comment, and optionally change the status of the commit.
1316 Set a commit comment, and optionally change the status of the commit.
1317
1317
1318 :param apiuser: This is filled automatically from the |authtoken|.
1318 :param apiuser: This is filled automatically from the |authtoken|.
1319 :type apiuser: AuthUser
1319 :type apiuser: AuthUser
1320 :param repoid: Set the repository name or repository ID.
1320 :param repoid: Set the repository name or repository ID.
1321 :type repoid: str or int
1321 :type repoid: str or int
1322 :param commit_id: Specify the commit_id for which to set a comment.
1322 :param commit_id: Specify the commit_id for which to set a comment.
1323 :type commit_id: str
1323 :type commit_id: str
1324 :param message: The comment text.
1324 :param message: The comment text.
1325 :type message: str
1325 :type message: str
1326 :param userid: Set the user name of the comment creator.
1326 :param userid: Set the user name of the comment creator.
1327 :type userid: Optional(str or int)
1327 :type userid: Optional(str or int)
1328 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1328 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1329 'under_review'
1329 'under_review'
1330 :type status: str
1330 :type status: str
1331
1331
1332 Example error output:
1332 Example error output:
1333
1333
1334 .. code-block:: json
1334 .. code-block:: json
1335
1335
1336 {
1336 {
1337 "id" : <id_given_in_input>,
1337 "id" : <id_given_in_input>,
1338 "result" : {
1338 "result" : {
1339 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1339 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1340 "status_change": null or <status>,
1340 "status_change": null or <status>,
1341 "success": true
1341 "success": true
1342 },
1342 },
1343 "error" : null
1343 "error" : null
1344 }
1344 }
1345
1345
1346 """
1346 """
1347 repo = get_repo_or_error(repoid)
1347 repo = get_repo_or_error(repoid)
1348 if not has_superadmin_permission(apiuser):
1348 if not has_superadmin_permission(apiuser):
1349 _perms = ('repository.read', 'repository.write', 'repository.admin')
1349 _perms = ('repository.read', 'repository.write', 'repository.admin')
1350 has_repo_permissions(apiuser, repoid, repo, _perms)
1350 has_repo_permissions(apiuser, repoid, repo, _perms)
1351
1351
1352 if isinstance(userid, Optional):
1352 if isinstance(userid, Optional):
1353 userid = apiuser.user_id
1353 userid = apiuser.user_id
1354
1354
1355 user = get_user_or_error(userid)
1355 user = get_user_or_error(userid)
1356 status = Optional.extract(status)
1356 status = Optional.extract(status)
1357
1357
1358 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1358 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1359 if status and status not in allowed_statuses:
1359 if status and status not in allowed_statuses:
1360 raise JSONRPCError('Bad status, must be on '
1360 raise JSONRPCError('Bad status, must be on '
1361 'of %s got %s' % (allowed_statuses, status,))
1361 'of %s got %s' % (allowed_statuses, status,))
1362
1362
1363 try:
1363 try:
1364 rc_config = SettingsModel().get_all_settings()
1364 rc_config = SettingsModel().get_all_settings()
1365 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1365 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1366
1366 status_change_label = ChangesetStatus.get_status_lbl(status)
1367 comm = ChangesetCommentsModel().create(
1367 comm = ChangesetCommentsModel().create(
1368 message, repo, user, revision=commit_id, status_change=status,
1368 message, repo, user, revision=commit_id,
1369 status_change=status_change_label,
1370 status_change_type=status,
1369 renderer=renderer)
1371 renderer=renderer)
1370 if status:
1372 if status:
1371 # also do a status change
1373 # also do a status change
1372 try:
1374 try:
1373 ChangesetStatusModel().set_status(
1375 ChangesetStatusModel().set_status(
1374 repo, status, user, comm, revision=commit_id,
1376 repo, status, user, comm, revision=commit_id,
1375 dont_allow_on_closed_pull_request=True
1377 dont_allow_on_closed_pull_request=True
1376 )
1378 )
1377 except StatusChangeOnClosedPullRequestError:
1379 except StatusChangeOnClosedPullRequestError:
1378 log.exception(
1380 log.exception(
1379 "Exception occurred while trying to change repo commit status")
1381 "Exception occurred while trying to change repo commit status")
1380 msg = ('Changing status on a changeset associated with '
1382 msg = ('Changing status on a changeset associated with '
1381 'a closed pull request is not allowed')
1383 'a closed pull request is not allowed')
1382 raise JSONRPCError(msg)
1384 raise JSONRPCError(msg)
1383
1385
1384 Session().commit()
1386 Session().commit()
1385 return {
1387 return {
1386 'msg': (
1388 'msg': (
1387 'Commented on commit `%s` for repository `%s`' % (
1389 'Commented on commit `%s` for repository `%s`' % (
1388 comm.revision, repo.repo_name)),
1390 comm.revision, repo.repo_name)),
1389 'status_change': status,
1391 'status_change': status,
1390 'success': True,
1392 'success': True,
1391 }
1393 }
1392 except JSONRPCError:
1394 except JSONRPCError:
1393 # catch any inside errors, and re-raise them to prevent from
1395 # catch any inside errors, and re-raise them to prevent from
1394 # below global catch to silence them
1396 # below global catch to silence them
1395 raise
1397 raise
1396 except Exception:
1398 except Exception:
1397 log.exception("Exception occurred while trying to comment on commit")
1399 log.exception("Exception occurred while trying to comment on commit")
1398 raise JSONRPCError(
1400 raise JSONRPCError(
1399 'failed to set comment on repository `%s`' % (repo.repo_name,)
1401 'failed to set comment on repository `%s`' % (repo.repo_name,)
1400 )
1402 )
1401
1403
1402
1404
1403 @jsonrpc_method()
1405 @jsonrpc_method()
1404 def grant_user_permission(request, apiuser, repoid, userid, perm):
1406 def grant_user_permission(request, apiuser, repoid, userid, perm):
1405 """
1407 """
1406 Grant permissions for the specified user on the given repository,
1408 Grant permissions for the specified user on the given repository,
1407 or update existing permissions if found.
1409 or update existing permissions if found.
1408
1410
1409 This command can only be run using an |authtoken| with admin
1411 This command can only be run using an |authtoken| with admin
1410 permissions on the |repo|.
1412 permissions on the |repo|.
1411
1413
1412 :param apiuser: This is filled automatically from the |authtoken|.
1414 :param apiuser: This is filled automatically from the |authtoken|.
1413 :type apiuser: AuthUser
1415 :type apiuser: AuthUser
1414 :param repoid: Set the repository name or repository ID.
1416 :param repoid: Set the repository name or repository ID.
1415 :type repoid: str or int
1417 :type repoid: str or int
1416 :param userid: Set the user name.
1418 :param userid: Set the user name.
1417 :type userid: str
1419 :type userid: str
1418 :param perm: Set the user permissions, using the following format
1420 :param perm: Set the user permissions, using the following format
1419 ``(repository.(none|read|write|admin))``
1421 ``(repository.(none|read|write|admin))``
1420 :type perm: str
1422 :type perm: str
1421
1423
1422 Example output:
1424 Example output:
1423
1425
1424 .. code-block:: bash
1426 .. code-block:: bash
1425
1427
1426 id : <id_given_in_input>
1428 id : <id_given_in_input>
1427 result: {
1429 result: {
1428 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1430 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1429 "success": true
1431 "success": true
1430 }
1432 }
1431 error: null
1433 error: null
1432 """
1434 """
1433
1435
1434 repo = get_repo_or_error(repoid)
1436 repo = get_repo_or_error(repoid)
1435 user = get_user_or_error(userid)
1437 user = get_user_or_error(userid)
1436 perm = get_perm_or_error(perm)
1438 perm = get_perm_or_error(perm)
1437 if not has_superadmin_permission(apiuser):
1439 if not has_superadmin_permission(apiuser):
1438 _perms = ('repository.admin',)
1440 _perms = ('repository.admin',)
1439 has_repo_permissions(apiuser, repoid, repo, _perms)
1441 has_repo_permissions(apiuser, repoid, repo, _perms)
1440
1442
1441 try:
1443 try:
1442
1444
1443 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1445 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1444
1446
1445 Session().commit()
1447 Session().commit()
1446 return {
1448 return {
1447 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1449 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1448 perm.permission_name, user.username, repo.repo_name
1450 perm.permission_name, user.username, repo.repo_name
1449 ),
1451 ),
1450 'success': True
1452 'success': True
1451 }
1453 }
1452 except Exception:
1454 except Exception:
1453 log.exception(
1455 log.exception(
1454 "Exception occurred while trying edit permissions for repo")
1456 "Exception occurred while trying edit permissions for repo")
1455 raise JSONRPCError(
1457 raise JSONRPCError(
1456 'failed to edit permission for user: `%s` in repo: `%s`' % (
1458 'failed to edit permission for user: `%s` in repo: `%s`' % (
1457 userid, repoid
1459 userid, repoid
1458 )
1460 )
1459 )
1461 )
1460
1462
1461
1463
1462 @jsonrpc_method()
1464 @jsonrpc_method()
1463 def revoke_user_permission(request, apiuser, repoid, userid):
1465 def revoke_user_permission(request, apiuser, repoid, userid):
1464 """
1466 """
1465 Revoke permission for a user on the specified repository.
1467 Revoke permission for a user on the specified repository.
1466
1468
1467 This command can only be run using an |authtoken| with admin
1469 This command can only be run using an |authtoken| with admin
1468 permissions on the |repo|.
1470 permissions on the |repo|.
1469
1471
1470 :param apiuser: This is filled automatically from the |authtoken|.
1472 :param apiuser: This is filled automatically from the |authtoken|.
1471 :type apiuser: AuthUser
1473 :type apiuser: AuthUser
1472 :param repoid: Set the repository name or repository ID.
1474 :param repoid: Set the repository name or repository ID.
1473 :type repoid: str or int
1475 :type repoid: str or int
1474 :param userid: Set the user name of revoked user.
1476 :param userid: Set the user name of revoked user.
1475 :type userid: str or int
1477 :type userid: str or int
1476
1478
1477 Example error output:
1479 Example error output:
1478
1480
1479 .. code-block:: bash
1481 .. code-block:: bash
1480
1482
1481 id : <id_given_in_input>
1483 id : <id_given_in_input>
1482 result: {
1484 result: {
1483 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1485 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1484 "success": true
1486 "success": true
1485 }
1487 }
1486 error: null
1488 error: null
1487 """
1489 """
1488
1490
1489 repo = get_repo_or_error(repoid)
1491 repo = get_repo_or_error(repoid)
1490 user = get_user_or_error(userid)
1492 user = get_user_or_error(userid)
1491 if not has_superadmin_permission(apiuser):
1493 if not has_superadmin_permission(apiuser):
1492 _perms = ('repository.admin',)
1494 _perms = ('repository.admin',)
1493 has_repo_permissions(apiuser, repoid, repo, _perms)
1495 has_repo_permissions(apiuser, repoid, repo, _perms)
1494
1496
1495 try:
1497 try:
1496 RepoModel().revoke_user_permission(repo=repo, user=user)
1498 RepoModel().revoke_user_permission(repo=repo, user=user)
1497 Session().commit()
1499 Session().commit()
1498 return {
1500 return {
1499 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1501 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1500 user.username, repo.repo_name
1502 user.username, repo.repo_name
1501 ),
1503 ),
1502 'success': True
1504 'success': True
1503 }
1505 }
1504 except Exception:
1506 except Exception:
1505 log.exception(
1507 log.exception(
1506 "Exception occurred while trying revoke permissions to repo")
1508 "Exception occurred while trying revoke permissions to repo")
1507 raise JSONRPCError(
1509 raise JSONRPCError(
1508 'failed to edit permission for user: `%s` in repo: `%s`' % (
1510 'failed to edit permission for user: `%s` in repo: `%s`' % (
1509 userid, repoid
1511 userid, repoid
1510 )
1512 )
1511 )
1513 )
1512
1514
1513
1515
1514 @jsonrpc_method()
1516 @jsonrpc_method()
1515 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1517 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1516 """
1518 """
1517 Grant permission for a user group on the specified repository,
1519 Grant permission for a user group on the specified repository,
1518 or update existing permissions.
1520 or update existing permissions.
1519
1521
1520 This command can only be run using an |authtoken| with admin
1522 This command can only be run using an |authtoken| with admin
1521 permissions on the |repo|.
1523 permissions on the |repo|.
1522
1524
1523 :param apiuser: This is filled automatically from the |authtoken|.
1525 :param apiuser: This is filled automatically from the |authtoken|.
1524 :type apiuser: AuthUser
1526 :type apiuser: AuthUser
1525 :param repoid: Set the repository name or repository ID.
1527 :param repoid: Set the repository name or repository ID.
1526 :type repoid: str or int
1528 :type repoid: str or int
1527 :param usergroupid: Specify the ID of the user group.
1529 :param usergroupid: Specify the ID of the user group.
1528 :type usergroupid: str or int
1530 :type usergroupid: str or int
1529 :param perm: Set the user group permissions using the following
1531 :param perm: Set the user group permissions using the following
1530 format: (repository.(none|read|write|admin))
1532 format: (repository.(none|read|write|admin))
1531 :type perm: str
1533 :type perm: str
1532
1534
1533 Example output:
1535 Example output:
1534
1536
1535 .. code-block:: bash
1537 .. code-block:: bash
1536
1538
1537 id : <id_given_in_input>
1539 id : <id_given_in_input>
1538 result : {
1540 result : {
1539 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1541 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1540 "success": true
1542 "success": true
1541
1543
1542 }
1544 }
1543 error : null
1545 error : null
1544
1546
1545 Example error output:
1547 Example error output:
1546
1548
1547 .. code-block:: bash
1549 .. code-block:: bash
1548
1550
1549 id : <id_given_in_input>
1551 id : <id_given_in_input>
1550 result : null
1552 result : null
1551 error : {
1553 error : {
1552 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1554 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1553 }
1555 }
1554
1556
1555 """
1557 """
1556
1558
1557 repo = get_repo_or_error(repoid)
1559 repo = get_repo_or_error(repoid)
1558 perm = get_perm_or_error(perm)
1560 perm = get_perm_or_error(perm)
1559 if not has_superadmin_permission(apiuser):
1561 if not has_superadmin_permission(apiuser):
1560 _perms = ('repository.admin',)
1562 _perms = ('repository.admin',)
1561 has_repo_permissions(apiuser, repoid, repo, _perms)
1563 has_repo_permissions(apiuser, repoid, repo, _perms)
1562
1564
1563 user_group = get_user_group_or_error(usergroupid)
1565 user_group = get_user_group_or_error(usergroupid)
1564 if not has_superadmin_permission(apiuser):
1566 if not has_superadmin_permission(apiuser):
1565 # check if we have at least read permission for this user group !
1567 # check if we have at least read permission for this user group !
1566 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1568 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1567 if not HasUserGroupPermissionAnyApi(*_perms)(
1569 if not HasUserGroupPermissionAnyApi(*_perms)(
1568 user=apiuser, user_group_name=user_group.users_group_name):
1570 user=apiuser, user_group_name=user_group.users_group_name):
1569 raise JSONRPCError(
1571 raise JSONRPCError(
1570 'user group `%s` does not exist' % (usergroupid,))
1572 'user group `%s` does not exist' % (usergroupid,))
1571
1573
1572 try:
1574 try:
1573 RepoModel().grant_user_group_permission(
1575 RepoModel().grant_user_group_permission(
1574 repo=repo, group_name=user_group, perm=perm)
1576 repo=repo, group_name=user_group, perm=perm)
1575
1577
1576 Session().commit()
1578 Session().commit()
1577 return {
1579 return {
1578 'msg': 'Granted perm: `%s` for user group: `%s` in '
1580 'msg': 'Granted perm: `%s` for user group: `%s` in '
1579 'repo: `%s`' % (
1581 'repo: `%s`' % (
1580 perm.permission_name, user_group.users_group_name,
1582 perm.permission_name, user_group.users_group_name,
1581 repo.repo_name
1583 repo.repo_name
1582 ),
1584 ),
1583 'success': True
1585 'success': True
1584 }
1586 }
1585 except Exception:
1587 except Exception:
1586 log.exception(
1588 log.exception(
1587 "Exception occurred while trying change permission on repo")
1589 "Exception occurred while trying change permission on repo")
1588 raise JSONRPCError(
1590 raise JSONRPCError(
1589 'failed to edit permission for user group: `%s` in '
1591 'failed to edit permission for user group: `%s` in '
1590 'repo: `%s`' % (
1592 'repo: `%s`' % (
1591 usergroupid, repo.repo_name
1593 usergroupid, repo.repo_name
1592 )
1594 )
1593 )
1595 )
1594
1596
1595
1597
1596 @jsonrpc_method()
1598 @jsonrpc_method()
1597 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1599 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1598 """
1600 """
1599 Revoke the permissions of a user group on a given repository.
1601 Revoke the permissions of a user group on a given repository.
1600
1602
1601 This command can only be run using an |authtoken| with admin
1603 This command can only be run using an |authtoken| with admin
1602 permissions on the |repo|.
1604 permissions on the |repo|.
1603
1605
1604 :param apiuser: This is filled automatically from the |authtoken|.
1606 :param apiuser: This is filled automatically from the |authtoken|.
1605 :type apiuser: AuthUser
1607 :type apiuser: AuthUser
1606 :param repoid: Set the repository name or repository ID.
1608 :param repoid: Set the repository name or repository ID.
1607 :type repoid: str or int
1609 :type repoid: str or int
1608 :param usergroupid: Specify the user group ID.
1610 :param usergroupid: Specify the user group ID.
1609 :type usergroupid: str or int
1611 :type usergroupid: str or int
1610
1612
1611 Example output:
1613 Example output:
1612
1614
1613 .. code-block:: bash
1615 .. code-block:: bash
1614
1616
1615 id : <id_given_in_input>
1617 id : <id_given_in_input>
1616 result: {
1618 result: {
1617 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1619 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1618 "success": true
1620 "success": true
1619 }
1621 }
1620 error: null
1622 error: null
1621 """
1623 """
1622
1624
1623 repo = get_repo_or_error(repoid)
1625 repo = get_repo_or_error(repoid)
1624 if not has_superadmin_permission(apiuser):
1626 if not has_superadmin_permission(apiuser):
1625 _perms = ('repository.admin',)
1627 _perms = ('repository.admin',)
1626 has_repo_permissions(apiuser, repoid, repo, _perms)
1628 has_repo_permissions(apiuser, repoid, repo, _perms)
1627
1629
1628 user_group = get_user_group_or_error(usergroupid)
1630 user_group = get_user_group_or_error(usergroupid)
1629 if not has_superadmin_permission(apiuser):
1631 if not has_superadmin_permission(apiuser):
1630 # check if we have at least read permission for this user group !
1632 # check if we have at least read permission for this user group !
1631 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1633 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1632 if not HasUserGroupPermissionAnyApi(*_perms)(
1634 if not HasUserGroupPermissionAnyApi(*_perms)(
1633 user=apiuser, user_group_name=user_group.users_group_name):
1635 user=apiuser, user_group_name=user_group.users_group_name):
1634 raise JSONRPCError(
1636 raise JSONRPCError(
1635 'user group `%s` does not exist' % (usergroupid,))
1637 'user group `%s` does not exist' % (usergroupid,))
1636
1638
1637 try:
1639 try:
1638 RepoModel().revoke_user_group_permission(
1640 RepoModel().revoke_user_group_permission(
1639 repo=repo, group_name=user_group)
1641 repo=repo, group_name=user_group)
1640
1642
1641 Session().commit()
1643 Session().commit()
1642 return {
1644 return {
1643 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1645 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1644 user_group.users_group_name, repo.repo_name
1646 user_group.users_group_name, repo.repo_name
1645 ),
1647 ),
1646 'success': True
1648 'success': True
1647 }
1649 }
1648 except Exception:
1650 except Exception:
1649 log.exception("Exception occurred while trying revoke "
1651 log.exception("Exception occurred while trying revoke "
1650 "user group permission on repo")
1652 "user group permission on repo")
1651 raise JSONRPCError(
1653 raise JSONRPCError(
1652 'failed to edit permission for user group: `%s` in '
1654 'failed to edit permission for user group: `%s` in '
1653 'repo: `%s`' % (
1655 'repo: `%s`' % (
1654 user_group.users_group_name, repo.repo_name
1656 user_group.users_group_name, repo.repo_name
1655 )
1657 )
1656 )
1658 )
1657
1659
1658
1660
1659 @jsonrpc_method()
1661 @jsonrpc_method()
1660 def pull(request, apiuser, repoid):
1662 def pull(request, apiuser, repoid):
1661 """
1663 """
1662 Triggers a pull on the given repository from a remote location. You
1664 Triggers a pull on the given repository from a remote location. You
1663 can use this to keep remote repositories up-to-date.
1665 can use this to keep remote repositories up-to-date.
1664
1666
1665 This command can only be run using an |authtoken| with admin
1667 This command can only be run using an |authtoken| with admin
1666 rights to the specified repository. For more information,
1668 rights to the specified repository. For more information,
1667 see :ref:`config-token-ref`.
1669 see :ref:`config-token-ref`.
1668
1670
1669 This command takes the following options:
1671 This command takes the following options:
1670
1672
1671 :param apiuser: This is filled automatically from the |authtoken|.
1673 :param apiuser: This is filled automatically from the |authtoken|.
1672 :type apiuser: AuthUser
1674 :type apiuser: AuthUser
1673 :param repoid: The repository name or repository ID.
1675 :param repoid: The repository name or repository ID.
1674 :type repoid: str or int
1676 :type repoid: str or int
1675
1677
1676 Example output:
1678 Example output:
1677
1679
1678 .. code-block:: bash
1680 .. code-block:: bash
1679
1681
1680 id : <id_given_in_input>
1682 id : <id_given_in_input>
1681 result : {
1683 result : {
1682 "msg": "Pulled from `<repository name>`"
1684 "msg": "Pulled from `<repository name>`"
1683 "repository": "<repository name>"
1685 "repository": "<repository name>"
1684 }
1686 }
1685 error : null
1687 error : null
1686
1688
1687 Example error output:
1689 Example error output:
1688
1690
1689 .. code-block:: bash
1691 .. code-block:: bash
1690
1692
1691 id : <id_given_in_input>
1693 id : <id_given_in_input>
1692 result : null
1694 result : null
1693 error : {
1695 error : {
1694 "Unable to pull changes from `<reponame>`"
1696 "Unable to pull changes from `<reponame>`"
1695 }
1697 }
1696
1698
1697 """
1699 """
1698
1700
1699 repo = get_repo_or_error(repoid)
1701 repo = get_repo_or_error(repoid)
1700 if not has_superadmin_permission(apiuser):
1702 if not has_superadmin_permission(apiuser):
1701 _perms = ('repository.admin',)
1703 _perms = ('repository.admin',)
1702 has_repo_permissions(apiuser, repoid, repo, _perms)
1704 has_repo_permissions(apiuser, repoid, repo, _perms)
1703
1705
1704 try:
1706 try:
1705 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1707 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1706 return {
1708 return {
1707 'msg': 'Pulled from `%s`' % repo.repo_name,
1709 'msg': 'Pulled from `%s`' % repo.repo_name,
1708 'repository': repo.repo_name
1710 'repository': repo.repo_name
1709 }
1711 }
1710 except Exception:
1712 except Exception:
1711 log.exception("Exception occurred while trying to "
1713 log.exception("Exception occurred while trying to "
1712 "pull changes from remote location")
1714 "pull changes from remote location")
1713 raise JSONRPCError(
1715 raise JSONRPCError(
1714 'Unable to pull changes from `%s`' % repo.repo_name
1716 'Unable to pull changes from `%s`' % repo.repo_name
1715 )
1717 )
1716
1718
1717
1719
1718 @jsonrpc_method()
1720 @jsonrpc_method()
1719 def strip(request, apiuser, repoid, revision, branch):
1721 def strip(request, apiuser, repoid, revision, branch):
1720 """
1722 """
1721 Strips the given revision from the specified repository.
1723 Strips the given revision from the specified repository.
1722
1724
1723 * This will remove the revision and all of its decendants.
1725 * This will remove the revision and all of its decendants.
1724
1726
1725 This command can only be run using an |authtoken| with admin rights to
1727 This command can only be run using an |authtoken| with admin rights to
1726 the specified repository.
1728 the specified repository.
1727
1729
1728 This command takes the following options:
1730 This command takes the following options:
1729
1731
1730 :param apiuser: This is filled automatically from the |authtoken|.
1732 :param apiuser: This is filled automatically from the |authtoken|.
1731 :type apiuser: AuthUser
1733 :type apiuser: AuthUser
1732 :param repoid: The repository name or repository ID.
1734 :param repoid: The repository name or repository ID.
1733 :type repoid: str or int
1735 :type repoid: str or int
1734 :param revision: The revision you wish to strip.
1736 :param revision: The revision you wish to strip.
1735 :type revision: str
1737 :type revision: str
1736 :param branch: The branch from which to strip the revision.
1738 :param branch: The branch from which to strip the revision.
1737 :type branch: str
1739 :type branch: str
1738
1740
1739 Example output:
1741 Example output:
1740
1742
1741 .. code-block:: bash
1743 .. code-block:: bash
1742
1744
1743 id : <id_given_in_input>
1745 id : <id_given_in_input>
1744 result : {
1746 result : {
1745 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1747 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1746 "repository": "<repository name>"
1748 "repository": "<repository name>"
1747 }
1749 }
1748 error : null
1750 error : null
1749
1751
1750 Example error output:
1752 Example error output:
1751
1753
1752 .. code-block:: bash
1754 .. code-block:: bash
1753
1755
1754 id : <id_given_in_input>
1756 id : <id_given_in_input>
1755 result : null
1757 result : null
1756 error : {
1758 error : {
1757 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1759 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1758 }
1760 }
1759
1761
1760 """
1762 """
1761
1763
1762 repo = get_repo_or_error(repoid)
1764 repo = get_repo_or_error(repoid)
1763 if not has_superadmin_permission(apiuser):
1765 if not has_superadmin_permission(apiuser):
1764 _perms = ('repository.admin',)
1766 _perms = ('repository.admin',)
1765 has_repo_permissions(apiuser, repoid, repo, _perms)
1767 has_repo_permissions(apiuser, repoid, repo, _perms)
1766
1768
1767 try:
1769 try:
1768 ScmModel().strip(repo, revision, branch)
1770 ScmModel().strip(repo, revision, branch)
1769 return {
1771 return {
1770 'msg': 'Stripped commit %s from repo `%s`' % (
1772 'msg': 'Stripped commit %s from repo `%s`' % (
1771 revision, repo.repo_name),
1773 revision, repo.repo_name),
1772 'repository': repo.repo_name
1774 'repository': repo.repo_name
1773 }
1775 }
1774 except Exception:
1776 except Exception:
1775 log.exception("Exception while trying to strip")
1777 log.exception("Exception while trying to strip")
1776 raise JSONRPCError(
1778 raise JSONRPCError(
1777 'Unable to strip commit %s from repo `%s`' % (
1779 'Unable to strip commit %s from repo `%s`' % (
1778 revision, repo.repo_name)
1780 revision, repo.repo_name)
1779 )
1781 )
1780
1782
1781
1783
1782 @jsonrpc_method()
1784 @jsonrpc_method()
1783 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1785 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1784 """
1786 """
1785 Returns all settings for a repository. If key is given it only returns the
1787 Returns all settings for a repository. If key is given it only returns the
1786 setting identified by the key or null.
1788 setting identified by the key or null.
1787
1789
1788 :param apiuser: This is filled automatically from the |authtoken|.
1790 :param apiuser: This is filled automatically from the |authtoken|.
1789 :type apiuser: AuthUser
1791 :type apiuser: AuthUser
1790 :param repoid: The repository name or repository id.
1792 :param repoid: The repository name or repository id.
1791 :type repoid: str or int
1793 :type repoid: str or int
1792 :param key: Key of the setting to return.
1794 :param key: Key of the setting to return.
1793 :type: key: Optional(str)
1795 :type: key: Optional(str)
1794
1796
1795 Example output:
1797 Example output:
1796
1798
1797 .. code-block:: bash
1799 .. code-block:: bash
1798
1800
1799 {
1801 {
1800 "error": null,
1802 "error": null,
1801 "id": 237,
1803 "id": 237,
1802 "result": {
1804 "result": {
1803 "extensions_largefiles": true,
1805 "extensions_largefiles": true,
1804 "hooks_changegroup_push_logger": true,
1806 "hooks_changegroup_push_logger": true,
1805 "hooks_changegroup_repo_size": false,
1807 "hooks_changegroup_repo_size": false,
1806 "hooks_outgoing_pull_logger": true,
1808 "hooks_outgoing_pull_logger": true,
1807 "phases_publish": "True",
1809 "phases_publish": "True",
1808 "rhodecode_hg_use_rebase_for_merging": true,
1810 "rhodecode_hg_use_rebase_for_merging": true,
1809 "rhodecode_pr_merge_enabled": true,
1811 "rhodecode_pr_merge_enabled": true,
1810 "rhodecode_use_outdated_comments": true
1812 "rhodecode_use_outdated_comments": true
1811 }
1813 }
1812 }
1814 }
1813 """
1815 """
1814
1816
1815 # Restrict access to this api method to admins only.
1817 # Restrict access to this api method to admins only.
1816 if not has_superadmin_permission(apiuser):
1818 if not has_superadmin_permission(apiuser):
1817 raise JSONRPCForbidden()
1819 raise JSONRPCForbidden()
1818
1820
1819 try:
1821 try:
1820 repo = get_repo_or_error(repoid)
1822 repo = get_repo_or_error(repoid)
1821 settings_model = VcsSettingsModel(repo=repo)
1823 settings_model = VcsSettingsModel(repo=repo)
1822 settings = settings_model.get_global_settings()
1824 settings = settings_model.get_global_settings()
1823 settings.update(settings_model.get_repo_settings())
1825 settings.update(settings_model.get_repo_settings())
1824
1826
1825 # If only a single setting is requested fetch it from all settings.
1827 # If only a single setting is requested fetch it from all settings.
1826 key = Optional.extract(key)
1828 key = Optional.extract(key)
1827 if key is not None:
1829 if key is not None:
1828 settings = settings.get(key, None)
1830 settings = settings.get(key, None)
1829 except Exception:
1831 except Exception:
1830 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1832 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1831 log.exception(msg)
1833 log.exception(msg)
1832 raise JSONRPCError(msg)
1834 raise JSONRPCError(msg)
1833
1835
1834 return settings
1836 return settings
1835
1837
1836
1838
1837 @jsonrpc_method()
1839 @jsonrpc_method()
1838 def set_repo_settings(request, apiuser, repoid, settings):
1840 def set_repo_settings(request, apiuser, repoid, settings):
1839 """
1841 """
1840 Update repository settings. Returns true on success.
1842 Update repository settings. Returns true on success.
1841
1843
1842 :param apiuser: This is filled automatically from the |authtoken|.
1844 :param apiuser: This is filled automatically from the |authtoken|.
1843 :type apiuser: AuthUser
1845 :type apiuser: AuthUser
1844 :param repoid: The repository name or repository id.
1846 :param repoid: The repository name or repository id.
1845 :type repoid: str or int
1847 :type repoid: str or int
1846 :param settings: The new settings for the repository.
1848 :param settings: The new settings for the repository.
1847 :type: settings: dict
1849 :type: settings: dict
1848
1850
1849 Example output:
1851 Example output:
1850
1852
1851 .. code-block:: bash
1853 .. code-block:: bash
1852
1854
1853 {
1855 {
1854 "error": null,
1856 "error": null,
1855 "id": 237,
1857 "id": 237,
1856 "result": true
1858 "result": true
1857 }
1859 }
1858 """
1860 """
1859 # Restrict access to this api method to admins only.
1861 # Restrict access to this api method to admins only.
1860 if not has_superadmin_permission(apiuser):
1862 if not has_superadmin_permission(apiuser):
1861 raise JSONRPCForbidden()
1863 raise JSONRPCForbidden()
1862
1864
1863 if type(settings) is not dict:
1865 if type(settings) is not dict:
1864 raise JSONRPCError('Settings have to be a JSON Object.')
1866 raise JSONRPCError('Settings have to be a JSON Object.')
1865
1867
1866 try:
1868 try:
1867 settings_model = VcsSettingsModel(repo=repoid)
1869 settings_model = VcsSettingsModel(repo=repoid)
1868
1870
1869 # Merge global, repo and incoming settings.
1871 # Merge global, repo and incoming settings.
1870 new_settings = settings_model.get_global_settings()
1872 new_settings = settings_model.get_global_settings()
1871 new_settings.update(settings_model.get_repo_settings())
1873 new_settings.update(settings_model.get_repo_settings())
1872 new_settings.update(settings)
1874 new_settings.update(settings)
1873
1875
1874 # Update the settings.
1876 # Update the settings.
1875 inherit_global_settings = new_settings.get(
1877 inherit_global_settings = new_settings.get(
1876 'inherit_global_settings', False)
1878 'inherit_global_settings', False)
1877 settings_model.create_or_update_repo_settings(
1879 settings_model.create_or_update_repo_settings(
1878 new_settings, inherit_global_settings=inherit_global_settings)
1880 new_settings, inherit_global_settings=inherit_global_settings)
1879 Session().commit()
1881 Session().commit()
1880 except Exception:
1882 except Exception:
1881 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1883 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1882 log.exception(msg)
1884 log.exception(msg)
1883 raise JSONRPCError(msg)
1885 raise JSONRPCError(msg)
1884
1886
1885 # Indicate success.
1887 # Indicate success.
1886 return True
1888 return True
@@ -1,464 +1,465 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs
35 from rhodecode.lib import diffs
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159 # get ranges of commit ids if preset
159 # get ranges of commit ids if preset
160 commit_range = commit_id_range.split('...')[:2]
160 commit_range = commit_id_range.split('...')[:2]
161 enable_comments = True
161 enable_comments = True
162 try:
162 try:
163 pre_load = ['affected_files', 'author', 'branch', 'date',
163 pre_load = ['affected_files', 'author', 'branch', 'date',
164 'message', 'parents']
164 'message', 'parents']
165
165
166 if len(commit_range) == 2:
166 if len(commit_range) == 2:
167 enable_comments = False
167 enable_comments = False
168 commits = c.rhodecode_repo.get_commits(
168 commits = c.rhodecode_repo.get_commits(
169 start_id=commit_range[0], end_id=commit_range[1],
169 start_id=commit_range[0], end_id=commit_range[1],
170 pre_load=pre_load)
170 pre_load=pre_load)
171 commits = list(commits)
171 commits = list(commits)
172 else:
172 else:
173 commits = [c.rhodecode_repo.get_commit(
173 commits = [c.rhodecode_repo.get_commit(
174 commit_id=commit_id_range, pre_load=pre_load)]
174 commit_id=commit_id_range, pre_load=pre_load)]
175
175
176 c.commit_ranges = commits
176 c.commit_ranges = commits
177 if not c.commit_ranges:
177 if not c.commit_ranges:
178 raise RepositoryError(
178 raise RepositoryError(
179 'The commit range returned an empty result')
179 'The commit range returned an empty result')
180 except CommitDoesNotExistError:
180 except CommitDoesNotExistError:
181 msg = _('No such commit exists for this repository')
181 msg = _('No such commit exists for this repository')
182 h.flash(msg, category='error')
182 h.flash(msg, category='error')
183 raise HTTPNotFound()
183 raise HTTPNotFound()
184 except Exception:
184 except Exception:
185 log.exception("General failure")
185 log.exception("General failure")
186 raise HTTPNotFound()
186 raise HTTPNotFound()
187
187
188 c.changes = OrderedDict()
188 c.changes = OrderedDict()
189 c.lines_added = 0
189 c.lines_added = 0
190 c.lines_deleted = 0
190 c.lines_deleted = 0
191
191
192 c.commit_statuses = ChangesetStatus.STATUSES
192 c.commit_statuses = ChangesetStatus.STATUSES
193 c.comments = []
193 c.comments = []
194 c.statuses = []
194 c.statuses = []
195 c.inline_comments = []
195 c.inline_comments = []
196 c.inline_cnt = 0
196 c.inline_cnt = 0
197 c.files = []
197 c.files = []
198
198
199 # Iterate over ranges (default commit view is always one commit)
199 # Iterate over ranges (default commit view is always one commit)
200 for commit in c.commit_ranges:
200 for commit in c.commit_ranges:
201 if method == 'show':
201 if method == 'show':
202 c.statuses.extend([ChangesetStatusModel().get_status(
202 c.statuses.extend([ChangesetStatusModel().get_status(
203 c.rhodecode_db_repo.repo_id, commit.raw_id)])
203 c.rhodecode_db_repo.repo_id, commit.raw_id)])
204
204
205 c.comments.extend(ChangesetCommentsModel().get_comments(
205 c.comments.extend(ChangesetCommentsModel().get_comments(
206 c.rhodecode_db_repo.repo_id,
206 c.rhodecode_db_repo.repo_id,
207 revision=commit.raw_id))
207 revision=commit.raw_id))
208
208
209 # comments from PR
209 # comments from PR
210 st = ChangesetStatusModel().get_statuses(
210 st = ChangesetStatusModel().get_statuses(
211 c.rhodecode_db_repo.repo_id, commit.raw_id,
211 c.rhodecode_db_repo.repo_id, commit.raw_id,
212 with_revisions=True)
212 with_revisions=True)
213
213
214 # from associated statuses, check the pull requests, and
214 # from associated statuses, check the pull requests, and
215 # show comments from them
215 # show comments from them
216
216
217 prs = set(x.pull_request for x in
217 prs = set(x.pull_request for x in
218 filter(lambda x: x.pull_request is not None, st))
218 filter(lambda x: x.pull_request is not None, st))
219 for pr in prs:
219 for pr in prs:
220 c.comments.extend(pr.comments)
220 c.comments.extend(pr.comments)
221
221
222 inlines = ChangesetCommentsModel().get_inline_comments(
222 inlines = ChangesetCommentsModel().get_inline_comments(
223 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
223 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
224 c.inline_comments.extend(inlines.iteritems())
224 c.inline_comments.extend(inlines.iteritems())
225
225
226 c.changes[commit.raw_id] = []
226 c.changes[commit.raw_id] = []
227
227
228 commit2 = commit
228 commit2 = commit
229 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
229 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
230
230
231 # fetch global flags of ignore ws or context lines
231 # fetch global flags of ignore ws or context lines
232 context_lcl = get_line_ctx('', request.GET)
232 context_lcl = get_line_ctx('', request.GET)
233 ign_whitespace_lcl = get_ignore_ws('', request.GET)
233 ign_whitespace_lcl = get_ignore_ws('', request.GET)
234
234
235 _diff = c.rhodecode_repo.get_diff(
235 _diff = c.rhodecode_repo.get_diff(
236 commit1, commit2,
236 commit1, commit2,
237 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
237 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238
238
239 # diff_limit will cut off the whole diff if the limit is applied
239 # diff_limit will cut off the whole diff if the limit is applied
240 # otherwise it will just hide the big files from the front-end
240 # otherwise it will just hide the big files from the front-end
241 diff_limit = self.cut_off_limit_diff
241 diff_limit = self.cut_off_limit_diff
242 file_limit = self.cut_off_limit_file
242 file_limit = self.cut_off_limit_file
243
243
244 diff_processor = diffs.DiffProcessor(
244 diff_processor = diffs.DiffProcessor(
245 _diff, format='gitdiff', diff_limit=diff_limit,
245 _diff, format='gitdiff', diff_limit=diff_limit,
246 file_limit=file_limit, show_full_diff=fulldiff)
246 file_limit=file_limit, show_full_diff=fulldiff)
247 commit_changes = OrderedDict()
247 commit_changes = OrderedDict()
248 if method == 'show':
248 if method == 'show':
249 _parsed = diff_processor.prepare()
249 _parsed = diff_processor.prepare()
250 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
250 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
251 for f in _parsed:
251 for f in _parsed:
252 c.files.append(f)
252 c.files.append(f)
253 st = f['stats']
253 st = f['stats']
254 c.lines_added += st['added']
254 c.lines_added += st['added']
255 c.lines_deleted += st['deleted']
255 c.lines_deleted += st['deleted']
256 fid = h.FID(commit.raw_id, f['filename'])
256 fid = h.FID(commit.raw_id, f['filename'])
257 diff = diff_processor.as_html(enable_comments=enable_comments,
257 diff = diff_processor.as_html(enable_comments=enable_comments,
258 parsed_lines=[f])
258 parsed_lines=[f])
259 commit_changes[fid] = [
259 commit_changes[fid] = [
260 commit1.raw_id, commit2.raw_id,
260 commit1.raw_id, commit2.raw_id,
261 f['operation'], f['filename'], diff, st, f]
261 f['operation'], f['filename'], diff, st, f]
262 else:
262 else:
263 # downloads/raw we only need RAW diff nothing else
263 # downloads/raw we only need RAW diff nothing else
264 diff = diff_processor.as_raw()
264 diff = diff_processor.as_raw()
265 commit_changes[''] = [None, None, None, None, diff, None, None]
265 commit_changes[''] = [None, None, None, None, diff, None, None]
266 c.changes[commit.raw_id] = commit_changes
266 c.changes[commit.raw_id] = commit_changes
267
267
268 # sort comments by how they were generated
268 # sort comments by how they were generated
269 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
269 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
270
270
271 # count inline comments
271 # count inline comments
272 for __, lines in c.inline_comments:
272 for __, lines in c.inline_comments:
273 for comments in lines.values():
273 for comments in lines.values():
274 c.inline_cnt += len(comments)
274 c.inline_cnt += len(comments)
275
275
276 if len(c.commit_ranges) == 1:
276 if len(c.commit_ranges) == 1:
277 c.commit = c.commit_ranges[0]
277 c.commit = c.commit_ranges[0]
278 c.parent_tmpl = ''.join(
278 c.parent_tmpl = ''.join(
279 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
279 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
280 if method == 'download':
280 if method == 'download':
281 response.content_type = 'text/plain'
281 response.content_type = 'text/plain'
282 response.content_disposition = (
282 response.content_disposition = (
283 'attachment; filename=%s.diff' % commit_id_range[:12])
283 'attachment; filename=%s.diff' % commit_id_range[:12])
284 return diff
284 return diff
285 elif method == 'patch':
285 elif method == 'patch':
286 response.content_type = 'text/plain'
286 response.content_type = 'text/plain'
287 c.diff = safe_unicode(diff)
287 c.diff = safe_unicode(diff)
288 return render('changeset/patch_changeset.html')
288 return render('changeset/patch_changeset.html')
289 elif method == 'raw':
289 elif method == 'raw':
290 response.content_type = 'text/plain'
290 response.content_type = 'text/plain'
291 return diff
291 return diff
292 elif method == 'show':
292 elif method == 'show':
293 if len(c.commit_ranges) == 1:
293 if len(c.commit_ranges) == 1:
294 return render('changeset/changeset.html')
294 return render('changeset/changeset.html')
295 else:
295 else:
296 c.ancestor = None
296 c.ancestor = None
297 c.target_repo = c.rhodecode_db_repo
297 c.target_repo = c.rhodecode_db_repo
298 return render('changeset/changeset_range.html')
298 return render('changeset/changeset_range.html')
299
299
300 @LoginRequired()
300 @LoginRequired()
301 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
301 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
302 'repository.admin')
302 'repository.admin')
303 def index(self, revision, method='show'):
303 def index(self, revision, method='show'):
304 return self._index(revision, method=method)
304 return self._index(revision, method=method)
305
305
306 @LoginRequired()
306 @LoginRequired()
307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
308 'repository.admin')
308 'repository.admin')
309 def changeset_raw(self, revision):
309 def changeset_raw(self, revision):
310 return self._index(revision, method='raw')
310 return self._index(revision, method='raw')
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
314 'repository.admin')
314 'repository.admin')
315 def changeset_patch(self, revision):
315 def changeset_patch(self, revision):
316 return self._index(revision, method='patch')
316 return self._index(revision, method='patch')
317
317
318 @LoginRequired()
318 @LoginRequired()
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
320 'repository.admin')
320 'repository.admin')
321 def changeset_download(self, revision):
321 def changeset_download(self, revision):
322 return self._index(revision, method='download')
322 return self._index(revision, method='download')
323
323
324 @LoginRequired()
324 @LoginRequired()
325 @NotAnonymous()
325 @NotAnonymous()
326 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
327 'repository.admin')
327 'repository.admin')
328 @auth.CSRFRequired()
328 @auth.CSRFRequired()
329 @jsonify
329 @jsonify
330 def comment(self, repo_name, revision):
330 def comment(self, repo_name, revision):
331 commit_id = revision
331 commit_id = revision
332 status = request.POST.get('changeset_status', None)
332 status = request.POST.get('changeset_status', None)
333 text = request.POST.get('text')
333 text = request.POST.get('text')
334 if status:
334 if status:
335 text = text or (_('Status change %(transition_icon)s %(status)s')
335 text = text or (_('Status change %(transition_icon)s %(status)s')
336 % {'transition_icon': '>',
336 % {'transition_icon': '>',
337 'status': ChangesetStatus.get_status_lbl(status)})
337 'status': ChangesetStatus.get_status_lbl(status)})
338
338
339 multi_commit_ids = filter(
339 multi_commit_ids = filter(
340 lambda s: s not in ['', None],
340 lambda s: s not in ['', None],
341 request.POST.get('commit_ids', '').split(','),)
341 request.POST.get('commit_ids', '').split(','),)
342
342
343 commit_ids = multi_commit_ids or [commit_id]
343 commit_ids = multi_commit_ids or [commit_id]
344 comment = None
344 comment = None
345 for current_id in filter(None, commit_ids):
345 for current_id in filter(None, commit_ids):
346 c.co = comment = ChangesetCommentsModel().create(
346 c.co = comment = ChangesetCommentsModel().create(
347 text=text,
347 text=text,
348 repo=c.rhodecode_db_repo.repo_id,
348 repo=c.rhodecode_db_repo.repo_id,
349 user=c.rhodecode_user.user_id,
349 user=c.rhodecode_user.user_id,
350 revision=current_id,
350 revision=current_id,
351 f_path=request.POST.get('f_path'),
351 f_path=request.POST.get('f_path'),
352 line_no=request.POST.get('line'),
352 line_no=request.POST.get('line'),
353 status_change=(ChangesetStatus.get_status_lbl(status)
353 status_change=(ChangesetStatus.get_status_lbl(status)
354 if status else None)
354 if status else None),
355 status_change_type=status
355 )
356 )
356 # get status if set !
357 # get status if set !
357 if status:
358 if status:
358 # if latest status was from pull request and it's closed
359 # if latest status was from pull request and it's closed
359 # disallow changing status !
360 # disallow changing status !
360 # dont_allow_on_closed_pull_request = True !
361 # dont_allow_on_closed_pull_request = True !
361
362
362 try:
363 try:
363 ChangesetStatusModel().set_status(
364 ChangesetStatusModel().set_status(
364 c.rhodecode_db_repo.repo_id,
365 c.rhodecode_db_repo.repo_id,
365 status,
366 status,
366 c.rhodecode_user.user_id,
367 c.rhodecode_user.user_id,
367 comment,
368 comment,
368 revision=current_id,
369 revision=current_id,
369 dont_allow_on_closed_pull_request=True
370 dont_allow_on_closed_pull_request=True
370 )
371 )
371 except StatusChangeOnClosedPullRequestError:
372 except StatusChangeOnClosedPullRequestError:
372 msg = _('Changing the status of a commit associated with '
373 msg = _('Changing the status of a commit associated with '
373 'a closed pull request is not allowed')
374 'a closed pull request is not allowed')
374 log.exception(msg)
375 log.exception(msg)
375 h.flash(msg, category='warning')
376 h.flash(msg, category='warning')
376 return redirect(h.url(
377 return redirect(h.url(
377 'changeset_home', repo_name=repo_name,
378 'changeset_home', repo_name=repo_name,
378 revision=current_id))
379 revision=current_id))
379
380
380 # finalize, commit and redirect
381 # finalize, commit and redirect
381 Session().commit()
382 Session().commit()
382
383
383 data = {
384 data = {
384 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
385 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
385 }
386 }
386 if comment:
387 if comment:
387 data.update(comment.get_dict())
388 data.update(comment.get_dict())
388 data.update({'rendered_text':
389 data.update({'rendered_text':
389 render('changeset/changeset_comment_block.html')})
390 render('changeset/changeset_comment_block.html')})
390
391
391 return data
392 return data
392
393
393 @LoginRequired()
394 @LoginRequired()
394 @NotAnonymous()
395 @NotAnonymous()
395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 'repository.admin')
397 'repository.admin')
397 @auth.CSRFRequired()
398 @auth.CSRFRequired()
398 def preview_comment(self):
399 def preview_comment(self):
399 # Technically a CSRF token is not needed as no state changes with this
400 # Technically a CSRF token is not needed as no state changes with this
400 # call. However, as this is a POST is better to have it, so automated
401 # call. However, as this is a POST is better to have it, so automated
401 # tools don't flag it as potential CSRF.
402 # tools don't flag it as potential CSRF.
402 # Post is required because the payload could be bigger than the maximum
403 # Post is required because the payload could be bigger than the maximum
403 # allowed by GET.
404 # allowed by GET.
404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 raise HTTPBadRequest()
406 raise HTTPBadRequest()
406 text = request.POST.get('text')
407 text = request.POST.get('text')
407 renderer = request.POST.get('renderer') or 'rst'
408 renderer = request.POST.get('renderer') or 'rst'
408 if text:
409 if text:
409 return h.render(text, renderer=renderer, mentions=True)
410 return h.render(text, renderer=renderer, mentions=True)
410 return ''
411 return ''
411
412
412 @LoginRequired()
413 @LoginRequired()
413 @NotAnonymous()
414 @NotAnonymous()
414 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
415 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
415 'repository.admin')
416 'repository.admin')
416 @auth.CSRFRequired()
417 @auth.CSRFRequired()
417 @jsonify
418 @jsonify
418 def delete_comment(self, repo_name, comment_id):
419 def delete_comment(self, repo_name, comment_id):
419 comment = ChangesetComment.get(comment_id)
420 comment = ChangesetComment.get(comment_id)
420 owner = (comment.author.user_id == c.rhodecode_user.user_id)
421 owner = (comment.author.user_id == c.rhodecode_user.user_id)
421 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
422 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
422 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
423 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
423 ChangesetCommentsModel().delete(comment=comment)
424 ChangesetCommentsModel().delete(comment=comment)
424 Session().commit()
425 Session().commit()
425 return True
426 return True
426 else:
427 else:
427 raise HTTPForbidden()
428 raise HTTPForbidden()
428
429
429 @LoginRequired()
430 @LoginRequired()
430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 'repository.admin')
432 'repository.admin')
432 @jsonify
433 @jsonify
433 def changeset_info(self, repo_name, revision):
434 def changeset_info(self, repo_name, revision):
434 if request.is_xhr:
435 if request.is_xhr:
435 try:
436 try:
436 return c.rhodecode_repo.get_commit(commit_id=revision)
437 return c.rhodecode_repo.get_commit(commit_id=revision)
437 except CommitDoesNotExistError as e:
438 except CommitDoesNotExistError as e:
438 return EmptyCommit(message=str(e))
439 return EmptyCommit(message=str(e))
439 else:
440 else:
440 raise HTTPBadRequest()
441 raise HTTPBadRequest()
441
442
442 @LoginRequired()
443 @LoginRequired()
443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 'repository.admin')
445 'repository.admin')
445 @jsonify
446 @jsonify
446 def changeset_children(self, repo_name, revision):
447 def changeset_children(self, repo_name, revision):
447 if request.is_xhr:
448 if request.is_xhr:
448 commit = c.rhodecode_repo.get_commit(commit_id=revision)
449 commit = c.rhodecode_repo.get_commit(commit_id=revision)
449 result = {"results": commit.children}
450 result = {"results": commit.children}
450 return result
451 return result
451 else:
452 else:
452 raise HTTPBadRequest()
453 raise HTTPBadRequest()
453
454
454 @LoginRequired()
455 @LoginRequired()
455 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 'repository.admin')
457 'repository.admin')
457 @jsonify
458 @jsonify
458 def changeset_parents(self, repo_name, revision):
459 def changeset_parents(self, repo_name, revision):
459 if request.is_xhr:
460 if request.is_xhr:
460 commit = c.rhodecode_repo.get_commit(commit_id=revision)
461 commit = c.rhodecode_repo.get_commit(commit_id=revision)
461 result = {"results": commit.parents}
462 result = {"results": commit.parents}
462 return result
463 return result
463 else:
464 else:
464 raise HTTPBadRequest()
465 raise HTTPBadRequest()
@@ -1,853 +1,855 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24
24
25 import formencode
25 import formencode
26 import logging
26 import logging
27
27
28 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
28 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from pylons import request, tmpl_context as c, url
29 from pylons import request, tmpl_context as c, url
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from sqlalchemy.sql import func
32 from sqlalchemy.sql import func
33 from sqlalchemy.sql.expression import or_
33 from sqlalchemy.sql.expression import or_
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.lib import auth, diffs, helpers as h
36 from rhodecode.lib import auth, diffs, helpers as h
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.base import (
38 from rhodecode.lib.base import (
39 BaseRepoController, render, vcs_operation_context)
39 BaseRepoController, render, vcs_operation_context)
40 from rhodecode.lib.auth import (
40 from rhodecode.lib.auth import (
41 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
41 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
42 HasAcceptedRepoType, XHRRequired)
42 HasAcceptedRepoType, XHRRequired)
43 from rhodecode.lib.utils import jsonify
43 from rhodecode.lib.utils import jsonify
44 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
44 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
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 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
47 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
48 from rhodecode.lib.diffs import LimitedDiffContainer
48 from rhodecode.lib.diffs import LimitedDiffContainer
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.comment import ChangesetCommentsModel
51 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
51 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
52 Repository
52 Repository
53 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.forms import PullRequestForm
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.pull_request import PullRequestModel
55 from rhodecode.model.pull_request import PullRequestModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class PullrequestsController(BaseRepoController):
60 class PullrequestsController(BaseRepoController):
61 def __before__(self):
61 def __before__(self):
62 super(PullrequestsController, self).__before__()
62 super(PullrequestsController, self).__before__()
63
63
64 def _load_compare_data(self, pull_request, enable_comments=True):
64 def _load_compare_data(self, pull_request, enable_comments=True):
65 """
65 """
66 Load context data needed for generating compare diff
66 Load context data needed for generating compare diff
67
67
68 :param pull_request: object related to the request
68 :param pull_request: object related to the request
69 :param enable_comments: flag to determine if comments are included
69 :param enable_comments: flag to determine if comments are included
70 """
70 """
71 source_repo = pull_request.source_repo
71 source_repo = pull_request.source_repo
72 source_ref_id = pull_request.source_ref_parts.commit_id
72 source_ref_id = pull_request.source_ref_parts.commit_id
73
73
74 target_repo = pull_request.target_repo
74 target_repo = pull_request.target_repo
75 target_ref_id = pull_request.target_ref_parts.commit_id
75 target_ref_id = pull_request.target_ref_parts.commit_id
76
76
77 # despite opening commits for bookmarks/branches/tags, we always
77 # despite opening commits for bookmarks/branches/tags, we always
78 # convert this to rev to prevent changes after bookmark or branch change
78 # convert this to rev to prevent changes after bookmark or branch change
79 c.source_ref_type = 'rev'
79 c.source_ref_type = 'rev'
80 c.source_ref = source_ref_id
80 c.source_ref = source_ref_id
81
81
82 c.target_ref_type = 'rev'
82 c.target_ref_type = 'rev'
83 c.target_ref = target_ref_id
83 c.target_ref = target_ref_id
84
84
85 c.source_repo = source_repo
85 c.source_repo = source_repo
86 c.target_repo = target_repo
86 c.target_repo = target_repo
87
87
88 c.fulldiff = bool(request.GET.get('fulldiff'))
88 c.fulldiff = bool(request.GET.get('fulldiff'))
89
89
90 # diff_limit is the old behavior, will cut off the whole diff
90 # diff_limit is the old behavior, will cut off the whole diff
91 # if the limit is applied otherwise will just hide the
91 # if the limit is applied otherwise will just hide the
92 # big files from the front-end
92 # big files from the front-end
93 diff_limit = self.cut_off_limit_diff
93 diff_limit = self.cut_off_limit_diff
94 file_limit = self.cut_off_limit_file
94 file_limit = self.cut_off_limit_file
95
95
96 pre_load = ["author", "branch", "date", "message"]
96 pre_load = ["author", "branch", "date", "message"]
97
97
98 c.commit_ranges = []
98 c.commit_ranges = []
99 source_commit = EmptyCommit()
99 source_commit = EmptyCommit()
100 target_commit = EmptyCommit()
100 target_commit = EmptyCommit()
101 c.missing_requirements = False
101 c.missing_requirements = False
102 try:
102 try:
103 c.commit_ranges = [
103 c.commit_ranges = [
104 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
104 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
105 for rev in pull_request.revisions]
105 for rev in pull_request.revisions]
106
106
107 c.statuses = source_repo.statuses(
107 c.statuses = source_repo.statuses(
108 [x.raw_id for x in c.commit_ranges])
108 [x.raw_id for x in c.commit_ranges])
109
109
110 target_commit = source_repo.get_commit(
110 target_commit = source_repo.get_commit(
111 commit_id=safe_str(target_ref_id))
111 commit_id=safe_str(target_ref_id))
112 source_commit = source_repo.get_commit(
112 source_commit = source_repo.get_commit(
113 commit_id=safe_str(source_ref_id))
113 commit_id=safe_str(source_ref_id))
114 except RepositoryRequirementError:
114 except RepositoryRequirementError:
115 c.missing_requirements = True
115 c.missing_requirements = True
116
116
117 c.missing_commits = False
117 c.missing_commits = False
118 if (c.missing_requirements or
118 if (c.missing_requirements or
119 isinstance(source_commit, EmptyCommit) or
119 isinstance(source_commit, EmptyCommit) or
120 source_commit == target_commit):
120 source_commit == target_commit):
121 _parsed = []
121 _parsed = []
122 c.missing_commits = True
122 c.missing_commits = True
123 else:
123 else:
124 vcs_diff = PullRequestModel().get_diff(pull_request)
124 vcs_diff = PullRequestModel().get_diff(pull_request)
125 diff_processor = diffs.DiffProcessor(
125 diff_processor = diffs.DiffProcessor(
126 vcs_diff, format='gitdiff', diff_limit=diff_limit,
126 vcs_diff, format='gitdiff', diff_limit=diff_limit,
127 file_limit=file_limit, show_full_diff=c.fulldiff)
127 file_limit=file_limit, show_full_diff=c.fulldiff)
128 _parsed = diff_processor.prepare()
128 _parsed = diff_processor.prepare()
129
129
130 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
130 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
131
131
132 c.files = []
132 c.files = []
133 c.changes = {}
133 c.changes = {}
134 c.lines_added = 0
134 c.lines_added = 0
135 c.lines_deleted = 0
135 c.lines_deleted = 0
136 c.included_files = []
136 c.included_files = []
137 c.deleted_files = []
137 c.deleted_files = []
138
138
139 for f in _parsed:
139 for f in _parsed:
140 st = f['stats']
140 st = f['stats']
141 c.lines_added += st['added']
141 c.lines_added += st['added']
142 c.lines_deleted += st['deleted']
142 c.lines_deleted += st['deleted']
143
143
144 fid = h.FID('', f['filename'])
144 fid = h.FID('', f['filename'])
145 c.files.append([fid, f['operation'], f['filename'], f['stats']])
145 c.files.append([fid, f['operation'], f['filename'], f['stats']])
146 c.included_files.append(f['filename'])
146 c.included_files.append(f['filename'])
147 html_diff = diff_processor.as_html(enable_comments=enable_comments,
147 html_diff = diff_processor.as_html(enable_comments=enable_comments,
148 parsed_lines=[f])
148 parsed_lines=[f])
149 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
149 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
150
150
151 def _extract_ordering(self, request):
151 def _extract_ordering(self, request):
152 column_index = safe_int(request.GET.get('order[0][column]'))
152 column_index = safe_int(request.GET.get('order[0][column]'))
153 order_dir = request.GET.get('order[0][dir]', 'desc')
153 order_dir = request.GET.get('order[0][dir]', 'desc')
154 order_by = request.GET.get(
154 order_by = request.GET.get(
155 'columns[%s][data][sort]' % column_index, 'name_raw')
155 'columns[%s][data][sort]' % column_index, 'name_raw')
156 return order_by, order_dir
156 return order_by, order_dir
157
157
158 @LoginRequired()
158 @LoginRequired()
159 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
159 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
160 'repository.admin')
160 'repository.admin')
161 @HasAcceptedRepoType('git', 'hg')
161 @HasAcceptedRepoType('git', 'hg')
162 def show_all(self, repo_name):
162 def show_all(self, repo_name):
163 # filter types
163 # filter types
164 c.active = 'open'
164 c.active = 'open'
165 c.source = str2bool(request.GET.get('source'))
165 c.source = str2bool(request.GET.get('source'))
166 c.closed = str2bool(request.GET.get('closed'))
166 c.closed = str2bool(request.GET.get('closed'))
167 c.my = str2bool(request.GET.get('my'))
167 c.my = str2bool(request.GET.get('my'))
168 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
168 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
169 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
169 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
170 c.repo_name = repo_name
170 c.repo_name = repo_name
171
171
172 opened_by = None
172 opened_by = None
173 if c.my:
173 if c.my:
174 c.active = 'my'
174 c.active = 'my'
175 opened_by = [c.rhodecode_user.user_id]
175 opened_by = [c.rhodecode_user.user_id]
176
176
177 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
177 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
178 if c.closed:
178 if c.closed:
179 c.active = 'closed'
179 c.active = 'closed'
180 statuses = [PullRequest.STATUS_CLOSED]
180 statuses = [PullRequest.STATUS_CLOSED]
181
181
182 if c.awaiting_review and not c.source:
182 if c.awaiting_review and not c.source:
183 c.active = 'awaiting'
183 c.active = 'awaiting'
184 if c.source and not c.awaiting_review:
184 if c.source and not c.awaiting_review:
185 c.active = 'source'
185 c.active = 'source'
186 if c.awaiting_my_review:
186 if c.awaiting_my_review:
187 c.active = 'awaiting_my'
187 c.active = 'awaiting_my'
188
188
189 data = self._get_pull_requests_list(
189 data = self._get_pull_requests_list(
190 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
190 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
191 if not request.is_xhr:
191 if not request.is_xhr:
192 c.data = json.dumps(data['data'])
192 c.data = json.dumps(data['data'])
193 c.records_total = data['recordsTotal']
193 c.records_total = data['recordsTotal']
194 return render('/pullrequests/pullrequests.html')
194 return render('/pullrequests/pullrequests.html')
195 else:
195 else:
196 return json.dumps(data)
196 return json.dumps(data)
197
197
198 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
198 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
199 # pagination
199 # pagination
200 start = safe_int(request.GET.get('start'), 0)
200 start = safe_int(request.GET.get('start'), 0)
201 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
201 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
202 order_by, order_dir = self._extract_ordering(request)
202 order_by, order_dir = self._extract_ordering(request)
203
203
204 if c.awaiting_review:
204 if c.awaiting_review:
205 pull_requests = PullRequestModel().get_awaiting_review(
205 pull_requests = PullRequestModel().get_awaiting_review(
206 repo_name, source=c.source, opened_by=opened_by,
206 repo_name, source=c.source, opened_by=opened_by,
207 statuses=statuses, offset=start, length=length,
207 statuses=statuses, offset=start, length=length,
208 order_by=order_by, order_dir=order_dir)
208 order_by=order_by, order_dir=order_dir)
209 pull_requests_total_count = PullRequestModel(
209 pull_requests_total_count = PullRequestModel(
210 ).count_awaiting_review(
210 ).count_awaiting_review(
211 repo_name, source=c.source, statuses=statuses,
211 repo_name, source=c.source, statuses=statuses,
212 opened_by=opened_by)
212 opened_by=opened_by)
213 elif c.awaiting_my_review:
213 elif c.awaiting_my_review:
214 pull_requests = PullRequestModel().get_awaiting_my_review(
214 pull_requests = PullRequestModel().get_awaiting_my_review(
215 repo_name, source=c.source, opened_by=opened_by,
215 repo_name, source=c.source, opened_by=opened_by,
216 user_id=c.rhodecode_user.user_id, statuses=statuses,
216 user_id=c.rhodecode_user.user_id, statuses=statuses,
217 offset=start, length=length, order_by=order_by,
217 offset=start, length=length, order_by=order_by,
218 order_dir=order_dir)
218 order_dir=order_dir)
219 pull_requests_total_count = PullRequestModel(
219 pull_requests_total_count = PullRequestModel(
220 ).count_awaiting_my_review(
220 ).count_awaiting_my_review(
221 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
221 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
222 statuses=statuses, opened_by=opened_by)
222 statuses=statuses, opened_by=opened_by)
223 else:
223 else:
224 pull_requests = PullRequestModel().get_all(
224 pull_requests = PullRequestModel().get_all(
225 repo_name, source=c.source, opened_by=opened_by,
225 repo_name, source=c.source, opened_by=opened_by,
226 statuses=statuses, offset=start, length=length,
226 statuses=statuses, offset=start, length=length,
227 order_by=order_by, order_dir=order_dir)
227 order_by=order_by, order_dir=order_dir)
228 pull_requests_total_count = PullRequestModel().count_all(
228 pull_requests_total_count = PullRequestModel().count_all(
229 repo_name, source=c.source, statuses=statuses,
229 repo_name, source=c.source, statuses=statuses,
230 opened_by=opened_by)
230 opened_by=opened_by)
231
231
232 from rhodecode.lib.utils import PartialRenderer
232 from rhodecode.lib.utils import PartialRenderer
233 _render = PartialRenderer('data_table/_dt_elements.html')
233 _render = PartialRenderer('data_table/_dt_elements.html')
234 data = []
234 data = []
235 for pr in pull_requests:
235 for pr in pull_requests:
236 comments = ChangesetCommentsModel().get_all_comments(
236 comments = ChangesetCommentsModel().get_all_comments(
237 c.rhodecode_db_repo.repo_id, pull_request=pr)
237 c.rhodecode_db_repo.repo_id, pull_request=pr)
238
238
239 data.append({
239 data.append({
240 'name': _render('pullrequest_name',
240 'name': _render('pullrequest_name',
241 pr.pull_request_id, pr.target_repo.repo_name),
241 pr.pull_request_id, pr.target_repo.repo_name),
242 'name_raw': pr.pull_request_id,
242 'name_raw': pr.pull_request_id,
243 'status': _render('pullrequest_status',
243 'status': _render('pullrequest_status',
244 pr.calculated_review_status()),
244 pr.calculated_review_status()),
245 'title': _render(
245 'title': _render(
246 'pullrequest_title', pr.title, pr.description),
246 'pullrequest_title', pr.title, pr.description),
247 'description': h.escape(pr.description),
247 'description': h.escape(pr.description),
248 'updated_on': _render('pullrequest_updated_on',
248 'updated_on': _render('pullrequest_updated_on',
249 h.datetime_to_time(pr.updated_on)),
249 h.datetime_to_time(pr.updated_on)),
250 'updated_on_raw': h.datetime_to_time(pr.updated_on),
250 'updated_on_raw': h.datetime_to_time(pr.updated_on),
251 'created_on': _render('pullrequest_updated_on',
251 'created_on': _render('pullrequest_updated_on',
252 h.datetime_to_time(pr.created_on)),
252 h.datetime_to_time(pr.created_on)),
253 'created_on_raw': h.datetime_to_time(pr.created_on),
253 'created_on_raw': h.datetime_to_time(pr.created_on),
254 'author': _render('pullrequest_author',
254 'author': _render('pullrequest_author',
255 pr.author.full_contact, ),
255 pr.author.full_contact, ),
256 'author_raw': pr.author.full_name,
256 'author_raw': pr.author.full_name,
257 'comments': _render('pullrequest_comments', len(comments)),
257 'comments': _render('pullrequest_comments', len(comments)),
258 'comments_raw': len(comments),
258 'comments_raw': len(comments),
259 'closed': pr.is_closed(),
259 'closed': pr.is_closed(),
260 })
260 })
261 # json used to render the grid
261 # json used to render the grid
262 data = ({
262 data = ({
263 'data': data,
263 'data': data,
264 'recordsTotal': pull_requests_total_count,
264 'recordsTotal': pull_requests_total_count,
265 'recordsFiltered': pull_requests_total_count,
265 'recordsFiltered': pull_requests_total_count,
266 })
266 })
267 return data
267 return data
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @NotAnonymous()
270 @NotAnonymous()
271 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
271 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
272 'repository.admin')
272 'repository.admin')
273 @HasAcceptedRepoType('git', 'hg')
273 @HasAcceptedRepoType('git', 'hg')
274 def index(self):
274 def index(self):
275 source_repo = c.rhodecode_db_repo
275 source_repo = c.rhodecode_db_repo
276
276
277 try:
277 try:
278 source_repo.scm_instance().get_commit()
278 source_repo.scm_instance().get_commit()
279 except EmptyRepositoryError:
279 except EmptyRepositoryError:
280 h.flash(h.literal(_('There are no commits yet')),
280 h.flash(h.literal(_('There are no commits yet')),
281 category='warning')
281 category='warning')
282 redirect(url('summary_home', repo_name=source_repo.repo_name))
282 redirect(url('summary_home', repo_name=source_repo.repo_name))
283
283
284 commit_id = request.GET.get('commit')
284 commit_id = request.GET.get('commit')
285 branch_ref = request.GET.get('branch')
285 branch_ref = request.GET.get('branch')
286 bookmark_ref = request.GET.get('bookmark')
286 bookmark_ref = request.GET.get('bookmark')
287
287
288 try:
288 try:
289 source_repo_data = PullRequestModel().generate_repo_data(
289 source_repo_data = PullRequestModel().generate_repo_data(
290 source_repo, commit_id=commit_id,
290 source_repo, commit_id=commit_id,
291 branch=branch_ref, bookmark=bookmark_ref)
291 branch=branch_ref, bookmark=bookmark_ref)
292 except CommitDoesNotExistError as e:
292 except CommitDoesNotExistError as e:
293 log.exception(e)
293 log.exception(e)
294 h.flash(_('Commit does not exist'), 'error')
294 h.flash(_('Commit does not exist'), 'error')
295 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
295 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
296
296
297 default_target_repo = source_repo
297 default_target_repo = source_repo
298 if (source_repo.parent and
298 if (source_repo.parent and
299 not source_repo.parent.scm_instance().is_empty()):
299 not source_repo.parent.scm_instance().is_empty()):
300 # change default if we have a parent repo
300 # change default if we have a parent repo
301 default_target_repo = source_repo.parent
301 default_target_repo = source_repo.parent
302
302
303 target_repo_data = PullRequestModel().generate_repo_data(
303 target_repo_data = PullRequestModel().generate_repo_data(
304 default_target_repo)
304 default_target_repo)
305
305
306 selected_source_ref = source_repo_data['refs']['selected_ref']
306 selected_source_ref = source_repo_data['refs']['selected_ref']
307
307
308 title_source_ref = selected_source_ref.split(':', 2)[1]
308 title_source_ref = selected_source_ref.split(':', 2)[1]
309 c.default_title = PullRequestModel().generate_pullrequest_title(
309 c.default_title = PullRequestModel().generate_pullrequest_title(
310 source=source_repo.repo_name,
310 source=source_repo.repo_name,
311 source_ref=title_source_ref,
311 source_ref=title_source_ref,
312 target=default_target_repo.repo_name
312 target=default_target_repo.repo_name
313 )
313 )
314
314
315 c.default_repo_data = {
315 c.default_repo_data = {
316 'source_repo_name': source_repo.repo_name,
316 'source_repo_name': source_repo.repo_name,
317 'source_refs_json': json.dumps(source_repo_data),
317 'source_refs_json': json.dumps(source_repo_data),
318 'target_repo_name': default_target_repo.repo_name,
318 'target_repo_name': default_target_repo.repo_name,
319 'target_refs_json': json.dumps(target_repo_data),
319 'target_refs_json': json.dumps(target_repo_data),
320 }
320 }
321 c.default_source_ref = selected_source_ref
321 c.default_source_ref = selected_source_ref
322
322
323 return render('/pullrequests/pullrequest.html')
323 return render('/pullrequests/pullrequest.html')
324
324
325 @LoginRequired()
325 @LoginRequired()
326 @NotAnonymous()
326 @NotAnonymous()
327 @XHRRequired()
327 @XHRRequired()
328 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
328 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 'repository.admin')
329 'repository.admin')
330 @jsonify
330 @jsonify
331 def get_repo_refs(self, repo_name, target_repo_name):
331 def get_repo_refs(self, repo_name, target_repo_name):
332 repo = Repository.get_by_repo_name(target_repo_name)
332 repo = Repository.get_by_repo_name(target_repo_name)
333 if not repo:
333 if not repo:
334 raise HTTPNotFound
334 raise HTTPNotFound
335 return PullRequestModel().generate_repo_data(repo)
335 return PullRequestModel().generate_repo_data(repo)
336
336
337 @LoginRequired()
337 @LoginRequired()
338 @NotAnonymous()
338 @NotAnonymous()
339 @XHRRequired()
339 @XHRRequired()
340 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
341 'repository.admin')
341 'repository.admin')
342 @jsonify
342 @jsonify
343 def get_repo_destinations(self, repo_name):
343 def get_repo_destinations(self, repo_name):
344 repo = Repository.get_by_repo_name(repo_name)
344 repo = Repository.get_by_repo_name(repo_name)
345 if not repo:
345 if not repo:
346 raise HTTPNotFound
346 raise HTTPNotFound
347 filter_query = request.GET.get('query')
347 filter_query = request.GET.get('query')
348
348
349 query = Repository.query() \
349 query = Repository.query() \
350 .order_by(func.length(Repository.repo_name)) \
350 .order_by(func.length(Repository.repo_name)) \
351 .filter(or_(
351 .filter(or_(
352 Repository.repo_name == repo.repo_name,
352 Repository.repo_name == repo.repo_name,
353 Repository.fork_id == repo.repo_id))
353 Repository.fork_id == repo.repo_id))
354
354
355 if filter_query:
355 if filter_query:
356 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
356 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
357 query = query.filter(
357 query = query.filter(
358 Repository.repo_name.ilike(ilike_expression))
358 Repository.repo_name.ilike(ilike_expression))
359
359
360 add_parent = False
360 add_parent = False
361 if repo.parent:
361 if repo.parent:
362 if filter_query in repo.parent.repo_name:
362 if filter_query in repo.parent.repo_name:
363 if not repo.parent.scm_instance().is_empty():
363 if not repo.parent.scm_instance().is_empty():
364 add_parent = True
364 add_parent = True
365
365
366 limit = 20 - 1 if add_parent else 20
366 limit = 20 - 1 if add_parent else 20
367 all_repos = query.limit(limit).all()
367 all_repos = query.limit(limit).all()
368 if add_parent:
368 if add_parent:
369 all_repos += [repo.parent]
369 all_repos += [repo.parent]
370
370
371 repos = []
371 repos = []
372 for obj in self.scm_model.get_repos(all_repos):
372 for obj in self.scm_model.get_repos(all_repos):
373 repos.append({
373 repos.append({
374 'id': obj['name'],
374 'id': obj['name'],
375 'text': obj['name'],
375 'text': obj['name'],
376 'type': 'repo',
376 'type': 'repo',
377 'obj': obj['dbrepo']
377 'obj': obj['dbrepo']
378 })
378 })
379
379
380 data = {
380 data = {
381 'more': False,
381 'more': False,
382 'results': [{
382 'results': [{
383 'text': _('Repositories'),
383 'text': _('Repositories'),
384 'children': repos
384 'children': repos
385 }] if repos else []
385 }] if repos else []
386 }
386 }
387 return data
387 return data
388
388
389 @LoginRequired()
389 @LoginRequired()
390 @NotAnonymous()
390 @NotAnonymous()
391 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
391 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
392 'repository.admin')
392 'repository.admin')
393 @HasAcceptedRepoType('git', 'hg')
393 @HasAcceptedRepoType('git', 'hg')
394 @auth.CSRFRequired()
394 @auth.CSRFRequired()
395 def create(self, repo_name):
395 def create(self, repo_name):
396 repo = Repository.get_by_repo_name(repo_name)
396 repo = Repository.get_by_repo_name(repo_name)
397 if not repo:
397 if not repo:
398 raise HTTPNotFound
398 raise HTTPNotFound
399
399
400 try:
400 try:
401 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
401 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
402 except formencode.Invalid as errors:
402 except formencode.Invalid as errors:
403 if errors.error_dict.get('revisions'):
403 if errors.error_dict.get('revisions'):
404 msg = 'Revisions: %s' % errors.error_dict['revisions']
404 msg = 'Revisions: %s' % errors.error_dict['revisions']
405 elif errors.error_dict.get('pullrequest_title'):
405 elif errors.error_dict.get('pullrequest_title'):
406 msg = _('Pull request requires a title with min. 3 chars')
406 msg = _('Pull request requires a title with min. 3 chars')
407 else:
407 else:
408 msg = _('Error creating pull request: {}').format(errors)
408 msg = _('Error creating pull request: {}').format(errors)
409 log.exception(msg)
409 log.exception(msg)
410 h.flash(msg, 'error')
410 h.flash(msg, 'error')
411
411
412 # would rather just go back to form ...
412 # would rather just go back to form ...
413 return redirect(url('pullrequest_home', repo_name=repo_name))
413 return redirect(url('pullrequest_home', repo_name=repo_name))
414
414
415 source_repo = _form['source_repo']
415 source_repo = _form['source_repo']
416 source_ref = _form['source_ref']
416 source_ref = _form['source_ref']
417 target_repo = _form['target_repo']
417 target_repo = _form['target_repo']
418 target_ref = _form['target_ref']
418 target_ref = _form['target_ref']
419 commit_ids = _form['revisions'][::-1]
419 commit_ids = _form['revisions'][::-1]
420 reviewers = _form['review_members']
420 reviewers = _form['review_members']
421
421
422 # find the ancestor for this pr
422 # find the ancestor for this pr
423 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
423 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
424 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
424 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
425
425
426 source_scm = source_db_repo.scm_instance()
426 source_scm = source_db_repo.scm_instance()
427 target_scm = target_db_repo.scm_instance()
427 target_scm = target_db_repo.scm_instance()
428
428
429 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
429 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
430 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
430 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
431
431
432 ancestor = source_scm.get_common_ancestor(
432 ancestor = source_scm.get_common_ancestor(
433 source_commit.raw_id, target_commit.raw_id, target_scm)
433 source_commit.raw_id, target_commit.raw_id, target_scm)
434
434
435 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
435 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
436 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
436 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
437
437
438 pullrequest_title = _form['pullrequest_title']
438 pullrequest_title = _form['pullrequest_title']
439 title_source_ref = source_ref.split(':', 2)[1]
439 title_source_ref = source_ref.split(':', 2)[1]
440 if not pullrequest_title:
440 if not pullrequest_title:
441 pullrequest_title = PullRequestModel().generate_pullrequest_title(
441 pullrequest_title = PullRequestModel().generate_pullrequest_title(
442 source=source_repo,
442 source=source_repo,
443 source_ref=title_source_ref,
443 source_ref=title_source_ref,
444 target=target_repo
444 target=target_repo
445 )
445 )
446
446
447 description = _form['pullrequest_desc']
447 description = _form['pullrequest_desc']
448 try:
448 try:
449 pull_request = PullRequestModel().create(
449 pull_request = PullRequestModel().create(
450 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
450 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
451 target_ref, commit_ids, reviewers, pullrequest_title,
451 target_ref, commit_ids, reviewers, pullrequest_title,
452 description
452 description
453 )
453 )
454 Session().commit()
454 Session().commit()
455 h.flash(_('Successfully opened new pull request'),
455 h.flash(_('Successfully opened new pull request'),
456 category='success')
456 category='success')
457 except Exception as e:
457 except Exception as e:
458 msg = _('Error occurred during sending pull request')
458 msg = _('Error occurred during sending pull request')
459 log.exception(msg)
459 log.exception(msg)
460 h.flash(msg, category='error')
460 h.flash(msg, category='error')
461 return redirect(url('pullrequest_home', repo_name=repo_name))
461 return redirect(url('pullrequest_home', repo_name=repo_name))
462
462
463 return redirect(url('pullrequest_show', repo_name=target_repo,
463 return redirect(url('pullrequest_show', repo_name=target_repo,
464 pull_request_id=pull_request.pull_request_id))
464 pull_request_id=pull_request.pull_request_id))
465
465
466 @LoginRequired()
466 @LoginRequired()
467 @NotAnonymous()
467 @NotAnonymous()
468 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
468 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
469 'repository.admin')
469 'repository.admin')
470 @auth.CSRFRequired()
470 @auth.CSRFRequired()
471 @jsonify
471 @jsonify
472 def update(self, repo_name, pull_request_id):
472 def update(self, repo_name, pull_request_id):
473 pull_request_id = safe_int(pull_request_id)
473 pull_request_id = safe_int(pull_request_id)
474 pull_request = PullRequest.get_or_404(pull_request_id)
474 pull_request = PullRequest.get_or_404(pull_request_id)
475 # only owner or admin can update it
475 # only owner or admin can update it
476 allowed_to_update = PullRequestModel().check_user_update(
476 allowed_to_update = PullRequestModel().check_user_update(
477 pull_request, c.rhodecode_user)
477 pull_request, c.rhodecode_user)
478 if allowed_to_update:
478 if allowed_to_update:
479 if 'reviewers_ids' in request.POST:
479 if 'reviewers_ids' in request.POST:
480 self._update_reviewers(pull_request_id)
480 self._update_reviewers(pull_request_id)
481 elif str2bool(request.POST.get('update_commits', 'false')):
481 elif str2bool(request.POST.get('update_commits', 'false')):
482 self._update_commits(pull_request)
482 self._update_commits(pull_request)
483 elif str2bool(request.POST.get('close_pull_request', 'false')):
483 elif str2bool(request.POST.get('close_pull_request', 'false')):
484 self._reject_close(pull_request)
484 self._reject_close(pull_request)
485 elif str2bool(request.POST.get('edit_pull_request', 'false')):
485 elif str2bool(request.POST.get('edit_pull_request', 'false')):
486 self._edit_pull_request(pull_request)
486 self._edit_pull_request(pull_request)
487 else:
487 else:
488 raise HTTPBadRequest()
488 raise HTTPBadRequest()
489 return True
489 return True
490 raise HTTPForbidden()
490 raise HTTPForbidden()
491
491
492 def _edit_pull_request(self, pull_request):
492 def _edit_pull_request(self, pull_request):
493 try:
493 try:
494 PullRequestModel().edit(
494 PullRequestModel().edit(
495 pull_request, request.POST.get('title'),
495 pull_request, request.POST.get('title'),
496 request.POST.get('description'))
496 request.POST.get('description'))
497 except ValueError:
497 except ValueError:
498 msg = _(u'Cannot update closed pull requests.')
498 msg = _(u'Cannot update closed pull requests.')
499 h.flash(msg, category='error')
499 h.flash(msg, category='error')
500 return
500 return
501 else:
501 else:
502 Session().commit()
502 Session().commit()
503
503
504 msg = _(u'Pull request title & description updated.')
504 msg = _(u'Pull request title & description updated.')
505 h.flash(msg, category='success')
505 h.flash(msg, category='success')
506 return
506 return
507
507
508 def _update_commits(self, pull_request):
508 def _update_commits(self, pull_request):
509 try:
509 try:
510 if PullRequestModel().has_valid_update_type(pull_request):
510 if PullRequestModel().has_valid_update_type(pull_request):
511 updated_version, changes = PullRequestModel().update_commits(
511 updated_version, changes = PullRequestModel().update_commits(
512 pull_request)
512 pull_request)
513 if updated_version:
513 if updated_version:
514 msg = _(
514 msg = _(
515 u'Pull request updated to "{source_commit_id}" with '
515 u'Pull request updated to "{source_commit_id}" with '
516 u'{count_added} added, {count_removed} removed '
516 u'{count_added} added, {count_removed} removed '
517 u'commits.'
517 u'commits.'
518 ).format(
518 ).format(
519 source_commit_id=pull_request.source_ref_parts.commit_id,
519 source_commit_id=pull_request.source_ref_parts.commit_id,
520 count_added=len(changes.added),
520 count_added=len(changes.added),
521 count_removed=len(changes.removed))
521 count_removed=len(changes.removed))
522 h.flash(msg, category='success')
522 h.flash(msg, category='success')
523 else:
523 else:
524 h.flash(_("Nothing changed in pull request."),
524 h.flash(_("Nothing changed in pull request."),
525 category='warning')
525 category='warning')
526 else:
526 else:
527 msg = _(
527 msg = _(
528 u"Skipping update of pull request due to reference "
528 u"Skipping update of pull request due to reference "
529 u"type: {reference_type}"
529 u"type: {reference_type}"
530 ).format(reference_type=pull_request.source_ref_parts.type)
530 ).format(reference_type=pull_request.source_ref_parts.type)
531 h.flash(msg, category='warning')
531 h.flash(msg, category='warning')
532 except CommitDoesNotExistError:
532 except CommitDoesNotExistError:
533 h.flash(
533 h.flash(
534 _(u'Update failed due to missing commits.'), category='error')
534 _(u'Update failed due to missing commits.'), category='error')
535
535
536 @auth.CSRFRequired()
536 @auth.CSRFRequired()
537 @LoginRequired()
537 @LoginRequired()
538 @NotAnonymous()
538 @NotAnonymous()
539 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
539 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
540 'repository.admin')
540 'repository.admin')
541 def merge(self, repo_name, pull_request_id):
541 def merge(self, repo_name, pull_request_id):
542 """
542 """
543 POST /{repo_name}/pull-request/{pull_request_id}
543 POST /{repo_name}/pull-request/{pull_request_id}
544
544
545 Merge will perform a server-side merge of the specified
545 Merge will perform a server-side merge of the specified
546 pull request, if the pull request is approved and mergeable.
546 pull request, if the pull request is approved and mergeable.
547 After succesfull merging, the pull request is automatically
547 After succesfull merging, the pull request is automatically
548 closed, with a relevant comment.
548 closed, with a relevant comment.
549 """
549 """
550 pull_request_id = safe_int(pull_request_id)
550 pull_request_id = safe_int(pull_request_id)
551 pull_request = PullRequest.get_or_404(pull_request_id)
551 pull_request = PullRequest.get_or_404(pull_request_id)
552 user = c.rhodecode_user
552 user = c.rhodecode_user
553
553
554 if self._meets_merge_pre_conditions(pull_request, user):
554 if self._meets_merge_pre_conditions(pull_request, user):
555 log.debug("Pre-conditions checked, trying to merge.")
555 log.debug("Pre-conditions checked, trying to merge.")
556 extras = vcs_operation_context(
556 extras = vcs_operation_context(
557 request.environ, repo_name=pull_request.target_repo.repo_name,
557 request.environ, repo_name=pull_request.target_repo.repo_name,
558 username=user.username, action='push',
558 username=user.username, action='push',
559 scm=pull_request.target_repo.repo_type)
559 scm=pull_request.target_repo.repo_type)
560 self._merge_pull_request(pull_request, user, extras)
560 self._merge_pull_request(pull_request, user, extras)
561
561
562 return redirect(url(
562 return redirect(url(
563 'pullrequest_show',
563 'pullrequest_show',
564 repo_name=pull_request.target_repo.repo_name,
564 repo_name=pull_request.target_repo.repo_name,
565 pull_request_id=pull_request.pull_request_id))
565 pull_request_id=pull_request.pull_request_id))
566
566
567 def _meets_merge_pre_conditions(self, pull_request, user):
567 def _meets_merge_pre_conditions(self, pull_request, user):
568 if not PullRequestModel().check_user_merge(pull_request, user):
568 if not PullRequestModel().check_user_merge(pull_request, user):
569 raise HTTPForbidden()
569 raise HTTPForbidden()
570
570
571 merge_status, msg = PullRequestModel().merge_status(pull_request)
571 merge_status, msg = PullRequestModel().merge_status(pull_request)
572 if not merge_status:
572 if not merge_status:
573 log.debug("Cannot merge, not mergeable.")
573 log.debug("Cannot merge, not mergeable.")
574 h.flash(msg, category='error')
574 h.flash(msg, category='error')
575 return False
575 return False
576
576
577 if (pull_request.calculated_review_status()
577 if (pull_request.calculated_review_status()
578 is not ChangesetStatus.STATUS_APPROVED):
578 is not ChangesetStatus.STATUS_APPROVED):
579 log.debug("Cannot merge, approval is pending.")
579 log.debug("Cannot merge, approval is pending.")
580 msg = _('Pull request reviewer approval is pending.')
580 msg = _('Pull request reviewer approval is pending.')
581 h.flash(msg, category='error')
581 h.flash(msg, category='error')
582 return False
582 return False
583 return True
583 return True
584
584
585 def _merge_pull_request(self, pull_request, user, extras):
585 def _merge_pull_request(self, pull_request, user, extras):
586 merge_resp = PullRequestModel().merge(
586 merge_resp = PullRequestModel().merge(
587 pull_request, user, extras=extras)
587 pull_request, user, extras=extras)
588
588
589 if merge_resp.executed:
589 if merge_resp.executed:
590 log.debug("The merge was successful, closing the pull request.")
590 log.debug("The merge was successful, closing the pull request.")
591 PullRequestModel().close_pull_request(
591 PullRequestModel().close_pull_request(
592 pull_request.pull_request_id, user)
592 pull_request.pull_request_id, user)
593 Session().commit()
593 Session().commit()
594 msg = _('Pull request was successfully merged and closed.')
594 msg = _('Pull request was successfully merged and closed.')
595 h.flash(msg, category='success')
595 h.flash(msg, category='success')
596 else:
596 else:
597 log.debug(
597 log.debug(
598 "The merge was not successful. Merge response: %s",
598 "The merge was not successful. Merge response: %s",
599 merge_resp)
599 merge_resp)
600 msg = PullRequestModel().merge_status_message(
600 msg = PullRequestModel().merge_status_message(
601 merge_resp.failure_reason)
601 merge_resp.failure_reason)
602 h.flash(msg, category='error')
602 h.flash(msg, category='error')
603
603
604 def _update_reviewers(self, pull_request_id):
604 def _update_reviewers(self, pull_request_id):
605 reviewers_ids = map(int, filter(
605 reviewers_ids = map(int, filter(
606 lambda v: v not in [None, ''],
606 lambda v: v not in [None, ''],
607 request.POST.get('reviewers_ids', '').split(',')))
607 request.POST.get('reviewers_ids', '').split(',')))
608 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
608 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
609 Session().commit()
609 Session().commit()
610
610
611 def _reject_close(self, pull_request):
611 def _reject_close(self, pull_request):
612 if pull_request.is_closed():
612 if pull_request.is_closed():
613 raise HTTPForbidden()
613 raise HTTPForbidden()
614
614
615 PullRequestModel().close_pull_request_with_comment(
615 PullRequestModel().close_pull_request_with_comment(
616 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
616 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
617 Session().commit()
617 Session().commit()
618
618
619 @LoginRequired()
619 @LoginRequired()
620 @NotAnonymous()
620 @NotAnonymous()
621 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
621 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
622 'repository.admin')
622 'repository.admin')
623 @auth.CSRFRequired()
623 @auth.CSRFRequired()
624 @jsonify
624 @jsonify
625 def delete(self, repo_name, pull_request_id):
625 def delete(self, repo_name, pull_request_id):
626 pull_request_id = safe_int(pull_request_id)
626 pull_request_id = safe_int(pull_request_id)
627 pull_request = PullRequest.get_or_404(pull_request_id)
627 pull_request = PullRequest.get_or_404(pull_request_id)
628 # only owner can delete it !
628 # only owner can delete it !
629 if pull_request.author.user_id == c.rhodecode_user.user_id:
629 if pull_request.author.user_id == c.rhodecode_user.user_id:
630 PullRequestModel().delete(pull_request)
630 PullRequestModel().delete(pull_request)
631 Session().commit()
631 Session().commit()
632 h.flash(_('Successfully deleted pull request'),
632 h.flash(_('Successfully deleted pull request'),
633 category='success')
633 category='success')
634 return redirect(url('my_account_pullrequests'))
634 return redirect(url('my_account_pullrequests'))
635 raise HTTPForbidden()
635 raise HTTPForbidden()
636
636
637 @LoginRequired()
637 @LoginRequired()
638 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
638 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
639 'repository.admin')
639 'repository.admin')
640 def show(self, repo_name, pull_request_id):
640 def show(self, repo_name, pull_request_id):
641 pull_request_id = safe_int(pull_request_id)
641 pull_request_id = safe_int(pull_request_id)
642 c.pull_request = PullRequest.get_or_404(pull_request_id)
642 c.pull_request = PullRequest.get_or_404(pull_request_id)
643
643
644 c.template_context['pull_request_data']['pull_request_id'] = \
644 c.template_context['pull_request_data']['pull_request_id'] = \
645 pull_request_id
645 pull_request_id
646
646
647 # pull_requests repo_name we opened it against
647 # pull_requests repo_name we opened it against
648 # ie. target_repo must match
648 # ie. target_repo must match
649 if repo_name != c.pull_request.target_repo.repo_name:
649 if repo_name != c.pull_request.target_repo.repo_name:
650 raise HTTPNotFound
650 raise HTTPNotFound
651
651
652 c.allowed_to_change_status = PullRequestModel(). \
652 c.allowed_to_change_status = PullRequestModel(). \
653 check_user_change_status(c.pull_request, c.rhodecode_user)
653 check_user_change_status(c.pull_request, c.rhodecode_user)
654 c.allowed_to_update = PullRequestModel().check_user_update(
654 c.allowed_to_update = PullRequestModel().check_user_update(
655 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
655 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
656 c.allowed_to_merge = PullRequestModel().check_user_merge(
656 c.allowed_to_merge = PullRequestModel().check_user_merge(
657 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
657 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
658
658
659 cc_model = ChangesetCommentsModel()
659 cc_model = ChangesetCommentsModel()
660
660
661 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
661 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
662
662
663 c.pull_request_review_status = c.pull_request.calculated_review_status()
663 c.pull_request_review_status = c.pull_request.calculated_review_status()
664 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
664 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
665 c.pull_request)
665 c.pull_request)
666 c.approval_msg = None
666 c.approval_msg = None
667 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
667 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
668 c.approval_msg = _('Reviewer approval is pending.')
668 c.approval_msg = _('Reviewer approval is pending.')
669 c.pr_merge_status = False
669 c.pr_merge_status = False
670 # load compare data into template context
670 # load compare data into template context
671 enable_comments = not c.pull_request.is_closed()
671 enable_comments = not c.pull_request.is_closed()
672 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
672 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
673
673
674 # this is a hack to properly display links, when creating PR, the
674 # this is a hack to properly display links, when creating PR, the
675 # compare view and others uses different notation, and
675 # compare view and others uses different notation, and
676 # compare_commits.html renders links based on the target_repo.
676 # compare_commits.html renders links based on the target_repo.
677 # We need to swap that here to generate it properly on the html side
677 # We need to swap that here to generate it properly on the html side
678 c.target_repo = c.source_repo
678 c.target_repo = c.source_repo
679
679
680 # inline comments
680 # inline comments
681 c.inline_cnt = 0
681 c.inline_cnt = 0
682 c.inline_comments = cc_model.get_inline_comments(
682 c.inline_comments = cc_model.get_inline_comments(
683 c.rhodecode_db_repo.repo_id,
683 c.rhodecode_db_repo.repo_id,
684 pull_request=pull_request_id).items()
684 pull_request=pull_request_id).items()
685 # count inline comments
685 # count inline comments
686 for __, lines in c.inline_comments:
686 for __, lines in c.inline_comments:
687 for comments in lines.values():
687 for comments in lines.values():
688 c.inline_cnt += len(comments)
688 c.inline_cnt += len(comments)
689
689
690 # outdated comments
690 # outdated comments
691 c.outdated_cnt = 0
691 c.outdated_cnt = 0
692 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
692 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
693 c.outdated_comments = cc_model.get_outdated_comments(
693 c.outdated_comments = cc_model.get_outdated_comments(
694 c.rhodecode_db_repo.repo_id,
694 c.rhodecode_db_repo.repo_id,
695 pull_request=c.pull_request)
695 pull_request=c.pull_request)
696 # Count outdated comments and check for deleted files
696 # Count outdated comments and check for deleted files
697 for file_name, lines in c.outdated_comments.iteritems():
697 for file_name, lines in c.outdated_comments.iteritems():
698 for comments in lines.values():
698 for comments in lines.values():
699 c.outdated_cnt += len(comments)
699 c.outdated_cnt += len(comments)
700 if file_name not in c.included_files:
700 if file_name not in c.included_files:
701 c.deleted_files.append(file_name)
701 c.deleted_files.append(file_name)
702 else:
702 else:
703 c.outdated_comments = {}
703 c.outdated_comments = {}
704
704
705 # comments
705 # comments
706 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
706 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
707 pull_request=pull_request_id)
707 pull_request=pull_request_id)
708
708
709 if c.allowed_to_update:
709 if c.allowed_to_update:
710 force_close = ('forced_closed', _('Close Pull Request'))
710 force_close = ('forced_closed', _('Close Pull Request'))
711 statuses = ChangesetStatus.STATUSES + [force_close]
711 statuses = ChangesetStatus.STATUSES + [force_close]
712 else:
712 else:
713 statuses = ChangesetStatus.STATUSES
713 statuses = ChangesetStatus.STATUSES
714 c.commit_statuses = statuses
714 c.commit_statuses = statuses
715
715
716 c.ancestor = None # TODO: add ancestor here
716 c.ancestor = None # TODO: add ancestor here
717
717
718 return render('/pullrequests/pullrequest_show.html')
718 return render('/pullrequests/pullrequest_show.html')
719
719
720 @LoginRequired()
720 @LoginRequired()
721 @NotAnonymous()
721 @NotAnonymous()
722 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
722 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
723 'repository.admin')
723 'repository.admin')
724 @auth.CSRFRequired()
724 @auth.CSRFRequired()
725 @jsonify
725 @jsonify
726 def comment(self, repo_name, pull_request_id):
726 def comment(self, repo_name, pull_request_id):
727 pull_request_id = safe_int(pull_request_id)
727 pull_request_id = safe_int(pull_request_id)
728 pull_request = PullRequest.get_or_404(pull_request_id)
728 pull_request = PullRequest.get_or_404(pull_request_id)
729 if pull_request.is_closed():
729 if pull_request.is_closed():
730 raise HTTPForbidden()
730 raise HTTPForbidden()
731
731
732 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
732 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
733 # as a changeset status, still we want to send it in one value.
733 # as a changeset status, still we want to send it in one value.
734 status = request.POST.get('changeset_status', None)
734 status = request.POST.get('changeset_status', None)
735 text = request.POST.get('text')
735 text = request.POST.get('text')
736 if status and '_closed' in status:
736 if status and '_closed' in status:
737 close_pr = True
737 close_pr = True
738 status = status.replace('_closed', '')
738 status = status.replace('_closed', '')
739 else:
739 else:
740 close_pr = False
740 close_pr = False
741
741
742 forced = (status == 'forced')
742 forced = (status == 'forced')
743 if forced:
743 if forced:
744 status = 'rejected'
744 status = 'rejected'
745
745
746 allowed_to_change_status = PullRequestModel().check_user_change_status(
746 allowed_to_change_status = PullRequestModel().check_user_change_status(
747 pull_request, c.rhodecode_user)
747 pull_request, c.rhodecode_user)
748
748
749 if status and allowed_to_change_status:
749 if status and allowed_to_change_status:
750 message = (_('Status change %(transition_icon)s %(status)s')
750 message = (_('Status change %(transition_icon)s %(status)s')
751 % {'transition_icon': '>',
751 % {'transition_icon': '>',
752 'status': ChangesetStatus.get_status_lbl(status)})
752 'status': ChangesetStatus.get_status_lbl(status)})
753 if close_pr:
753 if close_pr:
754 message = _('Closing with') + ' ' + message
754 message = _('Closing with') + ' ' + message
755 text = text or message
755 text = text or message
756 comm = ChangesetCommentsModel().create(
756 comm = ChangesetCommentsModel().create(
757 text=text,
757 text=text,
758 repo=c.rhodecode_db_repo.repo_id,
758 repo=c.rhodecode_db_repo.repo_id,
759 user=c.rhodecode_user.user_id,
759 user=c.rhodecode_user.user_id,
760 pull_request=pull_request_id,
760 pull_request=pull_request_id,
761 f_path=request.POST.get('f_path'),
761 f_path=request.POST.get('f_path'),
762 line_no=request.POST.get('line'),
762 line_no=request.POST.get('line'),
763 status_change=(ChangesetStatus.get_status_lbl(status)
763 status_change=(ChangesetStatus.get_status_lbl(status)
764 if status and allowed_to_change_status else None),
764 if status and allowed_to_change_status else None),
765 status_change_type=(status
766 if status and allowed_to_change_status else None),
765 closing_pr=close_pr
767 closing_pr=close_pr
766 )
768 )
767
769
768
770
769
771
770 if allowed_to_change_status:
772 if allowed_to_change_status:
771 old_calculated_status = pull_request.calculated_review_status()
773 old_calculated_status = pull_request.calculated_review_status()
772 # get status if set !
774 # get status if set !
773 if status:
775 if status:
774 ChangesetStatusModel().set_status(
776 ChangesetStatusModel().set_status(
775 c.rhodecode_db_repo.repo_id,
777 c.rhodecode_db_repo.repo_id,
776 status,
778 status,
777 c.rhodecode_user.user_id,
779 c.rhodecode_user.user_id,
778 comm,
780 comm,
779 pull_request=pull_request_id
781 pull_request=pull_request_id
780 )
782 )
781
783
782 Session().flush()
784 Session().flush()
783 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
785 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
784 # we now calculate the status of pull request, and based on that
786 # we now calculate the status of pull request, and based on that
785 # calculation we set the commits status
787 # calculation we set the commits status
786 calculated_status = pull_request.calculated_review_status()
788 calculated_status = pull_request.calculated_review_status()
787 if old_calculated_status != calculated_status:
789 if old_calculated_status != calculated_status:
788 PullRequestModel()._trigger_pull_request_hook(
790 PullRequestModel()._trigger_pull_request_hook(
789 pull_request, c.rhodecode_user, 'review_status_change')
791 pull_request, c.rhodecode_user, 'review_status_change')
790
792
791 calculated_status_lbl = ChangesetStatus.get_status_lbl(
793 calculated_status_lbl = ChangesetStatus.get_status_lbl(
792 calculated_status)
794 calculated_status)
793
795
794 if close_pr:
796 if close_pr:
795 status_completed = (
797 status_completed = (
796 calculated_status in [ChangesetStatus.STATUS_APPROVED,
798 calculated_status in [ChangesetStatus.STATUS_APPROVED,
797 ChangesetStatus.STATUS_REJECTED])
799 ChangesetStatus.STATUS_REJECTED])
798 if forced or status_completed:
800 if forced or status_completed:
799 PullRequestModel().close_pull_request(
801 PullRequestModel().close_pull_request(
800 pull_request_id, c.rhodecode_user)
802 pull_request_id, c.rhodecode_user)
801 else:
803 else:
802 h.flash(_('Closing pull request on other statuses than '
804 h.flash(_('Closing pull request on other statuses than '
803 'rejected or approved is forbidden. '
805 'rejected or approved is forbidden. '
804 'Calculated status from all reviewers '
806 'Calculated status from all reviewers '
805 'is currently: %s') % calculated_status_lbl,
807 'is currently: %s') % calculated_status_lbl,
806 category='warning')
808 category='warning')
807
809
808 Session().commit()
810 Session().commit()
809
811
810 if not request.is_xhr:
812 if not request.is_xhr:
811 return redirect(h.url('pullrequest_show', repo_name=repo_name,
813 return redirect(h.url('pullrequest_show', repo_name=repo_name,
812 pull_request_id=pull_request_id))
814 pull_request_id=pull_request_id))
813
815
814 data = {
816 data = {
815 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
817 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
816 }
818 }
817 if comm:
819 if comm:
818 c.co = comm
820 c.co = comm
819 data.update(comm.get_dict())
821 data.update(comm.get_dict())
820 data.update({'rendered_text':
822 data.update({'rendered_text':
821 render('changeset/changeset_comment_block.html')})
823 render('changeset/changeset_comment_block.html')})
822
824
823 return data
825 return data
824
826
825 @LoginRequired()
827 @LoginRequired()
826 @NotAnonymous()
828 @NotAnonymous()
827 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
829 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
828 'repository.admin')
830 'repository.admin')
829 @auth.CSRFRequired()
831 @auth.CSRFRequired()
830 @jsonify
832 @jsonify
831 def delete_comment(self, repo_name, comment_id):
833 def delete_comment(self, repo_name, comment_id):
832 return self._delete_comment(comment_id)
834 return self._delete_comment(comment_id)
833
835
834 def _delete_comment(self, comment_id):
836 def _delete_comment(self, comment_id):
835 comment_id = safe_int(comment_id)
837 comment_id = safe_int(comment_id)
836 co = ChangesetComment.get_or_404(comment_id)
838 co = ChangesetComment.get_or_404(comment_id)
837 if co.pull_request.is_closed():
839 if co.pull_request.is_closed():
838 # don't allow deleting comments on closed pull request
840 # don't allow deleting comments on closed pull request
839 raise HTTPForbidden()
841 raise HTTPForbidden()
840
842
841 is_owner = co.author.user_id == c.rhodecode_user.user_id
843 is_owner = co.author.user_id == c.rhodecode_user.user_id
842 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
844 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
843 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
845 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
844 old_calculated_status = co.pull_request.calculated_review_status()
846 old_calculated_status = co.pull_request.calculated_review_status()
845 ChangesetCommentsModel().delete(comment=co)
847 ChangesetCommentsModel().delete(comment=co)
846 Session().commit()
848 Session().commit()
847 calculated_status = co.pull_request.calculated_review_status()
849 calculated_status = co.pull_request.calculated_review_status()
848 if old_calculated_status != calculated_status:
850 if old_calculated_status != calculated_status:
849 PullRequestModel()._trigger_pull_request_hook(
851 PullRequestModel()._trigger_pull_request_hook(
850 co.pull_request, c.rhodecode_user, 'review_status_change')
852 co.pull_request, c.rhodecode_user, 'review_status_change')
851 return True
853 return True
852 else:
854 else:
853 raise HTTPForbidden()
855 raise HTTPForbidden()
@@ -1,512 +1,515 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest)
42 ChangesetComment, User, Notification, PullRequest)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class ChangesetCommentsModel(BaseModel):
51 class ChangesetCommentsModel(BaseModel):
52
52
53 cls = ChangesetComment
53 cls = ChangesetComment
54
54
55 DIFF_CONTEXT_BEFORE = 3
55 DIFF_CONTEXT_BEFORE = 3
56 DIFF_CONTEXT_AFTER = 3
56 DIFF_CONTEXT_AFTER = 3
57
57
58 def __get_commit_comment(self, changeset_comment):
58 def __get_commit_comment(self, changeset_comment):
59 return self._get_instance(ChangesetComment, changeset_comment)
59 return self._get_instance(ChangesetComment, changeset_comment)
60
60
61 def __get_pull_request(self, pull_request):
61 def __get_pull_request(self, pull_request):
62 return self._get_instance(PullRequest, pull_request)
62 return self._get_instance(PullRequest, pull_request)
63
63
64 def _extract_mentions(self, s):
64 def _extract_mentions(self, s):
65 user_objects = []
65 user_objects = []
66 for username in extract_mentioned_users(s):
66 for username in extract_mentioned_users(s):
67 user_obj = User.get_by_username(username, case_insensitive=True)
67 user_obj = User.get_by_username(username, case_insensitive=True)
68 if user_obj:
68 if user_obj:
69 user_objects.append(user_obj)
69 user_objects.append(user_obj)
70 return user_objects
70 return user_objects
71
71
72 def _get_renderer(self, global_renderer='rst'):
72 def _get_renderer(self, global_renderer='rst'):
73 try:
73 try:
74 # try reading from visual context
74 # try reading from visual context
75 from pylons import tmpl_context
75 from pylons import tmpl_context
76 global_renderer = tmpl_context.visual.default_renderer
76 global_renderer = tmpl_context.visual.default_renderer
77 except AttributeError:
77 except AttributeError:
78 log.debug("Renderer not set, falling back "
78 log.debug("Renderer not set, falling back "
79 "to default renderer '%s'", global_renderer)
79 "to default renderer '%s'", global_renderer)
80 except Exception:
80 except Exception:
81 log.error(traceback.format_exc())
81 log.error(traceback.format_exc())
82 return global_renderer
82 return global_renderer
83
83
84 def create(self, text, repo, user, revision=None, pull_request=None,
84 def create(self, text, repo, user, revision=None, pull_request=None,
85 f_path=None, line_no=None, status_change=None, closing_pr=False,
85 f_path=None, line_no=None, status_change=None,
86 status_change_type=None, closing_pr=False,
86 send_email=True, renderer=None):
87 send_email=True, renderer=None):
87 """
88 """
88 Creates new comment for commit or pull request.
89 Creates new comment for commit or pull request.
89 IF status_change is not none this comment is associated with a
90 IF status_change is not none this comment is associated with a
90 status change of commit or commit associated with pull request
91 status change of commit or commit associated with pull request
91
92
92 :param text:
93 :param text:
93 :param repo:
94 :param repo:
94 :param user:
95 :param user:
95 :param revision:
96 :param revision:
96 :param pull_request:
97 :param pull_request:
97 :param f_path:
98 :param f_path:
98 :param line_no:
99 :param line_no:
99 :param status_change:
100 :param status_change: Label for status change
101 :param status_change_type: type of status change
100 :param closing_pr:
102 :param closing_pr:
101 :param send_email:
103 :param send_email:
102 """
104 """
103 if not text:
105 if not text:
104 log.warning('Missing text for comment, skipping...')
106 log.warning('Missing text for comment, skipping...')
105 return
107 return
106
108
107 if not renderer:
109 if not renderer:
108 renderer = self._get_renderer()
110 renderer = self._get_renderer()
109
111
110 repo = self._get_repo(repo)
112 repo = self._get_repo(repo)
111 user = self._get_user(user)
113 user = self._get_user(user)
112 comment = ChangesetComment()
114 comment = ChangesetComment()
113 comment.renderer = renderer
115 comment.renderer = renderer
114 comment.repo = repo
116 comment.repo = repo
115 comment.author = user
117 comment.author = user
116 comment.text = text
118 comment.text = text
117 comment.f_path = f_path
119 comment.f_path = f_path
118 comment.line_no = line_no
120 comment.line_no = line_no
119
121
120 #TODO (marcink): fix this and remove revision as param
122 #TODO (marcink): fix this and remove revision as param
121 commit_id = revision
123 commit_id = revision
122 pull_request_id = pull_request
124 pull_request_id = pull_request
123
125
124 commit_obj = None
126 commit_obj = None
125 pull_request_obj = None
127 pull_request_obj = None
126
128
127 if commit_id:
129 if commit_id:
128 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
129 # do a lookup, so we don't pass something bad here
131 # do a lookup, so we don't pass something bad here
130 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
131 comment.revision = commit_obj.raw_id
133 comment.revision = commit_obj.raw_id
132
134
133 elif pull_request_id:
135 elif pull_request_id:
134 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
135 pull_request_obj = self.__get_pull_request(pull_request_id)
137 pull_request_obj = self.__get_pull_request(pull_request_id)
136 comment.pull_request = pull_request_obj
138 comment.pull_request = pull_request_obj
137 else:
139 else:
138 raise Exception('Please specify commit or pull_request_id')
140 raise Exception('Please specify commit or pull_request_id')
139
141
140 Session().add(comment)
142 Session().add(comment)
141 Session().flush()
143 Session().flush()
142 kwargs = {
144 kwargs = {
143 'user': user,
145 'user': user,
144 'renderer_type': renderer,
146 'renderer_type': renderer,
145 'repo_name': repo.repo_name,
147 'repo_name': repo.repo_name,
146 'status_change': status_change,
148 'status_change': status_change,
149 'status_change_type': status_change_type,
147 'comment_body': text,
150 'comment_body': text,
148 'comment_file': f_path,
151 'comment_file': f_path,
149 'comment_line': line_no,
152 'comment_line': line_no,
150 }
153 }
151
154
152 if commit_obj:
155 if commit_obj:
153 recipients = ChangesetComment.get_users(
156 recipients = ChangesetComment.get_users(
154 revision=commit_obj.raw_id)
157 revision=commit_obj.raw_id)
155 # add commit author if it's in RhodeCode system
158 # add commit author if it's in RhodeCode system
156 cs_author = User.get_from_cs_author(commit_obj.author)
159 cs_author = User.get_from_cs_author(commit_obj.author)
157 if not cs_author:
160 if not cs_author:
158 # use repo owner if we cannot extract the author correctly
161 # use repo owner if we cannot extract the author correctly
159 cs_author = repo.user
162 cs_author = repo.user
160 recipients += [cs_author]
163 recipients += [cs_author]
161
164
162 commit_comment_url = self.get_url(comment)
165 commit_comment_url = self.get_url(comment)
163
166
164 target_repo_url = h.link_to(
167 target_repo_url = h.link_to(
165 repo.repo_name,
168 repo.repo_name,
166 h.url('summary_home',
169 h.url('summary_home',
167 repo_name=repo.repo_name, qualified=True))
170 repo_name=repo.repo_name, qualified=True))
168
171
169 # commit specifics
172 # commit specifics
170 kwargs.update({
173 kwargs.update({
171 'commit': commit_obj,
174 'commit': commit_obj,
172 'commit_message': commit_obj.message,
175 'commit_message': commit_obj.message,
173 'commit_target_repo': target_repo_url,
176 'commit_target_repo': target_repo_url,
174 'commit_comment_url': commit_comment_url,
177 'commit_comment_url': commit_comment_url,
175 })
178 })
176
179
177 elif pull_request_obj:
180 elif pull_request_obj:
178 # get the current participants of this pull request
181 # get the current participants of this pull request
179 recipients = ChangesetComment.get_users(
182 recipients = ChangesetComment.get_users(
180 pull_request_id=pull_request_obj.pull_request_id)
183 pull_request_id=pull_request_obj.pull_request_id)
181 # add pull request author
184 # add pull request author
182 recipients += [pull_request_obj.author]
185 recipients += [pull_request_obj.author]
183
186
184 # add the reviewers to notification
187 # add the reviewers to notification
185 recipients += [x.user for x in pull_request_obj.reviewers]
188 recipients += [x.user for x in pull_request_obj.reviewers]
186
189
187 pr_target_repo = pull_request_obj.target_repo
190 pr_target_repo = pull_request_obj.target_repo
188 pr_source_repo = pull_request_obj.source_repo
191 pr_source_repo = pull_request_obj.source_repo
189
192
190 pr_comment_url = h.url(
193 pr_comment_url = h.url(
191 'pullrequest_show',
194 'pullrequest_show',
192 repo_name=pr_target_repo.repo_name,
195 repo_name=pr_target_repo.repo_name,
193 pull_request_id=pull_request_obj.pull_request_id,
196 pull_request_id=pull_request_obj.pull_request_id,
194 anchor='comment-%s' % comment.comment_id,
197 anchor='comment-%s' % comment.comment_id,
195 qualified=True,)
198 qualified=True,)
196
199
197 # set some variables for email notification
200 # set some variables for email notification
198 pr_target_repo_url = h.url(
201 pr_target_repo_url = h.url(
199 'summary_home', repo_name=pr_target_repo.repo_name,
202 'summary_home', repo_name=pr_target_repo.repo_name,
200 qualified=True)
203 qualified=True)
201
204
202 pr_source_repo_url = h.url(
205 pr_source_repo_url = h.url(
203 'summary_home', repo_name=pr_source_repo.repo_name,
206 'summary_home', repo_name=pr_source_repo.repo_name,
204 qualified=True)
207 qualified=True)
205
208
206 # pull request specifics
209 # pull request specifics
207 kwargs.update({
210 kwargs.update({
208 'pull_request': pull_request_obj,
211 'pull_request': pull_request_obj,
209 'pr_id': pull_request_obj.pull_request_id,
212 'pr_id': pull_request_obj.pull_request_id,
210 'pr_target_repo': pr_target_repo,
213 'pr_target_repo': pr_target_repo,
211 'pr_target_repo_url': pr_target_repo_url,
214 'pr_target_repo_url': pr_target_repo_url,
212 'pr_source_repo': pr_source_repo,
215 'pr_source_repo': pr_source_repo,
213 'pr_source_repo_url': pr_source_repo_url,
216 'pr_source_repo_url': pr_source_repo_url,
214 'pr_comment_url': pr_comment_url,
217 'pr_comment_url': pr_comment_url,
215 'pr_closing': closing_pr,
218 'pr_closing': closing_pr,
216 })
219 })
217 if send_email:
220 if send_email:
218 # pre-generate the subject for notification itself
221 # pre-generate the subject for notification itself
219 (subject,
222 (subject,
220 _h, _e, # we don't care about those
223 _h, _e, # we don't care about those
221 body_plaintext) = EmailNotificationModel().render_email(
224 body_plaintext) = EmailNotificationModel().render_email(
222 notification_type, **kwargs)
225 notification_type, **kwargs)
223
226
224 mention_recipients = set(
227 mention_recipients = set(
225 self._extract_mentions(text)).difference(recipients)
228 self._extract_mentions(text)).difference(recipients)
226
229
227 # create notification objects, and emails
230 # create notification objects, and emails
228 NotificationModel().create(
231 NotificationModel().create(
229 created_by=user,
232 created_by=user,
230 notification_subject=subject,
233 notification_subject=subject,
231 notification_body=body_plaintext,
234 notification_body=body_plaintext,
232 notification_type=notification_type,
235 notification_type=notification_type,
233 recipients=recipients,
236 recipients=recipients,
234 mention_recipients=mention_recipients,
237 mention_recipients=mention_recipients,
235 email_kwargs=kwargs,
238 email_kwargs=kwargs,
236 )
239 )
237
240
238 action = (
241 action = (
239 'user_commented_pull_request:{}'.format(
242 'user_commented_pull_request:{}'.format(
240 comment.pull_request.pull_request_id)
243 comment.pull_request.pull_request_id)
241 if comment.pull_request
244 if comment.pull_request
242 else 'user_commented_revision:{}'.format(comment.revision)
245 else 'user_commented_revision:{}'.format(comment.revision)
243 )
246 )
244 action_logger(user, action, comment.repo)
247 action_logger(user, action, comment.repo)
245
248
246 registry = get_current_registry()
249 registry = get_current_registry()
247 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
248 channelstream_config = rhodecode_plugins.get('channelstream', {})
251 channelstream_config = rhodecode_plugins.get('channelstream', {})
249 msg_url = ''
252 msg_url = ''
250 if commit_obj:
253 if commit_obj:
251 msg_url = commit_comment_url
254 msg_url = commit_comment_url
252 repo_name = repo.repo_name
255 repo_name = repo.repo_name
253 elif pull_request_obj:
256 elif pull_request_obj:
254 msg_url = pr_comment_url
257 msg_url = pr_comment_url
255 repo_name = pr_target_repo.repo_name
258 repo_name = pr_target_repo.repo_name
256
259
257 if channelstream_config.get('enabled'):
260 if channelstream_config.get('enabled'):
258 message = '<strong>{}</strong> {} - ' \
261 message = '<strong>{}</strong> {} - ' \
259 '<a onclick="window.location=\'{}\';' \
262 '<a onclick="window.location=\'{}\';' \
260 'window.location.reload()">' \
263 'window.location.reload()">' \
261 '<strong>{}</strong></a>'
264 '<strong>{}</strong></a>'
262 message = message.format(
265 message = message.format(
263 user.username, _('made a comment'), msg_url,
266 user.username, _('made a comment'), msg_url,
264 _('Refresh page'))
267 _('Refresh page'))
265 channel = '/repo${}$/pr/{}'.format(
268 channel = '/repo${}$/pr/{}'.format(
266 repo_name,
269 repo_name,
267 pull_request_id
270 pull_request_id
268 )
271 )
269 payload = {
272 payload = {
270 'type': 'message',
273 'type': 'message',
271 'timestamp': datetime.utcnow(),
274 'timestamp': datetime.utcnow(),
272 'user': 'system',
275 'user': 'system',
273 'exclude_users': [user.username],
276 'exclude_users': [user.username],
274 'channel': channel,
277 'channel': channel,
275 'message': {
278 'message': {
276 'message': message,
279 'message': message,
277 'level': 'info',
280 'level': 'info',
278 'topic': '/notifications'
281 'topic': '/notifications'
279 }
282 }
280 }
283 }
281 channelstream_request(channelstream_config, [payload],
284 channelstream_request(channelstream_config, [payload],
282 '/message', raise_exc=False)
285 '/message', raise_exc=False)
283
286
284 return comment
287 return comment
285
288
286 def delete(self, comment):
289 def delete(self, comment):
287 """
290 """
288 Deletes given comment
291 Deletes given comment
289
292
290 :param comment_id:
293 :param comment_id:
291 """
294 """
292 comment = self.__get_commit_comment(comment)
295 comment = self.__get_commit_comment(comment)
293 Session().delete(comment)
296 Session().delete(comment)
294
297
295 return comment
298 return comment
296
299
297 def get_all_comments(self, repo_id, revision=None, pull_request=None):
300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
298 q = ChangesetComment.query()\
301 q = ChangesetComment.query()\
299 .filter(ChangesetComment.repo_id == repo_id)
302 .filter(ChangesetComment.repo_id == repo_id)
300 if revision:
303 if revision:
301 q = q.filter(ChangesetComment.revision == revision)
304 q = q.filter(ChangesetComment.revision == revision)
302 elif pull_request:
305 elif pull_request:
303 pull_request = self.__get_pull_request(pull_request)
306 pull_request = self.__get_pull_request(pull_request)
304 q = q.filter(ChangesetComment.pull_request == pull_request)
307 q = q.filter(ChangesetComment.pull_request == pull_request)
305 else:
308 else:
306 raise Exception('Please specify commit or pull_request')
309 raise Exception('Please specify commit or pull_request')
307 q = q.order_by(ChangesetComment.created_on)
310 q = q.order_by(ChangesetComment.created_on)
308 return q.all()
311 return q.all()
309
312
310 def get_url(self, comment):
313 def get_url(self, comment):
311 comment = self.__get_commit_comment(comment)
314 comment = self.__get_commit_comment(comment)
312 if comment.pull_request:
315 if comment.pull_request:
313 return h.url(
316 return h.url(
314 'pullrequest_show',
317 'pullrequest_show',
315 repo_name=comment.pull_request.target_repo.repo_name,
318 repo_name=comment.pull_request.target_repo.repo_name,
316 pull_request_id=comment.pull_request.pull_request_id,
319 pull_request_id=comment.pull_request.pull_request_id,
317 anchor='comment-%s' % comment.comment_id,
320 anchor='comment-%s' % comment.comment_id,
318 qualified=True,)
321 qualified=True,)
319 else:
322 else:
320 return h.url(
323 return h.url(
321 'changeset_home',
324 'changeset_home',
322 repo_name=comment.repo.repo_name,
325 repo_name=comment.repo.repo_name,
323 revision=comment.revision,
326 revision=comment.revision,
324 anchor='comment-%s' % comment.comment_id,
327 anchor='comment-%s' % comment.comment_id,
325 qualified=True,)
328 qualified=True,)
326
329
327 def get_comments(self, repo_id, revision=None, pull_request=None):
330 def get_comments(self, repo_id, revision=None, pull_request=None):
328 """
331 """
329 Gets main comments based on revision or pull_request_id
332 Gets main comments based on revision or pull_request_id
330
333
331 :param repo_id:
334 :param repo_id:
332 :param revision:
335 :param revision:
333 :param pull_request:
336 :param pull_request:
334 """
337 """
335
338
336 q = ChangesetComment.query()\
339 q = ChangesetComment.query()\
337 .filter(ChangesetComment.repo_id == repo_id)\
340 .filter(ChangesetComment.repo_id == repo_id)\
338 .filter(ChangesetComment.line_no == None)\
341 .filter(ChangesetComment.line_no == None)\
339 .filter(ChangesetComment.f_path == None)
342 .filter(ChangesetComment.f_path == None)
340 if revision:
343 if revision:
341 q = q.filter(ChangesetComment.revision == revision)
344 q = q.filter(ChangesetComment.revision == revision)
342 elif pull_request:
345 elif pull_request:
343 pull_request = self.__get_pull_request(pull_request)
346 pull_request = self.__get_pull_request(pull_request)
344 q = q.filter(ChangesetComment.pull_request == pull_request)
347 q = q.filter(ChangesetComment.pull_request == pull_request)
345 else:
348 else:
346 raise Exception('Please specify commit or pull_request')
349 raise Exception('Please specify commit or pull_request')
347 q = q.order_by(ChangesetComment.created_on)
350 q = q.order_by(ChangesetComment.created_on)
348 return q.all()
351 return q.all()
349
352
350 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
351 q = self._get_inline_comments_query(repo_id, revision, pull_request)
354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
352 return self._group_comments_by_path_and_line_number(q)
355 return self._group_comments_by_path_and_line_number(q)
353
356
354 def get_outdated_comments(self, repo_id, pull_request):
357 def get_outdated_comments(self, repo_id, pull_request):
355 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
358 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
356 # of a pull request.
359 # of a pull request.
357 q = self._all_inline_comments_of_pull_request(pull_request)
360 q = self._all_inline_comments_of_pull_request(pull_request)
358 q = q.filter(
361 q = q.filter(
359 ChangesetComment.display_state ==
362 ChangesetComment.display_state ==
360 ChangesetComment.COMMENT_OUTDATED
363 ChangesetComment.COMMENT_OUTDATED
361 ).order_by(ChangesetComment.comment_id.asc())
364 ).order_by(ChangesetComment.comment_id.asc())
362
365
363 return self._group_comments_by_path_and_line_number(q)
366 return self._group_comments_by_path_and_line_number(q)
364
367
365 def _get_inline_comments_query(self, repo_id, revision, pull_request):
368 def _get_inline_comments_query(self, repo_id, revision, pull_request):
366 # TODO: johbo: Split this into two methods: One for PR and one for
369 # TODO: johbo: Split this into two methods: One for PR and one for
367 # commit.
370 # commit.
368 if revision:
371 if revision:
369 q = Session().query(ChangesetComment).filter(
372 q = Session().query(ChangesetComment).filter(
370 ChangesetComment.repo_id == repo_id,
373 ChangesetComment.repo_id == repo_id,
371 ChangesetComment.line_no != null(),
374 ChangesetComment.line_no != null(),
372 ChangesetComment.f_path != null(),
375 ChangesetComment.f_path != null(),
373 ChangesetComment.revision == revision)
376 ChangesetComment.revision == revision)
374
377
375 elif pull_request:
378 elif pull_request:
376 pull_request = self.__get_pull_request(pull_request)
379 pull_request = self.__get_pull_request(pull_request)
377 if ChangesetCommentsModel.use_outdated_comments(pull_request):
380 if ChangesetCommentsModel.use_outdated_comments(pull_request):
378 q = self._visible_inline_comments_of_pull_request(pull_request)
381 q = self._visible_inline_comments_of_pull_request(pull_request)
379 else:
382 else:
380 q = self._all_inline_comments_of_pull_request(pull_request)
383 q = self._all_inline_comments_of_pull_request(pull_request)
381
384
382 else:
385 else:
383 raise Exception('Please specify commit or pull_request_id')
386 raise Exception('Please specify commit or pull_request_id')
384 q = q.order_by(ChangesetComment.comment_id.asc())
387 q = q.order_by(ChangesetComment.comment_id.asc())
385 return q
388 return q
386
389
387 def _group_comments_by_path_and_line_number(self, q):
390 def _group_comments_by_path_and_line_number(self, q):
388 comments = q.all()
391 comments = q.all()
389 paths = collections.defaultdict(lambda: collections.defaultdict(list))
392 paths = collections.defaultdict(lambda: collections.defaultdict(list))
390 for co in comments:
393 for co in comments:
391 paths[co.f_path][co.line_no].append(co)
394 paths[co.f_path][co.line_no].append(co)
392 return paths
395 return paths
393
396
394 @classmethod
397 @classmethod
395 def needed_extra_diff_context(cls):
398 def needed_extra_diff_context(cls):
396 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
399 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
397
400
398 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
401 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
399 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
402 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
400 return
403 return
401
404
402 comments = self._visible_inline_comments_of_pull_request(pull_request)
405 comments = self._visible_inline_comments_of_pull_request(pull_request)
403 comments_to_outdate = comments.all()
406 comments_to_outdate = comments.all()
404
407
405 for comment in comments_to_outdate:
408 for comment in comments_to_outdate:
406 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
409 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
407
410
408 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
411 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
409 diff_line = _parse_comment_line_number(comment.line_no)
412 diff_line = _parse_comment_line_number(comment.line_no)
410
413
411 try:
414 try:
412 old_context = old_diff_proc.get_context_of_line(
415 old_context = old_diff_proc.get_context_of_line(
413 path=comment.f_path, diff_line=diff_line)
416 path=comment.f_path, diff_line=diff_line)
414 new_context = new_diff_proc.get_context_of_line(
417 new_context = new_diff_proc.get_context_of_line(
415 path=comment.f_path, diff_line=diff_line)
418 path=comment.f_path, diff_line=diff_line)
416 except (diffs.LineNotInDiffException,
419 except (diffs.LineNotInDiffException,
417 diffs.FileNotInDiffException):
420 diffs.FileNotInDiffException):
418 comment.display_state = ChangesetComment.COMMENT_OUTDATED
421 comment.display_state = ChangesetComment.COMMENT_OUTDATED
419 return
422 return
420
423
421 if old_context == new_context:
424 if old_context == new_context:
422 return
425 return
423
426
424 if self._should_relocate_diff_line(diff_line):
427 if self._should_relocate_diff_line(diff_line):
425 new_diff_lines = new_diff_proc.find_context(
428 new_diff_lines = new_diff_proc.find_context(
426 path=comment.f_path, context=old_context,
429 path=comment.f_path, context=old_context,
427 offset=self.DIFF_CONTEXT_BEFORE)
430 offset=self.DIFF_CONTEXT_BEFORE)
428 if not new_diff_lines:
431 if not new_diff_lines:
429 comment.display_state = ChangesetComment.COMMENT_OUTDATED
432 comment.display_state = ChangesetComment.COMMENT_OUTDATED
430 else:
433 else:
431 new_diff_line = self._choose_closest_diff_line(
434 new_diff_line = self._choose_closest_diff_line(
432 diff_line, new_diff_lines)
435 diff_line, new_diff_lines)
433 comment.line_no = _diff_to_comment_line_number(new_diff_line)
436 comment.line_no = _diff_to_comment_line_number(new_diff_line)
434 else:
437 else:
435 comment.display_state = ChangesetComment.COMMENT_OUTDATED
438 comment.display_state = ChangesetComment.COMMENT_OUTDATED
436
439
437 def _should_relocate_diff_line(self, diff_line):
440 def _should_relocate_diff_line(self, diff_line):
438 """
441 """
439 Checks if relocation shall be tried for the given `diff_line`.
442 Checks if relocation shall be tried for the given `diff_line`.
440
443
441 If a comment points into the first lines, then we can have a situation
444 If a comment points into the first lines, then we can have a situation
442 that after an update another line has been added on top. In this case
445 that after an update another line has been added on top. In this case
443 we would find the context still and move the comment around. This
446 we would find the context still and move the comment around. This
444 would be wrong.
447 would be wrong.
445 """
448 """
446 should_relocate = (
449 should_relocate = (
447 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
450 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
448 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
451 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
449 return should_relocate
452 return should_relocate
450
453
451 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
454 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
452 candidate = new_diff_lines[0]
455 candidate = new_diff_lines[0]
453 best_delta = _diff_line_delta(diff_line, candidate)
456 best_delta = _diff_line_delta(diff_line, candidate)
454 for new_diff_line in new_diff_lines[1:]:
457 for new_diff_line in new_diff_lines[1:]:
455 delta = _diff_line_delta(diff_line, new_diff_line)
458 delta = _diff_line_delta(diff_line, new_diff_line)
456 if delta < best_delta:
459 if delta < best_delta:
457 candidate = new_diff_line
460 candidate = new_diff_line
458 best_delta = delta
461 best_delta = delta
459 return candidate
462 return candidate
460
463
461 def _visible_inline_comments_of_pull_request(self, pull_request):
464 def _visible_inline_comments_of_pull_request(self, pull_request):
462 comments = self._all_inline_comments_of_pull_request(pull_request)
465 comments = self._all_inline_comments_of_pull_request(pull_request)
463 comments = comments.filter(
466 comments = comments.filter(
464 coalesce(ChangesetComment.display_state, '') !=
467 coalesce(ChangesetComment.display_state, '') !=
465 ChangesetComment.COMMENT_OUTDATED)
468 ChangesetComment.COMMENT_OUTDATED)
466 return comments
469 return comments
467
470
468 def _all_inline_comments_of_pull_request(self, pull_request):
471 def _all_inline_comments_of_pull_request(self, pull_request):
469 comments = Session().query(ChangesetComment)\
472 comments = Session().query(ChangesetComment)\
470 .filter(ChangesetComment.line_no != None)\
473 .filter(ChangesetComment.line_no != None)\
471 .filter(ChangesetComment.f_path != None)\
474 .filter(ChangesetComment.f_path != None)\
472 .filter(ChangesetComment.pull_request == pull_request)
475 .filter(ChangesetComment.pull_request == pull_request)
473 return comments
476 return comments
474
477
475 @staticmethod
478 @staticmethod
476 def use_outdated_comments(pull_request):
479 def use_outdated_comments(pull_request):
477 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
480 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
478 settings = settings_model.get_general_settings()
481 settings = settings_model.get_general_settings()
479 return settings.get('rhodecode_use_outdated_comments', False)
482 return settings.get('rhodecode_use_outdated_comments', False)
480
483
481
484
482 def _parse_comment_line_number(line_no):
485 def _parse_comment_line_number(line_no):
483 """
486 """
484 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
487 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
485 """
488 """
486 old_line = None
489 old_line = None
487 new_line = None
490 new_line = None
488 if line_no.startswith('o'):
491 if line_no.startswith('o'):
489 old_line = int(line_no[1:])
492 old_line = int(line_no[1:])
490 elif line_no.startswith('n'):
493 elif line_no.startswith('n'):
491 new_line = int(line_no[1:])
494 new_line = int(line_no[1:])
492 else:
495 else:
493 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
496 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
494 return diffs.DiffLineNumber(old_line, new_line)
497 return diffs.DiffLineNumber(old_line, new_line)
495
498
496
499
497 def _diff_to_comment_line_number(diff_line):
500 def _diff_to_comment_line_number(diff_line):
498 if diff_line.new is not None:
501 if diff_line.new is not None:
499 return u'n{}'.format(diff_line.new)
502 return u'n{}'.format(diff_line.new)
500 elif diff_line.old is not None:
503 elif diff_line.old is not None:
501 return u'o{}'.format(diff_line.old)
504 return u'o{}'.format(diff_line.old)
502 return u''
505 return u''
503
506
504
507
505 def _diff_line_delta(a, b):
508 def _diff_line_delta(a, b):
506 if None not in (a.new, b.new):
509 if None not in (a.new, b.new):
507 return abs(a.new - b.new)
510 return abs(a.new - b.new)
508 elif None not in (a.old, b.old):
511 elif None not in (a.old, b.old):
509 return abs(a.old - b.old)
512 return abs(a.old - b.old)
510 else:
513 else:
511 raise ValueError(
514 raise ValueError(
512 "Cannot compute delta between {} and {}".format(a, b))
515 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1154 +1,1155 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.i18n.translation import lazy_ugettext
32 from pylons.i18n.translation import lazy_ugettext
33
33
34 import rhodecode
34 import rhodecode
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.markup_renderer import (
38 from rhodecode.lib.markup_renderer import (
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 from rhodecode.lib.utils import action_logger
40 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.vcs.backends.base import (
42 from rhodecode.lib.vcs.backends.base import (
43 Reference, MergeResponse, MergeFailureReason)
43 Reference, MergeResponse, MergeFailureReason)
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, EmptyRepositoryError)
45 CommitDoesNotExistError, EmptyRepositoryError)
46 from rhodecode.model import BaseModel
46 from rhodecode.model import BaseModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 PullRequest, PullRequestReviewers, Notification, ChangesetStatus,
50 PullRequest, PullRequestReviewers, Notification, ChangesetStatus,
51 PullRequestVersion, ChangesetComment)
51 PullRequestVersion, ChangesetComment)
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.notification import NotificationModel, \
53 from rhodecode.model.notification import NotificationModel, \
54 EmailNotificationModel
54 EmailNotificationModel
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.settings import VcsSettingsModel
56 from rhodecode.model.settings import VcsSettingsModel
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class PullRequestModel(BaseModel):
62 class PullRequestModel(BaseModel):
63
63
64 cls = PullRequest
64 cls = PullRequest
65
65
66 DIFF_CONTEXT = 3
66 DIFF_CONTEXT = 3
67
67
68 MERGE_STATUS_MESSAGES = {
68 MERGE_STATUS_MESSAGES = {
69 MergeFailureReason.NONE: lazy_ugettext(
69 MergeFailureReason.NONE: lazy_ugettext(
70 'This pull request can be automatically merged.'),
70 'This pull request can be automatically merged.'),
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 'This pull request cannot be merged because of an unhandled'
72 'This pull request cannot be merged because of an unhandled'
73 ' exception.'),
73 ' exception.'),
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 'This pull request cannot be merged because of conflicts.'),
75 'This pull request cannot be merged because of conflicts.'),
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 'This pull request could not be merged because push to target'
77 'This pull request could not be merged because push to target'
78 ' failed.'),
78 ' failed.'),
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 'This pull request cannot be merged because the target is not a'
80 'This pull request cannot be merged because the target is not a'
81 ' head.'),
81 ' head.'),
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 'This pull request cannot be merged because the source contains'
83 'This pull request cannot be merged because the source contains'
84 ' more branches than the target.'),
84 ' more branches than the target.'),
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 'This pull request cannot be merged because the target has'
86 'This pull request cannot be merged because the target has'
87 ' multiple heads.'),
87 ' multiple heads.'),
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 'This pull request cannot be merged because the target repository'
89 'This pull request cannot be merged because the target repository'
90 ' is locked.'),
90 ' is locked.'),
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 'This pull request cannot be merged because the target or the '
92 'This pull request cannot be merged because the target or the '
93 'source reference is missing.'),
93 'source reference is missing.'),
94 }
94 }
95
95
96 def __get_pull_request(self, pull_request):
96 def __get_pull_request(self, pull_request):
97 return self._get_instance(PullRequest, pull_request)
97 return self._get_instance(PullRequest, pull_request)
98
98
99 def _check_perms(self, perms, pull_request, user, api=False):
99 def _check_perms(self, perms, pull_request, user, api=False):
100 if not api:
100 if not api:
101 return h.HasRepoPermissionAny(*perms)(
101 return h.HasRepoPermissionAny(*perms)(
102 user=user, repo_name=pull_request.target_repo.repo_name)
102 user=user, repo_name=pull_request.target_repo.repo_name)
103 else:
103 else:
104 return h.HasRepoPermissionAnyApi(*perms)(
104 return h.HasRepoPermissionAnyApi(*perms)(
105 user=user, repo_name=pull_request.target_repo.repo_name)
105 user=user, repo_name=pull_request.target_repo.repo_name)
106
106
107 def check_user_read(self, pull_request, user, api=False):
107 def check_user_read(self, pull_request, user, api=False):
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 return self._check_perms(_perms, pull_request, user, api)
109 return self._check_perms(_perms, pull_request, user, api)
110
110
111 def check_user_merge(self, pull_request, user, api=False):
111 def check_user_merge(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_update(self, pull_request, user, api=False):
115 def check_user_update(self, pull_request, user, api=False):
116 owner = user.user_id == pull_request.user_id
116 owner = user.user_id == pull_request.user_id
117 return self.check_user_merge(pull_request, user, api) or owner
117 return self.check_user_merge(pull_request, user, api) or owner
118
118
119 def check_user_change_status(self, pull_request, user, api=False):
119 def check_user_change_status(self, pull_request, user, api=False):
120 reviewer = user.user_id in [x.user_id for x in
120 reviewer = user.user_id in [x.user_id for x in
121 pull_request.reviewers]
121 pull_request.reviewers]
122 return self.check_user_update(pull_request, user, api) or reviewer
122 return self.check_user_update(pull_request, user, api) or reviewer
123
123
124 def get(self, pull_request):
124 def get(self, pull_request):
125 return self.__get_pull_request(pull_request)
125 return self.__get_pull_request(pull_request)
126
126
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 opened_by=None, order_by=None,
128 opened_by=None, order_by=None,
129 order_dir='desc'):
129 order_dir='desc'):
130 repo = self._get_repo(repo_name)
130 repo = self._get_repo(repo_name)
131 q = PullRequest.query()
131 q = PullRequest.query()
132 # source or target
132 # source or target
133 if source:
133 if source:
134 q = q.filter(PullRequest.source_repo == repo)
134 q = q.filter(PullRequest.source_repo == repo)
135 else:
135 else:
136 q = q.filter(PullRequest.target_repo == repo)
136 q = q.filter(PullRequest.target_repo == repo)
137
137
138 # closed,opened
138 # closed,opened
139 if statuses:
139 if statuses:
140 q = q.filter(PullRequest.status.in_(statuses))
140 q = q.filter(PullRequest.status.in_(statuses))
141
141
142 # opened by filter
142 # opened by filter
143 if opened_by:
143 if opened_by:
144 q = q.filter(PullRequest.user_id.in_(opened_by))
144 q = q.filter(PullRequest.user_id.in_(opened_by))
145
145
146 if order_by:
146 if order_by:
147 order_map = {
147 order_map = {
148 'name_raw': PullRequest.pull_request_id,
148 'name_raw': PullRequest.pull_request_id,
149 'title': PullRequest.title,
149 'title': PullRequest.title,
150 'updated_on_raw': PullRequest.updated_on
150 'updated_on_raw': PullRequest.updated_on
151 }
151 }
152 if order_dir == 'asc':
152 if order_dir == 'asc':
153 q = q.order_by(order_map[order_by].asc())
153 q = q.order_by(order_map[order_by].asc())
154 else:
154 else:
155 q = q.order_by(order_map[order_by].desc())
155 q = q.order_by(order_map[order_by].desc())
156
156
157 return q
157 return q
158
158
159 def count_all(self, repo_name, source=False, statuses=None,
159 def count_all(self, repo_name, source=False, statuses=None,
160 opened_by=None):
160 opened_by=None):
161 """
161 """
162 Count the number of pull requests for a specific repository.
162 Count the number of pull requests for a specific repository.
163
163
164 :param repo_name: target or source repo
164 :param repo_name: target or source repo
165 :param source: boolean flag to specify if repo_name refers to source
165 :param source: boolean flag to specify if repo_name refers to source
166 :param statuses: list of pull request statuses
166 :param statuses: list of pull request statuses
167 :param opened_by: author user of the pull request
167 :param opened_by: author user of the pull request
168 :returns: int number of pull requests
168 :returns: int number of pull requests
169 """
169 """
170 q = self._prepare_get_all_query(
170 q = self._prepare_get_all_query(
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172
172
173 return q.count()
173 return q.count()
174
174
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 offset=0, length=None, order_by=None, order_dir='desc'):
176 offset=0, length=None, order_by=None, order_dir='desc'):
177 """
177 """
178 Get all pull requests for a specific repository.
178 Get all pull requests for a specific repository.
179
179
180 :param repo_name: target or source repo
180 :param repo_name: target or source repo
181 :param source: boolean flag to specify if repo_name refers to source
181 :param source: boolean flag to specify if repo_name refers to source
182 :param statuses: list of pull request statuses
182 :param statuses: list of pull request statuses
183 :param opened_by: author user of the pull request
183 :param opened_by: author user of the pull request
184 :param offset: pagination offset
184 :param offset: pagination offset
185 :param length: length of returned list
185 :param length: length of returned list
186 :param order_by: order of the returned list
186 :param order_by: order of the returned list
187 :param order_dir: 'asc' or 'desc' ordering direction
187 :param order_dir: 'asc' or 'desc' ordering direction
188 :returns: list of pull requests
188 :returns: list of pull requests
189 """
189 """
190 q = self._prepare_get_all_query(
190 q = self._prepare_get_all_query(
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 order_by=order_by, order_dir=order_dir)
192 order_by=order_by, order_dir=order_dir)
193
193
194 if length:
194 if length:
195 pull_requests = q.limit(length).offset(offset).all()
195 pull_requests = q.limit(length).offset(offset).all()
196 else:
196 else:
197 pull_requests = q.all()
197 pull_requests = q.all()
198
198
199 return pull_requests
199 return pull_requests
200
200
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 opened_by=None):
202 opened_by=None):
203 """
203 """
204 Count the number of pull requests for a specific repository that are
204 Count the number of pull requests for a specific repository that are
205 awaiting review.
205 awaiting review.
206
206
207 :param repo_name: target or source repo
207 :param repo_name: target or source repo
208 :param source: boolean flag to specify if repo_name refers to source
208 :param source: boolean flag to specify if repo_name refers to source
209 :param statuses: list of pull request statuses
209 :param statuses: list of pull request statuses
210 :param opened_by: author user of the pull request
210 :param opened_by: author user of the pull request
211 :returns: int number of pull requests
211 :returns: int number of pull requests
212 """
212 """
213 pull_requests = self.get_awaiting_review(
213 pull_requests = self.get_awaiting_review(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215
215
216 return len(pull_requests)
216 return len(pull_requests)
217
217
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 opened_by=None, offset=0, length=None,
219 opened_by=None, offset=0, length=None,
220 order_by=None, order_dir='desc'):
220 order_by=None, order_dir='desc'):
221 """
221 """
222 Get all pull requests for a specific repository that are awaiting
222 Get all pull requests for a specific repository that are awaiting
223 review.
223 review.
224
224
225 :param repo_name: target or source repo
225 :param repo_name: target or source repo
226 :param source: boolean flag to specify if repo_name refers to source
226 :param source: boolean flag to specify if repo_name refers to source
227 :param statuses: list of pull request statuses
227 :param statuses: list of pull request statuses
228 :param opened_by: author user of the pull request
228 :param opened_by: author user of the pull request
229 :param offset: pagination offset
229 :param offset: pagination offset
230 :param length: length of returned list
230 :param length: length of returned list
231 :param order_by: order of the returned list
231 :param order_by: order of the returned list
232 :param order_dir: 'asc' or 'desc' ordering direction
232 :param order_dir: 'asc' or 'desc' ordering direction
233 :returns: list of pull requests
233 :returns: list of pull requests
234 """
234 """
235 pull_requests = self.get_all(
235 pull_requests = self.get_all(
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 order_by=order_by, order_dir=order_dir)
237 order_by=order_by, order_dir=order_dir)
238
238
239 _filtered_pull_requests = []
239 _filtered_pull_requests = []
240 for pr in pull_requests:
240 for pr in pull_requests:
241 status = pr.calculated_review_status()
241 status = pr.calculated_review_status()
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 _filtered_pull_requests.append(pr)
244 _filtered_pull_requests.append(pr)
245 if length:
245 if length:
246 return _filtered_pull_requests[offset:offset+length]
246 return _filtered_pull_requests[offset:offset+length]
247 else:
247 else:
248 return _filtered_pull_requests
248 return _filtered_pull_requests
249
249
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 opened_by=None, user_id=None):
251 opened_by=None, user_id=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review from a specific user.
254 awaiting review from a specific user.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :param user_id: reviewer user of the pull request
260 :param user_id: reviewer user of the pull request
261 :returns: int number of pull requests
261 :returns: int number of pull requests
262 """
262 """
263 pull_requests = self.get_awaiting_my_review(
263 pull_requests = self.get_awaiting_my_review(
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 user_id=user_id)
265 user_id=user_id)
266
266
267 return len(pull_requests)
267 return len(pull_requests)
268
268
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 opened_by=None, user_id=None, offset=0,
270 opened_by=None, user_id=None, offset=0,
271 length=None, order_by=None, order_dir='desc'):
271 length=None, order_by=None, order_dir='desc'):
272 """
272 """
273 Get all pull requests for a specific repository that are awaiting
273 Get all pull requests for a specific repository that are awaiting
274 review from a specific user.
274 review from a specific user.
275
275
276 :param repo_name: target or source repo
276 :param repo_name: target or source repo
277 :param source: boolean flag to specify if repo_name refers to source
277 :param source: boolean flag to specify if repo_name refers to source
278 :param statuses: list of pull request statuses
278 :param statuses: list of pull request statuses
279 :param opened_by: author user of the pull request
279 :param opened_by: author user of the pull request
280 :param user_id: reviewer user of the pull request
280 :param user_id: reviewer user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _my = PullRequestModel().get_not_reviewed(user_id)
291 _my = PullRequestModel().get_not_reviewed(user_id)
292 my_participation = []
292 my_participation = []
293 for pr in pull_requests:
293 for pr in pull_requests:
294 if pr in _my:
294 if pr in _my:
295 my_participation.append(pr)
295 my_participation.append(pr)
296 _filtered_pull_requests = my_participation
296 _filtered_pull_requests = my_participation
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def get_not_reviewed(self, user_id):
302 def get_not_reviewed(self, user_id):
303 return [
303 return [
304 x.pull_request for x in PullRequestReviewers.query().filter(
304 x.pull_request for x in PullRequestReviewers.query().filter(
305 PullRequestReviewers.user_id == user_id).all()
305 PullRequestReviewers.user_id == user_id).all()
306 ]
306 ]
307
307
308 def get_versions(self, pull_request):
308 def get_versions(self, pull_request):
309 """
309 """
310 returns version of pull request sorted by ID descending
310 returns version of pull request sorted by ID descending
311 """
311 """
312 return PullRequestVersion.query()\
312 return PullRequestVersion.query()\
313 .filter(PullRequestVersion.pull_request == pull_request)\
313 .filter(PullRequestVersion.pull_request == pull_request)\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 .all()
315 .all()
316
316
317 def create(self, created_by, source_repo, source_ref, target_repo,
317 def create(self, created_by, source_repo, source_ref, target_repo,
318 target_ref, revisions, reviewers, title, description=None):
318 target_ref, revisions, reviewers, title, description=None):
319 created_by_user = self._get_user(created_by)
319 created_by_user = self._get_user(created_by)
320 source_repo = self._get_repo(source_repo)
320 source_repo = self._get_repo(source_repo)
321 target_repo = self._get_repo(target_repo)
321 target_repo = self._get_repo(target_repo)
322
322
323 pull_request = PullRequest()
323 pull_request = PullRequest()
324 pull_request.source_repo = source_repo
324 pull_request.source_repo = source_repo
325 pull_request.source_ref = source_ref
325 pull_request.source_ref = source_ref
326 pull_request.target_repo = target_repo
326 pull_request.target_repo = target_repo
327 pull_request.target_ref = target_ref
327 pull_request.target_ref = target_ref
328 pull_request.revisions = revisions
328 pull_request.revisions = revisions
329 pull_request.title = title
329 pull_request.title = title
330 pull_request.description = description
330 pull_request.description = description
331 pull_request.author = created_by_user
331 pull_request.author = created_by_user
332
332
333 Session().add(pull_request)
333 Session().add(pull_request)
334 Session().flush()
334 Session().flush()
335
335
336 # members / reviewers
336 # members / reviewers
337 for user_id in set(reviewers):
337 for user_id in set(reviewers):
338 user = self._get_user(user_id)
338 user = self._get_user(user_id)
339 reviewer = PullRequestReviewers(user, pull_request)
339 reviewer = PullRequestReviewers(user, pull_request)
340 Session().add(reviewer)
340 Session().add(reviewer)
341
341
342 # Set approval status to "Under Review" for all commits which are
342 # Set approval status to "Under Review" for all commits which are
343 # part of this pull request.
343 # part of this pull request.
344 ChangesetStatusModel().set_status(
344 ChangesetStatusModel().set_status(
345 repo=target_repo,
345 repo=target_repo,
346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
347 user=created_by_user,
347 user=created_by_user,
348 pull_request=pull_request
348 pull_request=pull_request
349 )
349 )
350
350
351 self.notify_reviewers(pull_request, reviewers)
351 self.notify_reviewers(pull_request, reviewers)
352 self._trigger_pull_request_hook(
352 self._trigger_pull_request_hook(
353 pull_request, created_by_user, 'create')
353 pull_request, created_by_user, 'create')
354
354
355 return pull_request
355 return pull_request
356
356
357 def _trigger_pull_request_hook(self, pull_request, user, action):
357 def _trigger_pull_request_hook(self, pull_request, user, action):
358 pull_request = self.__get_pull_request(pull_request)
358 pull_request = self.__get_pull_request(pull_request)
359 target_scm = pull_request.target_repo.scm_instance()
359 target_scm = pull_request.target_repo.scm_instance()
360 if action == 'create':
360 if action == 'create':
361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
362 elif action == 'merge':
362 elif action == 'merge':
363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
364 elif action == 'close':
364 elif action == 'close':
365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
366 elif action == 'review_status_change':
366 elif action == 'review_status_change':
367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
368 elif action == 'update':
368 elif action == 'update':
369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
370 else:
370 else:
371 return
371 return
372
372
373 trigger_hook(
373 trigger_hook(
374 username=user.username,
374 username=user.username,
375 repo_name=pull_request.target_repo.repo_name,
375 repo_name=pull_request.target_repo.repo_name,
376 repo_alias=target_scm.alias,
376 repo_alias=target_scm.alias,
377 pull_request=pull_request)
377 pull_request=pull_request)
378
378
379 def _get_commit_ids(self, pull_request):
379 def _get_commit_ids(self, pull_request):
380 """
380 """
381 Return the commit ids of the merged pull request.
381 Return the commit ids of the merged pull request.
382
382
383 This method is not dealing correctly yet with the lack of autoupdates
383 This method is not dealing correctly yet with the lack of autoupdates
384 nor with the implicit target updates.
384 nor with the implicit target updates.
385 For example: if a commit in the source repo is already in the target it
385 For example: if a commit in the source repo is already in the target it
386 will be reported anyways.
386 will be reported anyways.
387 """
387 """
388 merge_rev = pull_request.merge_rev
388 merge_rev = pull_request.merge_rev
389 if merge_rev is None:
389 if merge_rev is None:
390 raise ValueError('This pull request was not merged yet')
390 raise ValueError('This pull request was not merged yet')
391
391
392 commit_ids = list(pull_request.revisions)
392 commit_ids = list(pull_request.revisions)
393 if merge_rev not in commit_ids:
393 if merge_rev not in commit_ids:
394 commit_ids.append(merge_rev)
394 commit_ids.append(merge_rev)
395
395
396 return commit_ids
396 return commit_ids
397
397
398 def merge(self, pull_request, user, extras):
398 def merge(self, pull_request, user, extras):
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
401 if merge_state.executed:
401 if merge_state.executed:
402 log.debug(
402 log.debug(
403 "Merge was successful, updating the pull request comments.")
403 "Merge was successful, updating the pull request comments.")
404 self._comment_and_close_pr(pull_request, user, merge_state)
404 self._comment_and_close_pr(pull_request, user, merge_state)
405 self._log_action('user_merged_pull_request', user, pull_request)
405 self._log_action('user_merged_pull_request', user, pull_request)
406 else:
406 else:
407 log.warn("Merge failed, not updating the pull request.")
407 log.warn("Merge failed, not updating the pull request.")
408 return merge_state
408 return merge_state
409
409
410 def _merge_pull_request(self, pull_request, user, extras):
410 def _merge_pull_request(self, pull_request, user, extras):
411 target_vcs = pull_request.target_repo.scm_instance()
411 target_vcs = pull_request.target_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
413 target_ref = self._refresh_reference(
413 target_ref = self._refresh_reference(
414 pull_request.target_ref_parts, target_vcs)
414 pull_request.target_ref_parts, target_vcs)
415
415
416 message = _(
416 message = _(
417 'Merge pull request #%(pr_id)s from '
417 'Merge pull request #%(pr_id)s from '
418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
419 'pr_id': pull_request.pull_request_id,
419 'pr_id': pull_request.pull_request_id,
420 'source_repo': source_vcs.name,
420 'source_repo': source_vcs.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
422 'pr_title': pull_request.title
422 'pr_title': pull_request.title
423 }
423 }
424
424
425 workspace_id = self._workspace_id(pull_request)
425 workspace_id = self._workspace_id(pull_request)
426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
428 use_rebase = self._use_rebase_for_merging(pull_request)
428 use_rebase = self._use_rebase_for_merging(pull_request)
429
429
430 callback_daemon, extras = prepare_callback_daemon(
430 callback_daemon, extras = prepare_callback_daemon(
431 extras, protocol=protocol, use_direct_calls=use_direct_calls)
431 extras, protocol=protocol, use_direct_calls=use_direct_calls)
432
432
433 with callback_daemon:
433 with callback_daemon:
434 # TODO: johbo: Implement a clean way to run a config_override
434 # TODO: johbo: Implement a clean way to run a config_override
435 # for a single call.
435 # for a single call.
436 target_vcs.config.set(
436 target_vcs.config.set(
437 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
437 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
438 merge_state = target_vcs.merge(
438 merge_state = target_vcs.merge(
439 target_ref, source_vcs, pull_request.source_ref_parts,
439 target_ref, source_vcs, pull_request.source_ref_parts,
440 workspace_id, user_name=user.username,
440 workspace_id, user_name=user.username,
441 user_email=user.email, message=message, use_rebase=use_rebase)
441 user_email=user.email, message=message, use_rebase=use_rebase)
442 return merge_state
442 return merge_state
443
443
444 def _comment_and_close_pr(self, pull_request, user, merge_state):
444 def _comment_and_close_pr(self, pull_request, user, merge_state):
445 pull_request.merge_rev = merge_state.merge_commit_id
445 pull_request.merge_rev = merge_state.merge_commit_id
446 pull_request.updated_on = datetime.datetime.now()
446 pull_request.updated_on = datetime.datetime.now()
447
447
448 ChangesetCommentsModel().create(
448 ChangesetCommentsModel().create(
449 text=unicode(_('Pull request merged and closed')),
449 text=unicode(_('Pull request merged and closed')),
450 repo=pull_request.target_repo.repo_id,
450 repo=pull_request.target_repo.repo_id,
451 user=user.user_id,
451 user=user.user_id,
452 pull_request=pull_request.pull_request_id,
452 pull_request=pull_request.pull_request_id,
453 f_path=None,
453 f_path=None,
454 line_no=None,
454 line_no=None,
455 closing_pr=True
455 closing_pr=True
456 )
456 )
457
457
458 Session().add(pull_request)
458 Session().add(pull_request)
459 Session().flush()
459 Session().flush()
460 # TODO: paris: replace invalidation with less radical solution
460 # TODO: paris: replace invalidation with less radical solution
461 ScmModel().mark_for_invalidation(
461 ScmModel().mark_for_invalidation(
462 pull_request.target_repo.repo_name)
462 pull_request.target_repo.repo_name)
463 self._trigger_pull_request_hook(pull_request, user, 'merge')
463 self._trigger_pull_request_hook(pull_request, user, 'merge')
464
464
465 def has_valid_update_type(self, pull_request):
465 def has_valid_update_type(self, pull_request):
466 source_ref_type = pull_request.source_ref_parts.type
466 source_ref_type = pull_request.source_ref_parts.type
467 return source_ref_type in ['book', 'branch', 'tag']
467 return source_ref_type in ['book', 'branch', 'tag']
468
468
469 def update_commits(self, pull_request):
469 def update_commits(self, pull_request):
470 """
470 """
471 Get the updated list of commits for the pull request
471 Get the updated list of commits for the pull request
472 and return the new pull request version and the list
472 and return the new pull request version and the list
473 of commits processed by this update action
473 of commits processed by this update action
474 """
474 """
475
475
476 pull_request = self.__get_pull_request(pull_request)
476 pull_request = self.__get_pull_request(pull_request)
477 source_ref_type = pull_request.source_ref_parts.type
477 source_ref_type = pull_request.source_ref_parts.type
478 source_ref_name = pull_request.source_ref_parts.name
478 source_ref_name = pull_request.source_ref_parts.name
479 source_ref_id = pull_request.source_ref_parts.commit_id
479 source_ref_id = pull_request.source_ref_parts.commit_id
480
480
481 if not self.has_valid_update_type(pull_request):
481 if not self.has_valid_update_type(pull_request):
482 log.debug(
482 log.debug(
483 "Skipping update of pull request %s due to ref type: %s",
483 "Skipping update of pull request %s due to ref type: %s",
484 pull_request, source_ref_type)
484 pull_request, source_ref_type)
485 return (None, None)
485 return (None, None)
486
486
487 source_repo = pull_request.source_repo.scm_instance()
487 source_repo = pull_request.source_repo.scm_instance()
488 source_commit = source_repo.get_commit(commit_id=source_ref_name)
488 source_commit = source_repo.get_commit(commit_id=source_ref_name)
489 if source_ref_id == source_commit.raw_id:
489 if source_ref_id == source_commit.raw_id:
490 log.debug("Nothing changed in pull request %s", pull_request)
490 log.debug("Nothing changed in pull request %s", pull_request)
491 return (None, None)
491 return (None, None)
492
492
493 # Finally there is a need for an update
493 # Finally there is a need for an update
494 pull_request_version = self._create_version_from_snapshot(pull_request)
494 pull_request_version = self._create_version_from_snapshot(pull_request)
495 self._link_comments_to_version(pull_request_version)
495 self._link_comments_to_version(pull_request_version)
496
496
497 target_ref_type = pull_request.target_ref_parts.type
497 target_ref_type = pull_request.target_ref_parts.type
498 target_ref_name = pull_request.target_ref_parts.name
498 target_ref_name = pull_request.target_ref_parts.name
499 target_ref_id = pull_request.target_ref_parts.commit_id
499 target_ref_id = pull_request.target_ref_parts.commit_id
500 target_repo = pull_request.target_repo.scm_instance()
500 target_repo = pull_request.target_repo.scm_instance()
501
501
502 if target_ref_type in ('tag', 'branch', 'book'):
502 if target_ref_type in ('tag', 'branch', 'book'):
503 target_commit = target_repo.get_commit(target_ref_name)
503 target_commit = target_repo.get_commit(target_ref_name)
504 else:
504 else:
505 target_commit = target_repo.get_commit(target_ref_id)
505 target_commit = target_repo.get_commit(target_ref_id)
506
506
507 # re-compute commit ids
507 # re-compute commit ids
508 old_commit_ids = set(pull_request.revisions)
508 old_commit_ids = set(pull_request.revisions)
509 pre_load = ["author", "branch", "date", "message"]
509 pre_load = ["author", "branch", "date", "message"]
510 commit_ranges = target_repo.compare(
510 commit_ranges = target_repo.compare(
511 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
511 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
512 pre_load=pre_load)
512 pre_load=pre_load)
513
513
514 ancestor = target_repo.get_common_ancestor(
514 ancestor = target_repo.get_common_ancestor(
515 target_commit.raw_id, source_commit.raw_id, source_repo)
515 target_commit.raw_id, source_commit.raw_id, source_repo)
516
516
517 pull_request.source_ref = '%s:%s:%s' % (
517 pull_request.source_ref = '%s:%s:%s' % (
518 source_ref_type, source_ref_name, source_commit.raw_id)
518 source_ref_type, source_ref_name, source_commit.raw_id)
519 pull_request.target_ref = '%s:%s:%s' % (
519 pull_request.target_ref = '%s:%s:%s' % (
520 target_ref_type, target_ref_name, ancestor)
520 target_ref_type, target_ref_name, ancestor)
521 pull_request.revisions = [
521 pull_request.revisions = [
522 commit.raw_id for commit in reversed(commit_ranges)]
522 commit.raw_id for commit in reversed(commit_ranges)]
523 pull_request.updated_on = datetime.datetime.now()
523 pull_request.updated_on = datetime.datetime.now()
524 Session().add(pull_request)
524 Session().add(pull_request)
525 new_commit_ids = set(pull_request.revisions)
525 new_commit_ids = set(pull_request.revisions)
526
526
527 changes = self._calculate_commit_id_changes(
527 changes = self._calculate_commit_id_changes(
528 old_commit_ids, new_commit_ids)
528 old_commit_ids, new_commit_ids)
529
529
530 old_diff_data, new_diff_data = self._generate_update_diffs(
530 old_diff_data, new_diff_data = self._generate_update_diffs(
531 pull_request, pull_request_version)
531 pull_request, pull_request_version)
532
532
533 ChangesetCommentsModel().outdate_comments(
533 ChangesetCommentsModel().outdate_comments(
534 pull_request, old_diff_data=old_diff_data,
534 pull_request, old_diff_data=old_diff_data,
535 new_diff_data=new_diff_data)
535 new_diff_data=new_diff_data)
536
536
537 file_changes = self._calculate_file_changes(
537 file_changes = self._calculate_file_changes(
538 old_diff_data, new_diff_data)
538 old_diff_data, new_diff_data)
539
539
540 # Add an automatic comment to the pull request
540 # Add an automatic comment to the pull request
541 update_comment = ChangesetCommentsModel().create(
541 update_comment = ChangesetCommentsModel().create(
542 text=self._render_update_message(changes, file_changes),
542 text=self._render_update_message(changes, file_changes),
543 repo=pull_request.target_repo,
543 repo=pull_request.target_repo,
544 user=pull_request.author,
544 user=pull_request.author,
545 pull_request=pull_request,
545 pull_request=pull_request,
546 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
546 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
547
547
548 # Update status to "Under Review" for added commits
548 # Update status to "Under Review" for added commits
549 for commit_id in changes.added:
549 for commit_id in changes.added:
550 ChangesetStatusModel().set_status(
550 ChangesetStatusModel().set_status(
551 repo=pull_request.source_repo,
551 repo=pull_request.source_repo,
552 status=ChangesetStatus.STATUS_UNDER_REVIEW,
552 status=ChangesetStatus.STATUS_UNDER_REVIEW,
553 comment=update_comment,
553 comment=update_comment,
554 user=pull_request.author,
554 user=pull_request.author,
555 pull_request=pull_request,
555 pull_request=pull_request,
556 revision=commit_id)
556 revision=commit_id)
557
557
558 log.debug(
558 log.debug(
559 'Updated pull request %s, added_ids: %s, common_ids: %s, '
559 'Updated pull request %s, added_ids: %s, common_ids: %s, '
560 'removed_ids: %s', pull_request.pull_request_id,
560 'removed_ids: %s', pull_request.pull_request_id,
561 changes.added, changes.common, changes.removed)
561 changes.added, changes.common, changes.removed)
562 log.debug('Updated pull request with the following file changes: %s',
562 log.debug('Updated pull request with the following file changes: %s',
563 file_changes)
563 file_changes)
564
564
565 log.info(
565 log.info(
566 "Updated pull request %s from commit %s to commit %s, "
566 "Updated pull request %s from commit %s to commit %s, "
567 "stored new version %s of this pull request.",
567 "stored new version %s of this pull request.",
568 pull_request.pull_request_id, source_ref_id,
568 pull_request.pull_request_id, source_ref_id,
569 pull_request.source_ref_parts.commit_id,
569 pull_request.source_ref_parts.commit_id,
570 pull_request_version.pull_request_version_id)
570 pull_request_version.pull_request_version_id)
571 Session().commit()
571 Session().commit()
572 self._trigger_pull_request_hook(pull_request, pull_request.author,
572 self._trigger_pull_request_hook(pull_request, pull_request.author,
573 'update')
573 'update')
574 return (pull_request_version, changes)
574 return (pull_request_version, changes)
575
575
576 def _create_version_from_snapshot(self, pull_request):
576 def _create_version_from_snapshot(self, pull_request):
577 version = PullRequestVersion()
577 version = PullRequestVersion()
578 version.title = pull_request.title
578 version.title = pull_request.title
579 version.description = pull_request.description
579 version.description = pull_request.description
580 version.status = pull_request.status
580 version.status = pull_request.status
581 version.created_on = pull_request.created_on
581 version.created_on = pull_request.created_on
582 version.updated_on = pull_request.updated_on
582 version.updated_on = pull_request.updated_on
583 version.user_id = pull_request.user_id
583 version.user_id = pull_request.user_id
584 version.source_repo = pull_request.source_repo
584 version.source_repo = pull_request.source_repo
585 version.source_ref = pull_request.source_ref
585 version.source_ref = pull_request.source_ref
586 version.target_repo = pull_request.target_repo
586 version.target_repo = pull_request.target_repo
587 version.target_ref = pull_request.target_ref
587 version.target_ref = pull_request.target_ref
588
588
589 version._last_merge_source_rev = pull_request._last_merge_source_rev
589 version._last_merge_source_rev = pull_request._last_merge_source_rev
590 version._last_merge_target_rev = pull_request._last_merge_target_rev
590 version._last_merge_target_rev = pull_request._last_merge_target_rev
591 version._last_merge_status = pull_request._last_merge_status
591 version._last_merge_status = pull_request._last_merge_status
592 version.merge_rev = pull_request.merge_rev
592 version.merge_rev = pull_request.merge_rev
593
593
594 version.revisions = pull_request.revisions
594 version.revisions = pull_request.revisions
595 version.pull_request = pull_request
595 version.pull_request = pull_request
596 Session().add(version)
596 Session().add(version)
597 Session().flush()
597 Session().flush()
598
598
599 return version
599 return version
600
600
601 def _generate_update_diffs(self, pull_request, pull_request_version):
601 def _generate_update_diffs(self, pull_request, pull_request_version):
602 diff_context = (
602 diff_context = (
603 self.DIFF_CONTEXT +
603 self.DIFF_CONTEXT +
604 ChangesetCommentsModel.needed_extra_diff_context())
604 ChangesetCommentsModel.needed_extra_diff_context())
605 old_diff = self._get_diff_from_pr_or_version(
605 old_diff = self._get_diff_from_pr_or_version(
606 pull_request_version, context=diff_context)
606 pull_request_version, context=diff_context)
607 new_diff = self._get_diff_from_pr_or_version(
607 new_diff = self._get_diff_from_pr_or_version(
608 pull_request, context=diff_context)
608 pull_request, context=diff_context)
609
609
610 old_diff_data = diffs.DiffProcessor(old_diff)
610 old_diff_data = diffs.DiffProcessor(old_diff)
611 old_diff_data.prepare()
611 old_diff_data.prepare()
612 new_diff_data = diffs.DiffProcessor(new_diff)
612 new_diff_data = diffs.DiffProcessor(new_diff)
613 new_diff_data.prepare()
613 new_diff_data.prepare()
614
614
615 return old_diff_data, new_diff_data
615 return old_diff_data, new_diff_data
616
616
617 def _link_comments_to_version(self, pull_request_version):
617 def _link_comments_to_version(self, pull_request_version):
618 """
618 """
619 Link all unlinked comments of this pull request to the given version.
619 Link all unlinked comments of this pull request to the given version.
620
620
621 :param pull_request_version: The `PullRequestVersion` to which
621 :param pull_request_version: The `PullRequestVersion` to which
622 the comments shall be linked.
622 the comments shall be linked.
623
623
624 """
624 """
625 pull_request = pull_request_version.pull_request
625 pull_request = pull_request_version.pull_request
626 comments = ChangesetComment.query().filter(
626 comments = ChangesetComment.query().filter(
627 # TODO: johbo: Should we query for the repo at all here?
627 # TODO: johbo: Should we query for the repo at all here?
628 # Pending decision on how comments of PRs are to be related
628 # Pending decision on how comments of PRs are to be related
629 # to either the source repo, the target repo or no repo at all.
629 # to either the source repo, the target repo or no repo at all.
630 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
630 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
631 ChangesetComment.pull_request == pull_request,
631 ChangesetComment.pull_request == pull_request,
632 ChangesetComment.pull_request_version == None)
632 ChangesetComment.pull_request_version == None)
633
633
634 # TODO: johbo: Find out why this breaks if it is done in a bulk
634 # TODO: johbo: Find out why this breaks if it is done in a bulk
635 # operation.
635 # operation.
636 for comment in comments:
636 for comment in comments:
637 comment.pull_request_version_id = (
637 comment.pull_request_version_id = (
638 pull_request_version.pull_request_version_id)
638 pull_request_version.pull_request_version_id)
639 Session().add(comment)
639 Session().add(comment)
640
640
641 def _calculate_commit_id_changes(self, old_ids, new_ids):
641 def _calculate_commit_id_changes(self, old_ids, new_ids):
642 added = new_ids.difference(old_ids)
642 added = new_ids.difference(old_ids)
643 common = old_ids.intersection(new_ids)
643 common = old_ids.intersection(new_ids)
644 removed = old_ids.difference(new_ids)
644 removed = old_ids.difference(new_ids)
645 return ChangeTuple(added, common, removed)
645 return ChangeTuple(added, common, removed)
646
646
647 def _calculate_file_changes(self, old_diff_data, new_diff_data):
647 def _calculate_file_changes(self, old_diff_data, new_diff_data):
648
648
649 old_files = OrderedDict()
649 old_files = OrderedDict()
650 for diff_data in old_diff_data.parsed_diff:
650 for diff_data in old_diff_data.parsed_diff:
651 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
651 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
652
652
653 added_files = []
653 added_files = []
654 modified_files = []
654 modified_files = []
655 removed_files = []
655 removed_files = []
656 for diff_data in new_diff_data.parsed_diff:
656 for diff_data in new_diff_data.parsed_diff:
657 new_filename = diff_data['filename']
657 new_filename = diff_data['filename']
658 new_hash = md5_safe(diff_data['raw_diff'])
658 new_hash = md5_safe(diff_data['raw_diff'])
659
659
660 old_hash = old_files.get(new_filename)
660 old_hash = old_files.get(new_filename)
661 if not old_hash:
661 if not old_hash:
662 # file is not present in old diff, means it's added
662 # file is not present in old diff, means it's added
663 added_files.append(new_filename)
663 added_files.append(new_filename)
664 else:
664 else:
665 if new_hash != old_hash:
665 if new_hash != old_hash:
666 modified_files.append(new_filename)
666 modified_files.append(new_filename)
667 # now remove a file from old, since we have seen it already
667 # now remove a file from old, since we have seen it already
668 del old_files[new_filename]
668 del old_files[new_filename]
669
669
670 # removed files is when there are present in old, but not in NEW,
670 # removed files is when there are present in old, but not in NEW,
671 # since we remove old files that are present in new diff, left-overs
671 # since we remove old files that are present in new diff, left-overs
672 # if any should be the removed files
672 # if any should be the removed files
673 removed_files.extend(old_files.keys())
673 removed_files.extend(old_files.keys())
674
674
675 return FileChangeTuple(added_files, modified_files, removed_files)
675 return FileChangeTuple(added_files, modified_files, removed_files)
676
676
677 def _render_update_message(self, changes, file_changes):
677 def _render_update_message(self, changes, file_changes):
678 """
678 """
679 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
679 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
680 so it's always looking the same disregarding on which default
680 so it's always looking the same disregarding on which default
681 renderer system is using.
681 renderer system is using.
682
682
683 :param changes: changes named tuple
683 :param changes: changes named tuple
684 :param file_changes: file changes named tuple
684 :param file_changes: file changes named tuple
685
685
686 """
686 """
687 new_status = ChangesetStatus.get_status_lbl(
687 new_status = ChangesetStatus.get_status_lbl(
688 ChangesetStatus.STATUS_UNDER_REVIEW)
688 ChangesetStatus.STATUS_UNDER_REVIEW)
689
689
690 changed_files = (
690 changed_files = (
691 file_changes.added + file_changes.modified + file_changes.removed)
691 file_changes.added + file_changes.modified + file_changes.removed)
692
692
693 params = {
693 params = {
694 'under_review_label': new_status,
694 'under_review_label': new_status,
695 'added_commits': changes.added,
695 'added_commits': changes.added,
696 'removed_commits': changes.removed,
696 'removed_commits': changes.removed,
697 'changed_files': changed_files,
697 'changed_files': changed_files,
698 'added_files': file_changes.added,
698 'added_files': file_changes.added,
699 'modified_files': file_changes.modified,
699 'modified_files': file_changes.modified,
700 'removed_files': file_changes.removed,
700 'removed_files': file_changes.removed,
701 }
701 }
702 renderer = RstTemplateRenderer()
702 renderer = RstTemplateRenderer()
703 return renderer.render('pull_request_update.mako', **params)
703 return renderer.render('pull_request_update.mako', **params)
704
704
705 def edit(self, pull_request, title, description):
705 def edit(self, pull_request, title, description):
706 pull_request = self.__get_pull_request(pull_request)
706 pull_request = self.__get_pull_request(pull_request)
707 if pull_request.is_closed():
707 if pull_request.is_closed():
708 raise ValueError('This pull request is closed')
708 raise ValueError('This pull request is closed')
709 if title:
709 if title:
710 pull_request.title = title
710 pull_request.title = title
711 pull_request.description = description
711 pull_request.description = description
712 pull_request.updated_on = datetime.datetime.now()
712 pull_request.updated_on = datetime.datetime.now()
713 Session().add(pull_request)
713 Session().add(pull_request)
714
714
715 def update_reviewers(self, pull_request, reviewers_ids):
715 def update_reviewers(self, pull_request, reviewers_ids):
716 reviewers_ids = set(reviewers_ids)
716 reviewers_ids = set(reviewers_ids)
717 pull_request = self.__get_pull_request(pull_request)
717 pull_request = self.__get_pull_request(pull_request)
718 current_reviewers = PullRequestReviewers.query()\
718 current_reviewers = PullRequestReviewers.query()\
719 .filter(PullRequestReviewers.pull_request ==
719 .filter(PullRequestReviewers.pull_request ==
720 pull_request).all()
720 pull_request).all()
721 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
721 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
722
722
723 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
723 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
724 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
724 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
725
725
726 log.debug("Adding %s reviewers", ids_to_add)
726 log.debug("Adding %s reviewers", ids_to_add)
727 log.debug("Removing %s reviewers", ids_to_remove)
727 log.debug("Removing %s reviewers", ids_to_remove)
728 changed = False
728 changed = False
729 for uid in ids_to_add:
729 for uid in ids_to_add:
730 changed = True
730 changed = True
731 _usr = self._get_user(uid)
731 _usr = self._get_user(uid)
732 reviewer = PullRequestReviewers(_usr, pull_request)
732 reviewer = PullRequestReviewers(_usr, pull_request)
733 Session().add(reviewer)
733 Session().add(reviewer)
734
734
735 self.notify_reviewers(pull_request, ids_to_add)
735 self.notify_reviewers(pull_request, ids_to_add)
736
736
737 for uid in ids_to_remove:
737 for uid in ids_to_remove:
738 changed = True
738 changed = True
739 reviewer = PullRequestReviewers.query()\
739 reviewer = PullRequestReviewers.query()\
740 .filter(PullRequestReviewers.user_id == uid,
740 .filter(PullRequestReviewers.user_id == uid,
741 PullRequestReviewers.pull_request == pull_request)\
741 PullRequestReviewers.pull_request == pull_request)\
742 .scalar()
742 .scalar()
743 if reviewer:
743 if reviewer:
744 Session().delete(reviewer)
744 Session().delete(reviewer)
745 if changed:
745 if changed:
746 pull_request.updated_on = datetime.datetime.now()
746 pull_request.updated_on = datetime.datetime.now()
747 Session().add(pull_request)
747 Session().add(pull_request)
748
748
749 return ids_to_add, ids_to_remove
749 return ids_to_add, ids_to_remove
750
750
751 def get_url(self, pull_request):
751 def get_url(self, pull_request):
752 return h.url('pullrequest_show',
752 return h.url('pullrequest_show',
753 repo_name=safe_str(pull_request.target_repo.repo_name),
753 repo_name=safe_str(pull_request.target_repo.repo_name),
754 pull_request_id=pull_request.pull_request_id,
754 pull_request_id=pull_request.pull_request_id,
755 qualified=True)
755 qualified=True)
756
756
757 def notify_reviewers(self, pull_request, reviewers_ids):
757 def notify_reviewers(self, pull_request, reviewers_ids):
758 # notification to reviewers
758 # notification to reviewers
759 if not reviewers_ids:
759 if not reviewers_ids:
760 return
760 return
761
761
762 pull_request_obj = pull_request
762 pull_request_obj = pull_request
763 # get the current participants of this pull request
763 # get the current participants of this pull request
764 recipients = reviewers_ids
764 recipients = reviewers_ids
765 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
765 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
766
766
767 pr_source_repo = pull_request_obj.source_repo
767 pr_source_repo = pull_request_obj.source_repo
768 pr_target_repo = pull_request_obj.target_repo
768 pr_target_repo = pull_request_obj.target_repo
769
769
770 pr_url = h.url(
770 pr_url = h.url(
771 'pullrequest_show',
771 'pullrequest_show',
772 repo_name=pr_target_repo.repo_name,
772 repo_name=pr_target_repo.repo_name,
773 pull_request_id=pull_request_obj.pull_request_id,
773 pull_request_id=pull_request_obj.pull_request_id,
774 qualified=True,)
774 qualified=True,)
775
775
776 # set some variables for email notification
776 # set some variables for email notification
777 pr_target_repo_url = h.url(
777 pr_target_repo_url = h.url(
778 'summary_home',
778 'summary_home',
779 repo_name=pr_target_repo.repo_name,
779 repo_name=pr_target_repo.repo_name,
780 qualified=True)
780 qualified=True)
781
781
782 pr_source_repo_url = h.url(
782 pr_source_repo_url = h.url(
783 'summary_home',
783 'summary_home',
784 repo_name=pr_source_repo.repo_name,
784 repo_name=pr_source_repo.repo_name,
785 qualified=True)
785 qualified=True)
786
786
787 # pull request specifics
787 # pull request specifics
788 pull_request_commits = [
788 pull_request_commits = [
789 (x.raw_id, x.message)
789 (x.raw_id, x.message)
790 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
790 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
791
791
792 kwargs = {
792 kwargs = {
793 'user': pull_request.author,
793 'user': pull_request.author,
794 'pull_request': pull_request_obj,
794 'pull_request': pull_request_obj,
795 'pull_request_commits': pull_request_commits,
795 'pull_request_commits': pull_request_commits,
796
796
797 'pull_request_target_repo': pr_target_repo,
797 'pull_request_target_repo': pr_target_repo,
798 'pull_request_target_repo_url': pr_target_repo_url,
798 'pull_request_target_repo_url': pr_target_repo_url,
799
799
800 'pull_request_source_repo': pr_source_repo,
800 'pull_request_source_repo': pr_source_repo,
801 'pull_request_source_repo_url': pr_source_repo_url,
801 'pull_request_source_repo_url': pr_source_repo_url,
802
802
803 'pull_request_url': pr_url,
803 'pull_request_url': pr_url,
804 }
804 }
805
805
806 # pre-generate the subject for notification itself
806 # pre-generate the subject for notification itself
807 (subject,
807 (subject,
808 _h, _e, # we don't care about those
808 _h, _e, # we don't care about those
809 body_plaintext) = EmailNotificationModel().render_email(
809 body_plaintext) = EmailNotificationModel().render_email(
810 notification_type, **kwargs)
810 notification_type, **kwargs)
811
811
812 # create notification objects, and emails
812 # create notification objects, and emails
813 NotificationModel().create(
813 NotificationModel().create(
814 created_by=pull_request.author,
814 created_by=pull_request.author,
815 notification_subject=subject,
815 notification_subject=subject,
816 notification_body=body_plaintext,
816 notification_body=body_plaintext,
817 notification_type=notification_type,
817 notification_type=notification_type,
818 recipients=recipients,
818 recipients=recipients,
819 email_kwargs=kwargs,
819 email_kwargs=kwargs,
820 )
820 )
821
821
822 def delete(self, pull_request):
822 def delete(self, pull_request):
823 pull_request = self.__get_pull_request(pull_request)
823 pull_request = self.__get_pull_request(pull_request)
824 self._cleanup_merge_workspace(pull_request)
824 self._cleanup_merge_workspace(pull_request)
825 Session().delete(pull_request)
825 Session().delete(pull_request)
826
826
827 def close_pull_request(self, pull_request, user):
827 def close_pull_request(self, pull_request, user):
828 pull_request = self.__get_pull_request(pull_request)
828 pull_request = self.__get_pull_request(pull_request)
829 self._cleanup_merge_workspace(pull_request)
829 self._cleanup_merge_workspace(pull_request)
830 pull_request.status = PullRequest.STATUS_CLOSED
830 pull_request.status = PullRequest.STATUS_CLOSED
831 pull_request.updated_on = datetime.datetime.now()
831 pull_request.updated_on = datetime.datetime.now()
832 Session().add(pull_request)
832 Session().add(pull_request)
833 self._trigger_pull_request_hook(
833 self._trigger_pull_request_hook(
834 pull_request, pull_request.author, 'close')
834 pull_request, pull_request.author, 'close')
835 self._log_action('user_closed_pull_request', user, pull_request)
835 self._log_action('user_closed_pull_request', user, pull_request)
836
836
837 def close_pull_request_with_comment(self, pull_request, user, repo,
837 def close_pull_request_with_comment(self, pull_request, user, repo,
838 message=None):
838 message=None):
839 status = ChangesetStatus.STATUS_REJECTED
839 status = ChangesetStatus.STATUS_REJECTED
840
840
841 if not message:
841 if not message:
842 message = (
842 message = (
843 _('Status change %(transition_icon)s %(status)s') % {
843 _('Status change %(transition_icon)s %(status)s') % {
844 'transition_icon': '>',
844 'transition_icon': '>',
845 'status': ChangesetStatus.get_status_lbl(status)})
845 'status': ChangesetStatus.get_status_lbl(status)})
846
846
847 internal_message = _('Closing with') + ' ' + message
847 internal_message = _('Closing with') + ' ' + message
848
848
849 comm = ChangesetCommentsModel().create(
849 comm = ChangesetCommentsModel().create(
850 text=internal_message,
850 text=internal_message,
851 repo=repo.repo_id,
851 repo=repo.repo_id,
852 user=user.user_id,
852 user=user.user_id,
853 pull_request=pull_request.pull_request_id,
853 pull_request=pull_request.pull_request_id,
854 f_path=None,
854 f_path=None,
855 line_no=None,
855 line_no=None,
856 status_change=ChangesetStatus.get_status_lbl(status),
856 status_change=ChangesetStatus.get_status_lbl(status),
857 status_change_type=status,
857 closing_pr=True
858 closing_pr=True
858 )
859 )
859
860
860 ChangesetStatusModel().set_status(
861 ChangesetStatusModel().set_status(
861 repo.repo_id,
862 repo.repo_id,
862 status,
863 status,
863 user.user_id,
864 user.user_id,
864 comm,
865 comm,
865 pull_request=pull_request.pull_request_id
866 pull_request=pull_request.pull_request_id
866 )
867 )
867 Session().flush()
868 Session().flush()
868
869
869 PullRequestModel().close_pull_request(
870 PullRequestModel().close_pull_request(
870 pull_request.pull_request_id, user)
871 pull_request.pull_request_id, user)
871
872
872 def merge_status(self, pull_request):
873 def merge_status(self, pull_request):
873 if not self._is_merge_enabled(pull_request):
874 if not self._is_merge_enabled(pull_request):
874 return False, _('Server-side pull request merging is disabled.')
875 return False, _('Server-side pull request merging is disabled.')
875 if pull_request.is_closed():
876 if pull_request.is_closed():
876 return False, _('This pull request is closed.')
877 return False, _('This pull request is closed.')
877 merge_possible, msg = self._check_repo_requirements(
878 merge_possible, msg = self._check_repo_requirements(
878 target=pull_request.target_repo, source=pull_request.source_repo)
879 target=pull_request.target_repo, source=pull_request.source_repo)
879 if not merge_possible:
880 if not merge_possible:
880 return merge_possible, msg
881 return merge_possible, msg
881
882
882 try:
883 try:
883 resp = self._try_merge(pull_request)
884 resp = self._try_merge(pull_request)
884 status = resp.possible, self.merge_status_message(
885 status = resp.possible, self.merge_status_message(
885 resp.failure_reason)
886 resp.failure_reason)
886 except NotImplementedError:
887 except NotImplementedError:
887 status = False, _('Pull request merging is not supported.')
888 status = False, _('Pull request merging is not supported.')
888
889
889 return status
890 return status
890
891
891 def _check_repo_requirements(self, target, source):
892 def _check_repo_requirements(self, target, source):
892 """
893 """
893 Check if `target` and `source` have compatible requirements.
894 Check if `target` and `source` have compatible requirements.
894
895
895 Currently this is just checking for largefiles.
896 Currently this is just checking for largefiles.
896 """
897 """
897 target_has_largefiles = self._has_largefiles(target)
898 target_has_largefiles = self._has_largefiles(target)
898 source_has_largefiles = self._has_largefiles(source)
899 source_has_largefiles = self._has_largefiles(source)
899 merge_possible = True
900 merge_possible = True
900 message = u''
901 message = u''
901
902
902 if target_has_largefiles != source_has_largefiles:
903 if target_has_largefiles != source_has_largefiles:
903 merge_possible = False
904 merge_possible = False
904 if source_has_largefiles:
905 if source_has_largefiles:
905 message = _(
906 message = _(
906 'Target repository large files support is disabled.')
907 'Target repository large files support is disabled.')
907 else:
908 else:
908 message = _(
909 message = _(
909 'Source repository large files support is disabled.')
910 'Source repository large files support is disabled.')
910
911
911 return merge_possible, message
912 return merge_possible, message
912
913
913 def _has_largefiles(self, repo):
914 def _has_largefiles(self, repo):
914 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
915 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
915 'extensions', 'largefiles')
916 'extensions', 'largefiles')
916 return largefiles_ui and largefiles_ui[0].active
917 return largefiles_ui and largefiles_ui[0].active
917
918
918 def _try_merge(self, pull_request):
919 def _try_merge(self, pull_request):
919 """
920 """
920 Try to merge the pull request and return the merge status.
921 Try to merge the pull request and return the merge status.
921 """
922 """
922 log.debug(
923 log.debug(
923 "Trying out if the pull request %s can be merged.",
924 "Trying out if the pull request %s can be merged.",
924 pull_request.pull_request_id)
925 pull_request.pull_request_id)
925 target_vcs = pull_request.target_repo.scm_instance()
926 target_vcs = pull_request.target_repo.scm_instance()
926 target_ref = self._refresh_reference(
927 target_ref = self._refresh_reference(
927 pull_request.target_ref_parts, target_vcs)
928 pull_request.target_ref_parts, target_vcs)
928
929
929 target_locked = pull_request.target_repo.locked
930 target_locked = pull_request.target_repo.locked
930 if target_locked and target_locked[0]:
931 if target_locked and target_locked[0]:
931 log.debug("The target repository is locked.")
932 log.debug("The target repository is locked.")
932 merge_state = MergeResponse(
933 merge_state = MergeResponse(
933 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
934 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
934 elif self._needs_merge_state_refresh(pull_request, target_ref):
935 elif self._needs_merge_state_refresh(pull_request, target_ref):
935 log.debug("Refreshing the merge status of the repository.")
936 log.debug("Refreshing the merge status of the repository.")
936 merge_state = self._refresh_merge_state(
937 merge_state = self._refresh_merge_state(
937 pull_request, target_vcs, target_ref)
938 pull_request, target_vcs, target_ref)
938 else:
939 else:
939 possible = pull_request.\
940 possible = pull_request.\
940 _last_merge_status == MergeFailureReason.NONE
941 _last_merge_status == MergeFailureReason.NONE
941 merge_state = MergeResponse(
942 merge_state = MergeResponse(
942 possible, False, None, pull_request._last_merge_status)
943 possible, False, None, pull_request._last_merge_status)
943 log.debug("Merge response: %s", merge_state)
944 log.debug("Merge response: %s", merge_state)
944 return merge_state
945 return merge_state
945
946
946 def _refresh_reference(self, reference, vcs_repository):
947 def _refresh_reference(self, reference, vcs_repository):
947 if reference.type in ('branch', 'book'):
948 if reference.type in ('branch', 'book'):
948 name_or_id = reference.name
949 name_or_id = reference.name
949 else:
950 else:
950 name_or_id = reference.commit_id
951 name_or_id = reference.commit_id
951 refreshed_commit = vcs_repository.get_commit(name_or_id)
952 refreshed_commit = vcs_repository.get_commit(name_or_id)
952 refreshed_reference = Reference(
953 refreshed_reference = Reference(
953 reference.type, reference.name, refreshed_commit.raw_id)
954 reference.type, reference.name, refreshed_commit.raw_id)
954 return refreshed_reference
955 return refreshed_reference
955
956
956 def _needs_merge_state_refresh(self, pull_request, target_reference):
957 def _needs_merge_state_refresh(self, pull_request, target_reference):
957 return not(
958 return not(
958 pull_request.revisions and
959 pull_request.revisions and
959 pull_request.revisions[0] == pull_request._last_merge_source_rev and
960 pull_request.revisions[0] == pull_request._last_merge_source_rev and
960 target_reference.commit_id == pull_request._last_merge_target_rev)
961 target_reference.commit_id == pull_request._last_merge_target_rev)
961
962
962 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
963 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
963 workspace_id = self._workspace_id(pull_request)
964 workspace_id = self._workspace_id(pull_request)
964 source_vcs = pull_request.source_repo.scm_instance()
965 source_vcs = pull_request.source_repo.scm_instance()
965 use_rebase = self._use_rebase_for_merging(pull_request)
966 use_rebase = self._use_rebase_for_merging(pull_request)
966 merge_state = target_vcs.merge(
967 merge_state = target_vcs.merge(
967 target_reference, source_vcs, pull_request.source_ref_parts,
968 target_reference, source_vcs, pull_request.source_ref_parts,
968 workspace_id, dry_run=True, use_rebase=use_rebase)
969 workspace_id, dry_run=True, use_rebase=use_rebase)
969
970
970 # Do not store the response if there was an unknown error.
971 # Do not store the response if there was an unknown error.
971 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
972 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
972 pull_request._last_merge_source_rev = pull_request.\
973 pull_request._last_merge_source_rev = pull_request.\
973 source_ref_parts.commit_id
974 source_ref_parts.commit_id
974 pull_request._last_merge_target_rev = target_reference.commit_id
975 pull_request._last_merge_target_rev = target_reference.commit_id
975 pull_request._last_merge_status = (
976 pull_request._last_merge_status = (
976 merge_state.failure_reason)
977 merge_state.failure_reason)
977 Session().add(pull_request)
978 Session().add(pull_request)
978 Session().flush()
979 Session().flush()
979
980
980 return merge_state
981 return merge_state
981
982
982 def _workspace_id(self, pull_request):
983 def _workspace_id(self, pull_request):
983 workspace_id = 'pr-%s' % pull_request.pull_request_id
984 workspace_id = 'pr-%s' % pull_request.pull_request_id
984 return workspace_id
985 return workspace_id
985
986
986 def merge_status_message(self, status_code):
987 def merge_status_message(self, status_code):
987 """
988 """
988 Return a human friendly error message for the given merge status code.
989 Return a human friendly error message for the given merge status code.
989 """
990 """
990 return self.MERGE_STATUS_MESSAGES[status_code]
991 return self.MERGE_STATUS_MESSAGES[status_code]
991
992
992 def generate_repo_data(self, repo, commit_id=None, branch=None,
993 def generate_repo_data(self, repo, commit_id=None, branch=None,
993 bookmark=None):
994 bookmark=None):
994 all_refs, selected_ref = \
995 all_refs, selected_ref = \
995 self._get_repo_pullrequest_sources(
996 self._get_repo_pullrequest_sources(
996 repo.scm_instance(), commit_id=commit_id,
997 repo.scm_instance(), commit_id=commit_id,
997 branch=branch, bookmark=bookmark)
998 branch=branch, bookmark=bookmark)
998
999
999 refs_select2 = []
1000 refs_select2 = []
1000 for element in all_refs:
1001 for element in all_refs:
1001 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1002 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1002 refs_select2.append({'text': element[1], 'children': children})
1003 refs_select2.append({'text': element[1], 'children': children})
1003
1004
1004 return {
1005 return {
1005 'user': {
1006 'user': {
1006 'user_id': repo.user.user_id,
1007 'user_id': repo.user.user_id,
1007 'username': repo.user.username,
1008 'username': repo.user.username,
1008 'firstname': repo.user.firstname,
1009 'firstname': repo.user.firstname,
1009 'lastname': repo.user.lastname,
1010 'lastname': repo.user.lastname,
1010 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1011 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1011 },
1012 },
1012 'description': h.chop_at_smart(repo.description, '\n'),
1013 'description': h.chop_at_smart(repo.description, '\n'),
1013 'refs': {
1014 'refs': {
1014 'all_refs': all_refs,
1015 'all_refs': all_refs,
1015 'selected_ref': selected_ref,
1016 'selected_ref': selected_ref,
1016 'select2_refs': refs_select2
1017 'select2_refs': refs_select2
1017 }
1018 }
1018 }
1019 }
1019
1020
1020 def generate_pullrequest_title(self, source, source_ref, target):
1021 def generate_pullrequest_title(self, source, source_ref, target):
1021 return '{source}#{at_ref} to {target}'.format(
1022 return '{source}#{at_ref} to {target}'.format(
1022 source=source,
1023 source=source,
1023 at_ref=source_ref,
1024 at_ref=source_ref,
1024 target=target,
1025 target=target,
1025 )
1026 )
1026
1027
1027 def _cleanup_merge_workspace(self, pull_request):
1028 def _cleanup_merge_workspace(self, pull_request):
1028 # Merging related cleanup
1029 # Merging related cleanup
1029 target_scm = pull_request.target_repo.scm_instance()
1030 target_scm = pull_request.target_repo.scm_instance()
1030 workspace_id = 'pr-%s' % pull_request.pull_request_id
1031 workspace_id = 'pr-%s' % pull_request.pull_request_id
1031
1032
1032 try:
1033 try:
1033 target_scm.cleanup_merge_workspace(workspace_id)
1034 target_scm.cleanup_merge_workspace(workspace_id)
1034 except NotImplementedError:
1035 except NotImplementedError:
1035 pass
1036 pass
1036
1037
1037 def _get_repo_pullrequest_sources(
1038 def _get_repo_pullrequest_sources(
1038 self, repo, commit_id=None, branch=None, bookmark=None):
1039 self, repo, commit_id=None, branch=None, bookmark=None):
1039 """
1040 """
1040 Return a structure with repo's interesting commits, suitable for
1041 Return a structure with repo's interesting commits, suitable for
1041 the selectors in pullrequest controller
1042 the selectors in pullrequest controller
1042
1043
1043 :param commit_id: a commit that must be in the list somehow
1044 :param commit_id: a commit that must be in the list somehow
1044 and selected by default
1045 and selected by default
1045 :param branch: a branch that must be in the list and selected
1046 :param branch: a branch that must be in the list and selected
1046 by default - even if closed
1047 by default - even if closed
1047 :param bookmark: a bookmark that must be in the list and selected
1048 :param bookmark: a bookmark that must be in the list and selected
1048 """
1049 """
1049
1050
1050 commit_id = safe_str(commit_id) if commit_id else None
1051 commit_id = safe_str(commit_id) if commit_id else None
1051 branch = safe_str(branch) if branch else None
1052 branch = safe_str(branch) if branch else None
1052 bookmark = safe_str(bookmark) if bookmark else None
1053 bookmark = safe_str(bookmark) if bookmark else None
1053
1054
1054 selected = None
1055 selected = None
1055
1056
1056 # order matters: first source that has commit_id in it will be selected
1057 # order matters: first source that has commit_id in it will be selected
1057 sources = []
1058 sources = []
1058 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1059 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1059 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1060 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1060
1061
1061 if commit_id:
1062 if commit_id:
1062 ref_commit = (h.short_id(commit_id), commit_id)
1063 ref_commit = (h.short_id(commit_id), commit_id)
1063 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1064 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1064
1065
1065 sources.append(
1066 sources.append(
1066 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1067 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1067 )
1068 )
1068
1069
1069 groups = []
1070 groups = []
1070 for group_key, ref_list, group_name, match in sources:
1071 for group_key, ref_list, group_name, match in sources:
1071 group_refs = []
1072 group_refs = []
1072 for ref_name, ref_id in ref_list:
1073 for ref_name, ref_id in ref_list:
1073 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1074 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1074 group_refs.append((ref_key, ref_name))
1075 group_refs.append((ref_key, ref_name))
1075
1076
1076 if not selected:
1077 if not selected:
1077 if set([commit_id, match]) & set([ref_id, ref_name]):
1078 if set([commit_id, match]) & set([ref_id, ref_name]):
1078 selected = ref_key
1079 selected = ref_key
1079
1080
1080 if group_refs:
1081 if group_refs:
1081 groups.append((group_refs, group_name))
1082 groups.append((group_refs, group_name))
1082
1083
1083 if not selected:
1084 if not selected:
1084 ref = commit_id or branch or bookmark
1085 ref = commit_id or branch or bookmark
1085 if ref:
1086 if ref:
1086 raise CommitDoesNotExistError(
1087 raise CommitDoesNotExistError(
1087 'No commit refs could be found matching: %s' % ref)
1088 'No commit refs could be found matching: %s' % ref)
1088 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1089 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1089 selected = 'branch:%s:%s' % (
1090 selected = 'branch:%s:%s' % (
1090 repo.DEFAULT_BRANCH_NAME,
1091 repo.DEFAULT_BRANCH_NAME,
1091 repo.branches[repo.DEFAULT_BRANCH_NAME]
1092 repo.branches[repo.DEFAULT_BRANCH_NAME]
1092 )
1093 )
1093 elif repo.commit_ids:
1094 elif repo.commit_ids:
1094 rev = repo.commit_ids[0]
1095 rev = repo.commit_ids[0]
1095 selected = 'rev:%s:%s' % (rev, rev)
1096 selected = 'rev:%s:%s' % (rev, rev)
1096 else:
1097 else:
1097 raise EmptyRepositoryError()
1098 raise EmptyRepositoryError()
1098 return groups, selected
1099 return groups, selected
1099
1100
1100 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1101 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1101 pull_request = self.__get_pull_request(pull_request)
1102 pull_request = self.__get_pull_request(pull_request)
1102 return self._get_diff_from_pr_or_version(pull_request, context=context)
1103 return self._get_diff_from_pr_or_version(pull_request, context=context)
1103
1104
1104 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1105 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1105 source_repo = pr_or_version.source_repo
1106 source_repo = pr_or_version.source_repo
1106
1107
1107 # we swap org/other ref since we run a simple diff on one repo
1108 # we swap org/other ref since we run a simple diff on one repo
1108 target_ref_id = pr_or_version.target_ref_parts.commit_id
1109 target_ref_id = pr_or_version.target_ref_parts.commit_id
1109 source_ref_id = pr_or_version.source_ref_parts.commit_id
1110 source_ref_id = pr_or_version.source_ref_parts.commit_id
1110 target_commit = source_repo.get_commit(
1111 target_commit = source_repo.get_commit(
1111 commit_id=safe_str(target_ref_id))
1112 commit_id=safe_str(target_ref_id))
1112 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1113 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1113 vcs_repo = source_repo.scm_instance()
1114 vcs_repo = source_repo.scm_instance()
1114
1115
1115 # TODO: johbo: In the context of an update, we cannot reach
1116 # TODO: johbo: In the context of an update, we cannot reach
1116 # the old commit anymore with our normal mechanisms. It needs
1117 # the old commit anymore with our normal mechanisms. It needs
1117 # some sort of special support in the vcs layer to avoid this
1118 # some sort of special support in the vcs layer to avoid this
1118 # workaround.
1119 # workaround.
1119 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1120 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1120 vcs_repo.alias == 'git'):
1121 vcs_repo.alias == 'git'):
1121 source_commit.raw_id = safe_str(source_ref_id)
1122 source_commit.raw_id = safe_str(source_ref_id)
1122
1123
1123 log.debug('calculating diff between '
1124 log.debug('calculating diff between '
1124 'source_ref:%s and target_ref:%s for repo `%s`',
1125 'source_ref:%s and target_ref:%s for repo `%s`',
1125 target_ref_id, source_ref_id,
1126 target_ref_id, source_ref_id,
1126 safe_unicode(vcs_repo.path))
1127 safe_unicode(vcs_repo.path))
1127
1128
1128 vcs_diff = vcs_repo.get_diff(
1129 vcs_diff = vcs_repo.get_diff(
1129 commit1=target_commit, commit2=source_commit, context=context)
1130 commit1=target_commit, commit2=source_commit, context=context)
1130 return vcs_diff
1131 return vcs_diff
1131
1132
1132 def _is_merge_enabled(self, pull_request):
1133 def _is_merge_enabled(self, pull_request):
1133 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1134 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1134 settings = settings_model.get_general_settings()
1135 settings = settings_model.get_general_settings()
1135 return settings.get('rhodecode_pr_merge_enabled', False)
1136 return settings.get('rhodecode_pr_merge_enabled', False)
1136
1137
1137 def _use_rebase_for_merging(self, pull_request):
1138 def _use_rebase_for_merging(self, pull_request):
1138 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1139 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1139 settings = settings_model.get_general_settings()
1140 settings = settings_model.get_general_settings()
1140 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1141 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1141
1142
1142 def _log_action(self, action, user, pull_request):
1143 def _log_action(self, action, user, pull_request):
1143 action_logger(
1144 action_logger(
1144 user,
1145 user,
1145 '{action}:{pr_id}'.format(
1146 '{action}:{pr_id}'.format(
1146 action=action, pr_id=pull_request.pull_request_id),
1147 action=action, pr_id=pull_request.pull_request_id),
1147 pull_request.target_repo)
1148 pull_request.target_repo)
1148
1149
1149
1150
1150 ChangeTuple = namedtuple('ChangeTuple',
1151 ChangeTuple = namedtuple('ChangeTuple',
1151 ['added', 'common', 'removed'])
1152 ['added', 'common', 'removed'])
1152
1153
1153 FileChangeTuple = namedtuple('FileChangeTuple',
1154 FileChangeTuple = namedtuple('FileChangeTuple',
1154 ['added', 'modified', 'removed'])
1155 ['added', 'modified', 'removed'])
@@ -1,106 +1,131 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 ## helpers
4 <%def name="tag_button(text, tag_type=None)">
5 <%
6 color_scheme = {
7 'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
8 'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
9 'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
10 'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
11 }
12 %>
13 <pre style="display:inline;border-radius:2px;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
14 </%def>
15
16 <%def name="status_text(text, tag_type=None)">
17 <%
18 color_scheme = {
19 'default': 'color:#666666',
20 'approved': 'color:#0ac878',
21 'rejected': 'color:#e85e4d',
22 'under_review': 'color:#ffc854',
23 }
24 %>
25 <span style="font-weight:bold;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</span>
26 </%def>
27
3 ## headers we additionally can set for email
28 ## headers we additionally can set for email
4 <%def name="headers()" filter="n,trim"></%def>
29 <%def name="headers()" filter="n,trim"></%def>
5
30
6 <%def name="plaintext_footer()">
31 <%def name="plaintext_footer()">
7 ${_('This is a notification from RhodeCode. %(instance_url)s') % {'instance_url': instance_url}}
32 ${_('This is a notification from RhodeCode. %(instance_url)s') % {'instance_url': instance_url}}
8 </%def>
33 </%def>
9
34
10 <%def name="body_plaintext()" filter="n,trim">
35 <%def name="body_plaintext()" filter="n,trim">
11 ## this example is not called itself but overridden in each template
36 ## this example is not called itself but overridden in each template
12 ## the plaintext_footer should be at the bottom of both html and text emails
37 ## the plaintext_footer should be at the bottom of both html and text emails
13 ${self.plaintext_footer()}
38 ${self.plaintext_footer()}
14 </%def>
39 </%def>
15
40
16 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
41 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
17 <html xmlns="http://www.w3.org/1999/xhtml">
42 <html xmlns="http://www.w3.org/1999/xhtml">
18 <head>
43 <head>
19 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
44 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
20 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
45 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
21 <title>${self.subject()}</title>
46 <title>${self.subject()}</title>
22 <style type="text/css">
47 <style type="text/css">
23 /* Based on The MailChimp Reset INLINE: Yes. */
48 /* Based on The MailChimp Reset INLINE: Yes. */
24 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
49 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
25 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
50 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
26 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
51 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
27 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
52 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
28 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
53 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
29 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
54 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
30 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
55 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
31 /* End reset */
56 /* End reset */
32
57
33 /* defaults for images*/
58 /* defaults for images*/
34 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
59 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
35 a img {border:none;}
60 a img {border:none;}
36 .image_fix {display:block;}
61 .image_fix {display:block;}
37
62
38 body {line-height:1.2em;}
63 body {line-height:1.2em;}
39 p {margin: 0 0 20px;}
64 p {margin: 0 0 20px;}
40 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
65 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
41 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
66 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
42 a:focus {outline:none;}
67 a:focus {outline:none;}
43 a:hover {color: #305b91;}
68 a:hover {color: #305b91;}
44 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
69 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
45 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
70 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
46 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
71 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
47 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
72 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
48 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
73 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
49 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
74 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
50 input:focus {outline: 1px solid #979797}
75 input:focus {outline: 1px solid #979797}
51 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
76 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
52 /* Put your iPhone 4g styles in here */
77 /* Put your iPhone 4g styles in here */
53 }
78 }
54
79
55 /* Android targeting */
80 /* Android targeting */
56 @media only screen and (-webkit-device-pixel-ratio:.75){
81 @media only screen and (-webkit-device-pixel-ratio:.75){
57 /* Put CSS for low density (ldpi) Android layouts in here */
82 /* Put CSS for low density (ldpi) Android layouts in here */
58 }
83 }
59 @media only screen and (-webkit-device-pixel-ratio:1){
84 @media only screen and (-webkit-device-pixel-ratio:1){
60 /* Put CSS for medium density (mdpi) Android layouts in here */
85 /* Put CSS for medium density (mdpi) Android layouts in here */
61 }
86 }
62 @media only screen and (-webkit-device-pixel-ratio:1.5){
87 @media only screen and (-webkit-device-pixel-ratio:1.5){
63 /* Put CSS for high density (hdpi) Android layouts in here */
88 /* Put CSS for high density (hdpi) Android layouts in here */
64 }
89 }
65 /* end Android targeting */
90 /* end Android targeting */
66
91
67 </style>
92 </style>
68
93
69 <!-- Targeting Windows Mobile -->
94 <!-- Targeting Windows Mobile -->
70 <!--[if IEMobile 7]>
95 <!--[if IEMobile 7]>
71 <style type="text/css">
96 <style type="text/css">
72
97
73 </style>
98 </style>
74 <![endif]-->
99 <![endif]-->
75
100
76 <!--[if gte mso 9]>
101 <!--[if gte mso 9]>
77 <style>
102 <style>
78 /* Target Outlook 2007 and 2010 */
103 /* Target Outlook 2007 and 2010 */
79 </style>
104 </style>
80 <![endif]-->
105 <![endif]-->
81 </head>
106 </head>
82 <body>
107 <body>
83 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
108 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
84 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
109 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
85 <tr>
110 <tr>
86 <td valign="top" style="padding:0;">
111 <td valign="top" style="padding:0;">
87 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
112 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
88 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
113 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
89 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
114 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
90 ${_('RhodeCode')}
115 ${_('RhodeCode')}
91 % if rhodecode_instance_name:
116 % if rhodecode_instance_name:
92 - ${rhodecode_instance_name}
117 - ${rhodecode_instance_name}
93 % endif
118 % endif
94 </a>
119 </a>
95 </td></tr>
120 </td></tr>
96 <tr><td style="padding:15px;" valign="top">${self.body()}</td></tr>
121 <tr><td style="padding:15px;" valign="top">${self.body()}</td></tr>
97 </table>
122 </table>
98 </td>
123 </td>
99 </tr>
124 </tr>
100 </table>
125 </table>
101 <!-- End of wrapper table -->
126 <!-- End of wrapper table -->
102 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
127 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
103 ${self.plaintext_footer()}
128 ${self.plaintext_footer()}
104 </a></p>
129 </a></p>
105 </body>
130 </body>
106 </html>
131 </html>
@@ -1,88 +1,88 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3
3 <%namespace name="base" file="base.mako"/>
4
4
5 <%def name="subject()" filter="n,trim">
5 <%def name="subject()" filter="n,trim">
6 <%
6 <%
7 data = {
7 data = {
8 'user': h.person(user),
8 'user': h.person(user),
9 'repo_name': repo_name,
9 'repo_name': repo_name,
10 'commit_id': h.show_id(commit),
10 'commit_id': h.show_id(commit),
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 }
14 }
15 %>
15 %>
16 ${_('[mention]') if mention else ''} \
16 ${_('[mention]') if mention else ''} \
17
17
18 % if comment_file:
18 % if comment_file:
19 ${_('%(user)s commented on commit `%(commit_id)s` (file: `%(comment_file)s`)') % data} ${_('in the %(repo_name)s repository') % data |n}
19 ${_('%(user)s commented on commit `%(commit_id)s` (file: `%(comment_file)s`)') % data} ${_('in the %(repo_name)s repository') % data |n}
20 % else:
20 % else:
21 % if status_change:
21 % if status_change:
22 ${_('%(user)s commented on commit `%(commit_id)s` (status: %(status)s)') % data |n} ${_('in the %(repo_name)s repository') % data |n}
22 ${_('%(user)s commented on commit `%(commit_id)s` (status: %(status)s)') % data |n} ${_('in the %(repo_name)s repository') % data |n}
23 % else:
23 % else:
24 ${_('%(user)s commented on commit `%(commit_id)s`') % data |n} ${_('in the %(repo_name)s repository') % data |n}
24 ${_('%(user)s commented on commit `%(commit_id)s`') % data |n} ${_('in the %(repo_name)s repository') % data |n}
25 % endif
25 % endif
26 % endif
26 % endif
27
27
28 </%def>
28 </%def>
29
29
30 <%def name="body_plaintext()" filter="n,trim">
30 <%def name="body_plaintext()" filter="n,trim">
31 <%
31 <%
32 data = {
32 data = {
33 'user': h.person(user),
33 'user': h.person(user),
34 'repo_name': repo_name,
34 'repo_name': repo_name,
35 'commit_id': h.show_id(commit),
35 'commit_id': h.show_id(commit),
36 'status': status_change,
36 'status': status_change,
37 'comment_file': comment_file,
37 'comment_file': comment_file,
38 'comment_line': comment_line,
38 'comment_line': comment_line,
39 }
39 }
40 %>
40 %>
41 ${self.subject()}
41 ${self.subject()}
42
42
43 * ${_('Comment link')}: ${commit_comment_url}
43 * ${_('Comment link')}: ${commit_comment_url}
44
44
45 * ${_('Commit')}: ${h.show_id(commit)}
45 * ${_('Commit')}: ${h.show_id(commit)}
46
46
47 %if comment_file:
47 %if comment_file:
48 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
48 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
49 %endif
49 %endif
50
50
51 ---
51 ---
52
52
53 %if status_change:
53 %if status_change:
54 ${_('Commit status was changed to')}: *${status_change}*
54 ${_('Commit status was changed to')}: *${status_change}*
55 %endif
55 %endif
56
56
57 ${comment_body|n}
57 ${comment_body|n}
58
58
59 ${self.plaintext_footer()}
59 ${self.plaintext_footer()}
60 </%def>
60 </%def>
61
61
62
62
63 <%
63 <%
64 data = {
64 data = {
65 'user': h.person(user),
65 'user': h.person(user),
66 'comment_file': comment_file,
66 'comment_file': comment_file,
67 'comment_line': comment_line,
67 'comment_line': comment_line,
68 'repo': commit_target_repo,
68 'repo': commit_target_repo,
69 'repo_name': repo_name,
69 'repo_name': repo_name,
70 'commit_id': h.show_id(commit),
70 'commit_id': h.show_id(commit),
71 }
71 }
72 %>
72 %>
73 <table style="text-align:left;vertical-align:middle;">
73 <table style="text-align:left;vertical-align:middle;">
74 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
74 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
75 % if comment_file:
75 % if comment_file:
76 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s` (file:`%(comment_file)s`)') % data}</a> ${_('in the %(repo)s repository') % data |n}</h4>
76 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s` (file:`%(comment_file)s`)') % data}</a> ${_('in the %(repo)s repository') % data |n}</h4>
77 % else:
77 % else:
78 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s`') % data |n}</a> ${_('in the %(repo)s repository') % data |n}</h4>
78 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s`') % data |n}</a> ${_('in the %(repo)s repository') % data |n}</h4>
79 % endif
79 % endif
80 </td></tr>
80 </td></tr>
81 <tr><td style="padding-right:20px;padding-top:15px;">${_('Commit')}</td><td style="padding-top:15px;"><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${h.show_id(commit)}</a></td></tr>
81 <tr><td style="padding-right:20px;padding-top:15px;">${_('Commit')}</td><td style="padding-top:15px;"><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${h.show_id(commit)}</a></td></tr>
82 <tr><td style="padding-right:20px;">${_('Description')}</td><td>${h.urlify_commit_message(commit.message, repo_name)}</td></tr>
82 <tr><td style="padding-right:20px;">${_('Description')}</td><td>${h.urlify_commit_message(commit.message, repo_name)}</td></tr>
83
83
84 % if status_change:
84 % if status_change:
85 <tr><td style="padding-right:20px;">${_('Status')}</td><td>${_('The commit status was changed to')}: ${status_change}.</td></tr>
85 <tr><td style="padding-right:20px;">${_('Status')}</td><td>${_('The commit status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}</td></tr>
86 % endif
86 % endif
87 <tr><td style="padding-right:20px;">${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}</td><td style="line-height:1.2em;">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
87 <tr><td style="padding-right:20px;">${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}</td><td style="line-height:1.2em;">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
88 </table>
88 </table>
@@ -1,94 +1,98 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3
4
4
5
5 <%def name="subject()" filter="n,trim">
6 <%def name="subject()" filter="n,trim">
6 <%
7 <%
7 data = {
8 data = {
8 'user': h.person(user),
9 'user': h.person(user),
9 'pr_title': pull_request.title,
10 'pr_title': pull_request.title,
10 'pr_id': pull_request.pull_request_id,
11 'pr_id': pull_request.pull_request_id,
11 'status': status_change,
12 'status': status_change,
12 'comment_file': comment_file,
13 'comment_file': comment_file,
13 'comment_line': comment_line,
14 'comment_line': comment_line,
14 }
15 }
15 %>
16 %>
16
17
17 ${_('[mention]') if mention else ''} \
18 ${_('[mention]') if mention else ''} \
18
19
19 % if comment_file:
20 % if comment_file:
20 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file: `%(comment_file)s`)') % data |n}
21 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file: `%(comment_file)s`)') % data |n}
21 % else:
22 % else:
22 % if status_change:
23 % if status_change:
23 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (status: %(status)s)') % data |n}
24 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (status: %(status)s)') % data |n}
24 % else:
25 % else:
25 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
26 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
26 % endif
27 % endif
27 % endif
28 % endif
28 </%def>
29 </%def>
29
30
30 <%def name="body_plaintext()" filter="n,trim">
31 <%def name="body_plaintext()" filter="n,trim">
31 <%
32 <%
32 data = {
33 data = {
33 'user': h.person(user),
34 'user': h.person(user),
34 'pr_title': pull_request.title,
35 'pr_title': pull_request.title,
35 'pr_id': pull_request.pull_request_id,
36 'pr_id': pull_request.pull_request_id,
36 'status': status_change,
37 'status': status_change,
37 'comment_file': comment_file,
38 'comment_file': comment_file,
38 'comment_line': comment_line,
39 'comment_line': comment_line,
39 }
40 }
40 %>
41 %>
41 ${self.subject()}
42 ${self.subject()}
42
43
43 * ${_('Comment link')}: ${pr_comment_url}
44 * ${_('Comment link')}: ${pr_comment_url}
44
45
45 * ${_('Source repository')}: ${pr_source_repo_url}
46 * ${_('Source repository')}: ${pr_source_repo_url}
46
47
47 %if comment_file:
48 %if comment_file:
48 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
49 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
49 %endif
50 %endif
50
51
51 ---
52 ---
52
53
53 %if status_change and not closing_pr:
54 %if status_change and not closing_pr:
54 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s*') % data}
55 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s*') % data}
55 %elif status_change and closing_pr:
56 %elif status_change and closing_pr:
56 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s and closed*') % data}
57 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s and closed*') % data}
57 %endif
58 %endif
58
59
59 ${comment_body|n}
60 ${comment_body|n}
60
61
61 ${self.plaintext_footer()}
62 ${self.plaintext_footer()}
62 </%def>
63 </%def>
63
64
64
65
65 <%
66 <%
66 data = {
67 data = {
67 'user': h.person(user),
68 'user': h.person(user),
68 'pr_title': pull_request.title,
69 'pr_title': pull_request.title,
69 'pr_id': pull_request.pull_request_id,
70 'pr_id': pull_request.pull_request_id,
70 'status': status_change,
71 'status': status_change,
71 'comment_file': comment_file,
72 'comment_file': comment_file,
72 'comment_line': comment_line,
73 'comment_line': comment_line,
73 }
74 }
74 %>
75 %>
75 <table style="text-align:left;vertical-align:middle;">
76 <table style="text-align:left;vertical-align:middle;">
76 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
77 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
77 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">
78 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">
78
79
79 % if comment_file:
80 % if comment_file:
80 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file:`%(comment_file)s`)') % data |n}
81 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file:`%(comment_file)s`)') % data |n}
81 % else:
82 % else:
82 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
83 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
83 % endif
84 % endif
84 </a>
85 </a>
85 %if status_change and not closing_pr:
86 %if status_change and not closing_pr:
86 , ${_('submitted pull request status: %(status)s') % data}
87 , ${_('submitted pull request status: %(status)s') % data}
87 %elif status_change and closing_pr:
88 %elif status_change and closing_pr:
88 , ${_('submitted pull request status: %(status)s and closed') % data}
89 , ${_('submitted pull request status: %(status)s and closed') % data}
89 %endif
90 %endif
90 </h4>
91 </h4>
91 </td></tr>
92 </td></tr>
92 <tr><td style="padding-right:20px;padding-top:15px;">${_('Source')}</td><td style="padding-top:15px;"><a style="color:#427cc9;text-decoration:none;cursor:pointer" href="${pr_source_repo_url}">${pr_source_repo.repo_name}</a></td></tr>
93 <tr><td style="padding-right:20px;padding-top:15px;">${_('Source')}</td><td style="padding-top:15px;"><a style="color:#427cc9;text-decoration:none;cursor:pointer" href="${pr_source_repo_url}">${pr_source_repo.repo_name}</a></td></tr>
94 % if status_change:
95 <tr><td style="padding-right:20px;">${_('Submitted status')}</td><td>${base.status_text(status_change, tag_type=status_change_type)}</td></tr>
96 % endif
93 <tr><td style="padding-right:20px;">${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}</td><td style="line-height:1.2em;">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
97 <tr><td style="padding-right:20px;">${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}</td><td style="line-height:1.2em;">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
94 </table>
98 </table>
@@ -1,61 +1,85 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3
4
4 <%def name="subject()" filter="n,trim">
5 <%def name="subject()" filter="n,trim">
5 ${_('%(user)s wants you to review pull request #%(pr_url)s: "%(pr_title)s"') % {
6 <%
6 'user': h.person(user),
7 data = {
7 'pr_title': pull_request.title,
8 'user': h.person(user),
8 'pr_url': pull_request.pull_request_id
9 'pr_id': pull_request.pull_request_id,
9 } |n}
10 'pr_title': pull_request.title,
11 }
12 %>
13
14 ${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s"') % data |n}
10 </%def>
15 </%def>
11
16
12
17
13 <%def name="body_plaintext()" filter="n,trim">
18 <%def name="body_plaintext()" filter="n,trim">
19 <%
20 data = {
21 'user': h.person(user),
22 'pr_id': pull_request.pull_request_id,
23 'pr_title': pull_request.title,
24 'source_ref_type': pull_request.source_ref_parts.type,
25 'source_ref_name': pull_request.source_ref_parts.name,
26 'target_ref_type': pull_request.target_ref_parts.type,
27 'target_ref_name': pull_request.target_ref_parts.name,
28 'repo_url': pull_request_source_repo_url
29 }
30 %>
14 ${self.subject()}
31 ${self.subject()}
15
32
16
33
17 ${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % {
34 ${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % data)}
18 'source_ref_type': pull_request.source_ref_parts.type,
19 'source_ref_name': pull_request.source_ref_parts.name,
20 'target_ref_type': pull_request.target_ref_parts.type,
21 'target_ref_name': pull_request.target_ref_parts.name,
22 'repo_url': pull_request_source_repo_url
23 })}
24
35
25
36
26 * ${_('Link')}: ${pull_request_url}
37 * ${_('Link')}: ${pull_request_url}
27
38
28 * ${_('Title')}: ${pull_request.title}
39 * ${_('Title')}: ${pull_request.title}
29
40
30 * ${_('Description')}:
41 * ${_('Description')}:
31
42
32 ${pull_request.description}
43 ${pull_request.description}
33
44
34
45
35 * ${ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
46 * ${ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
36
47
37 % for commit_id, message in pull_request_commits:
48 % for commit_id, message in pull_request_commits:
38 - ${h.short_id(commit_id)}
49 - ${h.short_id(commit_id)}
39 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
50 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
40
51
41 % endfor
52 % endfor
42
53
43 ${self.plaintext_footer()}
54 ${self.plaintext_footer()}
44 </%def>
55 </%def>
45
56 <%
57 data = {
58 'user': h.person(user),
59 'pr_id': pull_request.pull_request_id,
60 'pr_title': pull_request.title,
61 'source_ref_type': pull_request.source_ref_parts.type,
62 'source_ref_name': pull_request.source_ref_parts.name,
63 'target_ref_type': pull_request.target_ref_parts.type,
64 'target_ref_name': pull_request.target_ref_parts.name,
65 'repo_url': pull_request_source_repo_url,
66 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
67 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url)
68 }
69 %>
46 <table style="text-align:left;vertical-align:middle;">
70 <table style="text-align:left;vertical-align:middle;">
47 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${pull_request_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % { 'user': h.person(user), 'pr_title': pull_request.title, 'pr_id': pull_request.pull_request_id } }</a></h4></td></tr>
71 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${pull_request_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % data }</a></h4></td></tr>
48 <tr><td style="padding-right:20px;padding-top:15px;">${_('Title')}</td><td style="padding-top:15px;">${pull_request.title}</td></tr>
72 <tr><td style="padding-right:20px;padding-top:15px;">${_('Title')}</td><td style="padding-top:15px;">${pull_request.title}</td></tr>
49 <tr><td style="padding-right:20px;">${_('Source')}</td><td><pre style="display:inline;border-radius:2px;color:#666666;font-size:12px;background-color:#f9f9f9;padding:.2em;border:1px solid #979797;">${pull_request.source_ref_parts.name}</pre>${h.literal(_('%(source_ref_type)s of %(source_repo_url)s') % {'source_ref_type': pull_request.source_ref_parts.type, 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url)})}</td></tr>
73 <tr><td style="padding-right:20px;">${_('Source')}</td><td>${base.tag_button(pull_request.source_ref_parts.name)} ${h.literal(_('%(source_ref_type)s of %(source_repo_url)s') % data)}</td></tr>
50 <tr><td style="padding-right:20px;">${_('Target')}</td><td><pre style="display:inline;border-radius:2px;color:#666666;font-size:12px;background-color:#f9f9f9;padding:.2em;border:1px solid #979797;">${pull_request.target_ref_parts.name}</pre>${h.literal(_('%(target_ref_type)s of %(target_repo_url)s') % {'target_ref_type': pull_request.target_ref_parts.type, 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url)})}</td></tr>
74 <tr><td style="padding-right:20px;">${_('Target')}</td><td>${base.tag_button(pull_request.target_ref_parts.name)} ${h.literal(_('%(target_ref_type)s of %(target_repo_url)s') % data)}</td></tr>
51 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${pull_request.description}</td></tr>
75 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${pull_request.description}</td></tr>
52 <tr><td style="padding-right:20px;">${ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}</td>
76 <tr><td style="padding-right:20px;">${ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}</td>
53 <td><ol style="margin:0 0 0 1em;padding:0;text-align:left;">
77 <td><ol style="margin:0 0 0 1em;padding:0;text-align:left;">
54 % for commit_id, message in pull_request_commits:
78 % for commit_id, message in pull_request_commits:
55 <li style="margin:0 0 1em;"><pre style="margin:0 0 .5em">${h.short_id(commit_id)}</pre>
79 <li style="margin:0 0 1em;"><pre style="margin:0 0 .5em">${h.short_id(commit_id)}</pre>
56 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
80 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
57 </li>
81 </li>
58 % endfor
82 % endfor
59 </ol></td>
83 </ol></td>
60 </tr>
84 </tr>
61 </table>
85 </table>
General Comments 0
You need to be logged in to leave comments. Login now