##// END OF EJS Templates
reviewers: optimize diff data, and creation of PR with advanced default reviewers
marcink -
r4510:b532b1b7 stable
parent child Browse files
Show More
@@ -1,1052 +1,1056 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib import channelstream
29 from rhodecode.lib import channelstream
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 @jsonrpc_method()
44 @jsonrpc_method()
45 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
45 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
46 merge_state=Optional(False)):
46 merge_state=Optional(False)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57 :param merge_state: Optional calculate merge state for each repository.
57 :param merge_state: Optional calculate merge state for each repository.
58 This could result in longer time to fetch the data
58 This could result in longer time to fetch the data
59 :type merge_state: bool
59 :type merge_state: bool
60
60
61 Example output:
61 Example output:
62
62
63 .. code-block:: bash
63 .. code-block:: bash
64
64
65 "id": <id_given_in_input>,
65 "id": <id_given_in_input>,
66 "result":
66 "result":
67 {
67 {
68 "pull_request_id": "<pull_request_id>",
68 "pull_request_id": "<pull_request_id>",
69 "url": "<url>",
69 "url": "<url>",
70 "title": "<title>",
70 "title": "<title>",
71 "description": "<description>",
71 "description": "<description>",
72 "status" : "<status>",
72 "status" : "<status>",
73 "created_on": "<date_time_created>",
73 "created_on": "<date_time_created>",
74 "updated_on": "<date_time_updated>",
74 "updated_on": "<date_time_updated>",
75 "versions": "<number_or_versions_of_pr>",
75 "versions": "<number_or_versions_of_pr>",
76 "commit_ids": [
76 "commit_ids": [
77 ...
77 ...
78 "<commit_id>",
78 "<commit_id>",
79 "<commit_id>",
79 "<commit_id>",
80 ...
80 ...
81 ],
81 ],
82 "review_status": "<review_status>",
82 "review_status": "<review_status>",
83 "mergeable": {
83 "mergeable": {
84 "status": "<bool>",
84 "status": "<bool>",
85 "message": "<message>",
85 "message": "<message>",
86 },
86 },
87 "source": {
87 "source": {
88 "clone_url": "<clone_url>",
88 "clone_url": "<clone_url>",
89 "repository": "<repository_name>",
89 "repository": "<repository_name>",
90 "reference":
90 "reference":
91 {
91 {
92 "name": "<name>",
92 "name": "<name>",
93 "type": "<type>",
93 "type": "<type>",
94 "commit_id": "<commit_id>",
94 "commit_id": "<commit_id>",
95 }
95 }
96 },
96 },
97 "target": {
97 "target": {
98 "clone_url": "<clone_url>",
98 "clone_url": "<clone_url>",
99 "repository": "<repository_name>",
99 "repository": "<repository_name>",
100 "reference":
100 "reference":
101 {
101 {
102 "name": "<name>",
102 "name": "<name>",
103 "type": "<type>",
103 "type": "<type>",
104 "commit_id": "<commit_id>",
104 "commit_id": "<commit_id>",
105 }
105 }
106 },
106 },
107 "merge": {
107 "merge": {
108 "clone_url": "<clone_url>",
108 "clone_url": "<clone_url>",
109 "reference":
109 "reference":
110 {
110 {
111 "name": "<name>",
111 "name": "<name>",
112 "type": "<type>",
112 "type": "<type>",
113 "commit_id": "<commit_id>",
113 "commit_id": "<commit_id>",
114 }
114 }
115 },
115 },
116 "author": <user_obj>,
116 "author": <user_obj>,
117 "reviewers": [
117 "reviewers": [
118 ...
118 ...
119 {
119 {
120 "user": "<user_obj>",
120 "user": "<user_obj>",
121 "review_status": "<review_status>",
121 "review_status": "<review_status>",
122 }
122 }
123 ...
123 ...
124 ]
124 ]
125 },
125 },
126 "error": null
126 "error": null
127 """
127 """
128
128
129 pull_request = get_pull_request_or_error(pullrequestid)
129 pull_request = get_pull_request_or_error(pullrequestid)
130 if Optional.extract(repoid):
130 if Optional.extract(repoid):
131 repo = get_repo_or_error(repoid)
131 repo = get_repo_or_error(repoid)
132 else:
132 else:
133 repo = pull_request.target_repo
133 repo = pull_request.target_repo
134
134
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 raise JSONRPCError('repository `%s` or pull request `%s` '
136 raise JSONRPCError('repository `%s` or pull request `%s` '
137 'does not exist' % (repoid, pullrequestid))
137 'does not exist' % (repoid, pullrequestid))
138
138
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 # otherwise we can lock the repo on calculation of merge state while update/merge
140 # otherwise we can lock the repo on calculation of merge state while update/merge
141 # is happening.
141 # is happening.
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 data = pull_request.get_api_data(with_merge_state=merge_state)
144 data = pull_request.get_api_data(with_merge_state=merge_state)
145 return data
145 return data
146
146
147
147
148 @jsonrpc_method()
148 @jsonrpc_method()
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 merge_state=Optional(False)):
150 merge_state=Optional(False)):
151 """
151 """
152 Get all pull requests from the repository specified in `repoid`.
152 Get all pull requests from the repository specified in `repoid`.
153
153
154 :param apiuser: This is filled automatically from the |authtoken|.
154 :param apiuser: This is filled automatically from the |authtoken|.
155 :type apiuser: AuthUser
155 :type apiuser: AuthUser
156 :param repoid: Optional repository name or repository ID.
156 :param repoid: Optional repository name or repository ID.
157 :type repoid: str or int
157 :type repoid: str or int
158 :param status: Only return pull requests with the specified status.
158 :param status: Only return pull requests with the specified status.
159 Valid options are.
159 Valid options are.
160 * ``new`` (default)
160 * ``new`` (default)
161 * ``open``
161 * ``open``
162 * ``closed``
162 * ``closed``
163 :type status: str
163 :type status: str
164 :param merge_state: Optional calculate merge state for each repository.
164 :param merge_state: Optional calculate merge state for each repository.
165 This could result in longer time to fetch the data
165 This could result in longer time to fetch the data
166 :type merge_state: bool
166 :type merge_state: bool
167
167
168 Example output:
168 Example output:
169
169
170 .. code-block:: bash
170 .. code-block:: bash
171
171
172 "id": <id_given_in_input>,
172 "id": <id_given_in_input>,
173 "result":
173 "result":
174 [
174 [
175 ...
175 ...
176 {
176 {
177 "pull_request_id": "<pull_request_id>",
177 "pull_request_id": "<pull_request_id>",
178 "url": "<url>",
178 "url": "<url>",
179 "title" : "<title>",
179 "title" : "<title>",
180 "description": "<description>",
180 "description": "<description>",
181 "status": "<status>",
181 "status": "<status>",
182 "created_on": "<date_time_created>",
182 "created_on": "<date_time_created>",
183 "updated_on": "<date_time_updated>",
183 "updated_on": "<date_time_updated>",
184 "commit_ids": [
184 "commit_ids": [
185 ...
185 ...
186 "<commit_id>",
186 "<commit_id>",
187 "<commit_id>",
187 "<commit_id>",
188 ...
188 ...
189 ],
189 ],
190 "review_status": "<review_status>",
190 "review_status": "<review_status>",
191 "mergeable": {
191 "mergeable": {
192 "status": "<bool>",
192 "status": "<bool>",
193 "message: "<message>",
193 "message: "<message>",
194 },
194 },
195 "source": {
195 "source": {
196 "clone_url": "<clone_url>",
196 "clone_url": "<clone_url>",
197 "reference":
197 "reference":
198 {
198 {
199 "name": "<name>",
199 "name": "<name>",
200 "type": "<type>",
200 "type": "<type>",
201 "commit_id": "<commit_id>",
201 "commit_id": "<commit_id>",
202 }
202 }
203 },
203 },
204 "target": {
204 "target": {
205 "clone_url": "<clone_url>",
205 "clone_url": "<clone_url>",
206 "reference":
206 "reference":
207 {
207 {
208 "name": "<name>",
208 "name": "<name>",
209 "type": "<type>",
209 "type": "<type>",
210 "commit_id": "<commit_id>",
210 "commit_id": "<commit_id>",
211 }
211 }
212 },
212 },
213 "merge": {
213 "merge": {
214 "clone_url": "<clone_url>",
214 "clone_url": "<clone_url>",
215 "reference":
215 "reference":
216 {
216 {
217 "name": "<name>",
217 "name": "<name>",
218 "type": "<type>",
218 "type": "<type>",
219 "commit_id": "<commit_id>",
219 "commit_id": "<commit_id>",
220 }
220 }
221 },
221 },
222 "author": <user_obj>,
222 "author": <user_obj>,
223 "reviewers": [
223 "reviewers": [
224 ...
224 ...
225 {
225 {
226 "user": "<user_obj>",
226 "user": "<user_obj>",
227 "review_status": "<review_status>",
227 "review_status": "<review_status>",
228 }
228 }
229 ...
229 ...
230 ]
230 ]
231 }
231 }
232 ...
232 ...
233 ],
233 ],
234 "error": null
234 "error": null
235
235
236 """
236 """
237 repo = get_repo_or_error(repoid)
237 repo = get_repo_or_error(repoid)
238 if not has_superadmin_permission(apiuser):
238 if not has_superadmin_permission(apiuser):
239 _perms = (
239 _perms = (
240 'repository.admin', 'repository.write', 'repository.read',)
240 'repository.admin', 'repository.write', 'repository.read',)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
242
242
243 status = Optional.extract(status)
243 status = Optional.extract(status)
244 merge_state = Optional.extract(merge_state, binary=True)
244 merge_state = Optional.extract(merge_state, binary=True)
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 order_by='id', order_dir='desc')
246 order_by='id', order_dir='desc')
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 return data
248 return data
249
249
250
250
251 @jsonrpc_method()
251 @jsonrpc_method()
252 def merge_pull_request(
252 def merge_pull_request(
253 request, apiuser, pullrequestid, repoid=Optional(None),
253 request, apiuser, pullrequestid, repoid=Optional(None),
254 userid=Optional(OAttr('apiuser'))):
254 userid=Optional(OAttr('apiuser'))):
255 """
255 """
256 Merge the pull request specified by `pullrequestid` into its target
256 Merge the pull request specified by `pullrequestid` into its target
257 repository.
257 repository.
258
258
259 :param apiuser: This is filled automatically from the |authtoken|.
259 :param apiuser: This is filled automatically from the |authtoken|.
260 :type apiuser: AuthUser
260 :type apiuser: AuthUser
261 :param repoid: Optional, repository name or repository ID of the
261 :param repoid: Optional, repository name or repository ID of the
262 target repository to which the |pr| is to be merged.
262 target repository to which the |pr| is to be merged.
263 :type repoid: str or int
263 :type repoid: str or int
264 :param pullrequestid: ID of the pull request which shall be merged.
264 :param pullrequestid: ID of the pull request which shall be merged.
265 :type pullrequestid: int
265 :type pullrequestid: int
266 :param userid: Merge the pull request as this user.
266 :param userid: Merge the pull request as this user.
267 :type userid: Optional(str or int)
267 :type userid: Optional(str or int)
268
268
269 Example output:
269 Example output:
270
270
271 .. code-block:: bash
271 .. code-block:: bash
272
272
273 "id": <id_given_in_input>,
273 "id": <id_given_in_input>,
274 "result": {
274 "result": {
275 "executed": "<bool>",
275 "executed": "<bool>",
276 "failure_reason": "<int>",
276 "failure_reason": "<int>",
277 "merge_status_message": "<str>",
277 "merge_status_message": "<str>",
278 "merge_commit_id": "<merge_commit_id>",
278 "merge_commit_id": "<merge_commit_id>",
279 "possible": "<bool>",
279 "possible": "<bool>",
280 "merge_ref": {
280 "merge_ref": {
281 "commit_id": "<commit_id>",
281 "commit_id": "<commit_id>",
282 "type": "<type>",
282 "type": "<type>",
283 "name": "<name>"
283 "name": "<name>"
284 }
284 }
285 },
285 },
286 "error": null
286 "error": null
287 """
287 """
288 pull_request = get_pull_request_or_error(pullrequestid)
288 pull_request = get_pull_request_or_error(pullrequestid)
289 if Optional.extract(repoid):
289 if Optional.extract(repoid):
290 repo = get_repo_or_error(repoid)
290 repo = get_repo_or_error(repoid)
291 else:
291 else:
292 repo = pull_request.target_repo
292 repo = pull_request.target_repo
293 auth_user = apiuser
293 auth_user = apiuser
294
294
295 if not isinstance(userid, Optional):
295 if not isinstance(userid, Optional):
296 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
297 user=apiuser, repo_name=repo.repo_name)
297 user=apiuser, repo_name=repo.repo_name)
298 if has_superadmin_permission(apiuser) or is_repo_admin:
298 if has_superadmin_permission(apiuser) or is_repo_admin:
299 apiuser = get_user_or_error(userid)
299 apiuser = get_user_or_error(userid)
300 auth_user = apiuser.AuthUser()
300 auth_user = apiuser.AuthUser()
301 else:
301 else:
302 raise JSONRPCError('userid is not the same as your user')
302 raise JSONRPCError('userid is not the same as your user')
303
303
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
305 raise JSONRPCError(
305 raise JSONRPCError(
306 'Operation forbidden because pull request is in state {}, '
306 'Operation forbidden because pull request is in state {}, '
307 'only state {} is allowed.'.format(
307 'only state {} is allowed.'.format(
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
309
309
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
312 translator=request.translate)
312 translator=request.translate)
313 merge_possible = not check.failed
313 merge_possible = not check.failed
314
314
315 if not merge_possible:
315 if not merge_possible:
316 error_messages = []
316 error_messages = []
317 for err_type, error_msg in check.errors:
317 for err_type, error_msg in check.errors:
318 error_msg = request.translate(error_msg)
318 error_msg = request.translate(error_msg)
319 error_messages.append(error_msg)
319 error_messages.append(error_msg)
320
320
321 reasons = ','.join(error_messages)
321 reasons = ','.join(error_messages)
322 raise JSONRPCError(
322 raise JSONRPCError(
323 'merge not possible for following reasons: {}'.format(reasons))
323 'merge not possible for following reasons: {}'.format(reasons))
324
324
325 target_repo = pull_request.target_repo
325 target_repo = pull_request.target_repo
326 extras = vcs_operation_context(
326 extras = vcs_operation_context(
327 request.environ, repo_name=target_repo.repo_name,
327 request.environ, repo_name=target_repo.repo_name,
328 username=auth_user.username, action='push',
328 username=auth_user.username, action='push',
329 scm=target_repo.repo_type)
329 scm=target_repo.repo_type)
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
331 merge_response = PullRequestModel().merge_repo(
331 merge_response = PullRequestModel().merge_repo(
332 pull_request, apiuser, extras=extras)
332 pull_request, apiuser, extras=extras)
333 if merge_response.executed:
333 if merge_response.executed:
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
335
335
336 Session().commit()
336 Session().commit()
337
337
338 # In previous versions the merge response directly contained the merge
338 # In previous versions the merge response directly contained the merge
339 # commit id. It is now contained in the merge reference object. To be
339 # commit id. It is now contained in the merge reference object. To be
340 # backwards compatible we have to extract it again.
340 # backwards compatible we have to extract it again.
341 merge_response = merge_response.asdict()
341 merge_response = merge_response.asdict()
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
343
343
344 return merge_response
344 return merge_response
345
345
346
346
347 @jsonrpc_method()
347 @jsonrpc_method()
348 def get_pull_request_comments(
348 def get_pull_request_comments(
349 request, apiuser, pullrequestid, repoid=Optional(None)):
349 request, apiuser, pullrequestid, repoid=Optional(None)):
350 """
350 """
351 Get all comments of pull request specified with the `pullrequestid`
351 Get all comments of pull request specified with the `pullrequestid`
352
352
353 :param apiuser: This is filled automatically from the |authtoken|.
353 :param apiuser: This is filled automatically from the |authtoken|.
354 :type apiuser: AuthUser
354 :type apiuser: AuthUser
355 :param repoid: Optional repository name or repository ID.
355 :param repoid: Optional repository name or repository ID.
356 :type repoid: str or int
356 :type repoid: str or int
357 :param pullrequestid: The pull request ID.
357 :param pullrequestid: The pull request ID.
358 :type pullrequestid: int
358 :type pullrequestid: int
359
359
360 Example output:
360 Example output:
361
361
362 .. code-block:: bash
362 .. code-block:: bash
363
363
364 id : <id_given_in_input>
364 id : <id_given_in_input>
365 result : [
365 result : [
366 {
366 {
367 "comment_author": {
367 "comment_author": {
368 "active": true,
368 "active": true,
369 "full_name_or_username": "Tom Gore",
369 "full_name_or_username": "Tom Gore",
370 "username": "admin"
370 "username": "admin"
371 },
371 },
372 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_created_on": "2017-01-02T18:43:45.533",
373 "comment_f_path": null,
373 "comment_f_path": null,
374 "comment_id": 25,
374 "comment_id": 25,
375 "comment_lineno": null,
375 "comment_lineno": null,
376 "comment_status": {
376 "comment_status": {
377 "status": "under_review",
377 "status": "under_review",
378 "status_lbl": "Under Review"
378 "status_lbl": "Under Review"
379 },
379 },
380 "comment_text": "Example text",
380 "comment_text": "Example text",
381 "comment_type": null,
381 "comment_type": null,
382 "comment_last_version: 0,
382 "comment_last_version: 0,
383 "pull_request_version": null,
383 "pull_request_version": null,
384 "comment_commit_id": None,
384 "comment_commit_id": None,
385 "comment_pull_request_id": <pull_request_id>
385 "comment_pull_request_id": <pull_request_id>
386 }
386 }
387 ],
387 ],
388 error : null
388 error : null
389 """
389 """
390
390
391 pull_request = get_pull_request_or_error(pullrequestid)
391 pull_request = get_pull_request_or_error(pullrequestid)
392 if Optional.extract(repoid):
392 if Optional.extract(repoid):
393 repo = get_repo_or_error(repoid)
393 repo = get_repo_or_error(repoid)
394 else:
394 else:
395 repo = pull_request.target_repo
395 repo = pull_request.target_repo
396
396
397 if not PullRequestModel().check_user_read(
397 if not PullRequestModel().check_user_read(
398 pull_request, apiuser, api=True):
398 pull_request, apiuser, api=True):
399 raise JSONRPCError('repository `%s` or pull request `%s` '
399 raise JSONRPCError('repository `%s` or pull request `%s` '
400 'does not exist' % (repoid, pullrequestid))
400 'does not exist' % (repoid, pullrequestid))
401
401
402 (pull_request_latest,
402 (pull_request_latest,
403 pull_request_at_ver,
403 pull_request_at_ver,
404 pull_request_display_obj,
404 pull_request_display_obj,
405 at_version) = PullRequestModel().get_pr_version(
405 at_version) = PullRequestModel().get_pr_version(
406 pull_request.pull_request_id, version=None)
406 pull_request.pull_request_id, version=None)
407
407
408 versions = pull_request_display_obj.versions()
408 versions = pull_request_display_obj.versions()
409 ver_map = {
409 ver_map = {
410 ver.pull_request_version_id: cnt
410 ver.pull_request_version_id: cnt
411 for cnt, ver in enumerate(versions, 1)
411 for cnt, ver in enumerate(versions, 1)
412 }
412 }
413
413
414 # GENERAL COMMENTS with versions #
414 # GENERAL COMMENTS with versions #
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
416 q = q.order_by(ChangesetComment.comment_id.asc())
416 q = q.order_by(ChangesetComment.comment_id.asc())
417 general_comments = q.all()
417 general_comments = q.all()
418
418
419 # INLINE COMMENTS with versions #
419 # INLINE COMMENTS with versions #
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
421 q = q.order_by(ChangesetComment.comment_id.asc())
421 q = q.order_by(ChangesetComment.comment_id.asc())
422 inline_comments = q.all()
422 inline_comments = q.all()
423
423
424 data = []
424 data = []
425 for comment in inline_comments + general_comments:
425 for comment in inline_comments + general_comments:
426 full_data = comment.get_api_data()
426 full_data = comment.get_api_data()
427 pr_version_id = None
427 pr_version_id = None
428 if comment.pull_request_version_id:
428 if comment.pull_request_version_id:
429 pr_version_id = 'v{}'.format(
429 pr_version_id = 'v{}'.format(
430 ver_map[comment.pull_request_version_id])
430 ver_map[comment.pull_request_version_id])
431
431
432 # sanitize some entries
432 # sanitize some entries
433
433
434 full_data['pull_request_version'] = pr_version_id
434 full_data['pull_request_version'] = pr_version_id
435 full_data['comment_author'] = {
435 full_data['comment_author'] = {
436 'username': full_data['comment_author'].username,
436 'username': full_data['comment_author'].username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
438 'active': full_data['comment_author'].active,
438 'active': full_data['comment_author'].active,
439 }
439 }
440
440
441 if full_data['comment_status']:
441 if full_data['comment_status']:
442 full_data['comment_status'] = {
442 full_data['comment_status'] = {
443 'status': full_data['comment_status'][0].status,
443 'status': full_data['comment_status'][0].status,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
445 }
445 }
446 else:
446 else:
447 full_data['comment_status'] = {}
447 full_data['comment_status'] = {}
448
448
449 data.append(full_data)
449 data.append(full_data)
450 return data
450 return data
451
451
452
452
453 @jsonrpc_method()
453 @jsonrpc_method()
454 def comment_pull_request(
454 def comment_pull_request(
455 request, apiuser, pullrequestid, repoid=Optional(None),
455 request, apiuser, pullrequestid, repoid=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
460 """
460 """
461 Comment on the pull request specified with the `pullrequestid`,
461 Comment on the pull request specified with the `pullrequestid`,
462 in the |repo| specified by the `repoid`, and optionally change the
462 in the |repo| specified by the `repoid`, and optionally change the
463 review status.
463 review status.
464
464
465 :param apiuser: This is filled automatically from the |authtoken|.
465 :param apiuser: This is filled automatically from the |authtoken|.
466 :type apiuser: AuthUser
466 :type apiuser: AuthUser
467 :param repoid: Optional repository name or repository ID.
467 :param repoid: Optional repository name or repository ID.
468 :type repoid: str or int
468 :type repoid: str or int
469 :param pullrequestid: The pull request ID.
469 :param pullrequestid: The pull request ID.
470 :type pullrequestid: int
470 :type pullrequestid: int
471 :param commit_id: Specify the commit_id for which to set a comment. If
471 :param commit_id: Specify the commit_id for which to set a comment. If
472 given commit_id is different than latest in the PR status
472 given commit_id is different than latest in the PR status
473 change won't be performed.
473 change won't be performed.
474 :type commit_id: str
474 :type commit_id: str
475 :param message: The text content of the comment.
475 :param message: The text content of the comment.
476 :type message: str
476 :type message: str
477 :param status: (**Optional**) Set the approval status of the pull
477 :param status: (**Optional**) Set the approval status of the pull
478 request. One of: 'not_reviewed', 'approved', 'rejected',
478 request. One of: 'not_reviewed', 'approved', 'rejected',
479 'under_review'
479 'under_review'
480 :type status: str
480 :type status: str
481 :param comment_type: Comment type, one of: 'note', 'todo'
481 :param comment_type: Comment type, one of: 'note', 'todo'
482 :type comment_type: Optional(str), default: 'note'
482 :type comment_type: Optional(str), default: 'note'
483 :param resolves_comment_id: id of comment which this one will resolve
483 :param resolves_comment_id: id of comment which this one will resolve
484 :type resolves_comment_id: Optional(int)
484 :type resolves_comment_id: Optional(int)
485 :param extra_recipients: list of user ids or usernames to add
485 :param extra_recipients: list of user ids or usernames to add
486 notifications for this comment. Acts like a CC for notification
486 notifications for this comment. Acts like a CC for notification
487 :type extra_recipients: Optional(list)
487 :type extra_recipients: Optional(list)
488 :param userid: Comment on the pull request as this user
488 :param userid: Comment on the pull request as this user
489 :type userid: Optional(str or int)
489 :type userid: Optional(str or int)
490 :param send_email: Define if this comment should also send email notification
490 :param send_email: Define if this comment should also send email notification
491 :type send_email: Optional(bool)
491 :type send_email: Optional(bool)
492
492
493 Example output:
493 Example output:
494
494
495 .. code-block:: bash
495 .. code-block:: bash
496
496
497 id : <id_given_in_input>
497 id : <id_given_in_input>
498 result : {
498 result : {
499 "pull_request_id": "<Integer>",
499 "pull_request_id": "<Integer>",
500 "comment_id": "<Integer>",
500 "comment_id": "<Integer>",
501 "status": {"given": <given_status>,
501 "status": {"given": <given_status>,
502 "was_changed": <bool status_was_actually_changed> },
502 "was_changed": <bool status_was_actually_changed> },
503 },
503 },
504 error : null
504 error : null
505 """
505 """
506 _ = request.translate
506 _ = request.translate
507
507
508 pull_request = get_pull_request_or_error(pullrequestid)
508 pull_request = get_pull_request_or_error(pullrequestid)
509 if Optional.extract(repoid):
509 if Optional.extract(repoid):
510 repo = get_repo_or_error(repoid)
510 repo = get_repo_or_error(repoid)
511 else:
511 else:
512 repo = pull_request.target_repo
512 repo = pull_request.target_repo
513
513
514 db_repo_name = repo.repo_name
514 db_repo_name = repo.repo_name
515 auth_user = apiuser
515 auth_user = apiuser
516 if not isinstance(userid, Optional):
516 if not isinstance(userid, Optional):
517 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
517 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
518 user=apiuser, repo_name=db_repo_name)
518 user=apiuser, repo_name=db_repo_name)
519 if has_superadmin_permission(apiuser) or is_repo_admin:
519 if has_superadmin_permission(apiuser) or is_repo_admin:
520 apiuser = get_user_or_error(userid)
520 apiuser = get_user_or_error(userid)
521 auth_user = apiuser.AuthUser()
521 auth_user = apiuser.AuthUser()
522 else:
522 else:
523 raise JSONRPCError('userid is not the same as your user')
523 raise JSONRPCError('userid is not the same as your user')
524
524
525 if pull_request.is_closed():
525 if pull_request.is_closed():
526 raise JSONRPCError(
526 raise JSONRPCError(
527 'pull request `%s` comment failed, pull request is closed' % (
527 'pull request `%s` comment failed, pull request is closed' % (
528 pullrequestid,))
528 pullrequestid,))
529
529
530 if not PullRequestModel().check_user_read(
530 if not PullRequestModel().check_user_read(
531 pull_request, apiuser, api=True):
531 pull_request, apiuser, api=True):
532 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
532 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
533 message = Optional.extract(message)
533 message = Optional.extract(message)
534 status = Optional.extract(status)
534 status = Optional.extract(status)
535 commit_id = Optional.extract(commit_id)
535 commit_id = Optional.extract(commit_id)
536 comment_type = Optional.extract(comment_type)
536 comment_type = Optional.extract(comment_type)
537 resolves_comment_id = Optional.extract(resolves_comment_id)
537 resolves_comment_id = Optional.extract(resolves_comment_id)
538 extra_recipients = Optional.extract(extra_recipients)
538 extra_recipients = Optional.extract(extra_recipients)
539 send_email = Optional.extract(send_email, binary=True)
539 send_email = Optional.extract(send_email, binary=True)
540
540
541 if not message and not status:
541 if not message and not status:
542 raise JSONRPCError(
542 raise JSONRPCError(
543 'Both message and status parameters are missing. '
543 'Both message and status parameters are missing. '
544 'At least one is required.')
544 'At least one is required.')
545
545
546 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
546 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
547 status is not None):
547 status is not None):
548 raise JSONRPCError('Unknown comment status: `%s`' % status)
548 raise JSONRPCError('Unknown comment status: `%s`' % status)
549
549
550 if commit_id and commit_id not in pull_request.revisions:
550 if commit_id and commit_id not in pull_request.revisions:
551 raise JSONRPCError(
551 raise JSONRPCError(
552 'Invalid commit_id `%s` for this pull request.' % commit_id)
552 'Invalid commit_id `%s` for this pull request.' % commit_id)
553
553
554 allowed_to_change_status = PullRequestModel().check_user_change_status(
554 allowed_to_change_status = PullRequestModel().check_user_change_status(
555 pull_request, apiuser)
555 pull_request, apiuser)
556
556
557 # if commit_id is passed re-validated if user is allowed to change status
557 # if commit_id is passed re-validated if user is allowed to change status
558 # based on latest commit_id from the PR
558 # based on latest commit_id from the PR
559 if commit_id:
559 if commit_id:
560 commit_idx = pull_request.revisions.index(commit_id)
560 commit_idx = pull_request.revisions.index(commit_id)
561 if commit_idx != 0:
561 if commit_idx != 0:
562 allowed_to_change_status = False
562 allowed_to_change_status = False
563
563
564 if resolves_comment_id:
564 if resolves_comment_id:
565 comment = ChangesetComment.get(resolves_comment_id)
565 comment = ChangesetComment.get(resolves_comment_id)
566 if not comment:
566 if not comment:
567 raise JSONRPCError(
567 raise JSONRPCError(
568 'Invalid resolves_comment_id `%s` for this pull request.'
568 'Invalid resolves_comment_id `%s` for this pull request.'
569 % resolves_comment_id)
569 % resolves_comment_id)
570 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
570 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
571 raise JSONRPCError(
571 raise JSONRPCError(
572 'Comment `%s` is wrong type for setting status to resolved.'
572 'Comment `%s` is wrong type for setting status to resolved.'
573 % resolves_comment_id)
573 % resolves_comment_id)
574
574
575 text = message
575 text = message
576 status_label = ChangesetStatus.get_status_lbl(status)
576 status_label = ChangesetStatus.get_status_lbl(status)
577 if status and allowed_to_change_status:
577 if status and allowed_to_change_status:
578 st_message = ('Status change %(transition_icon)s %(status)s'
578 st_message = ('Status change %(transition_icon)s %(status)s'
579 % {'transition_icon': '>', 'status': status_label})
579 % {'transition_icon': '>', 'status': status_label})
580 text = message or st_message
580 text = message or st_message
581
581
582 rc_config = SettingsModel().get_all_settings()
582 rc_config = SettingsModel().get_all_settings()
583 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
583 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
584
584
585 status_change = status and allowed_to_change_status
585 status_change = status and allowed_to_change_status
586 comment = CommentsModel().create(
586 comment = CommentsModel().create(
587 text=text,
587 text=text,
588 repo=pull_request.target_repo.repo_id,
588 repo=pull_request.target_repo.repo_id,
589 user=apiuser.user_id,
589 user=apiuser.user_id,
590 pull_request=pull_request.pull_request_id,
590 pull_request=pull_request.pull_request_id,
591 f_path=None,
591 f_path=None,
592 line_no=None,
592 line_no=None,
593 status_change=(status_label if status_change else None),
593 status_change=(status_label if status_change else None),
594 status_change_type=(status if status_change else None),
594 status_change_type=(status if status_change else None),
595 closing_pr=False,
595 closing_pr=False,
596 renderer=renderer,
596 renderer=renderer,
597 comment_type=comment_type,
597 comment_type=comment_type,
598 resolves_comment_id=resolves_comment_id,
598 resolves_comment_id=resolves_comment_id,
599 auth_user=auth_user,
599 auth_user=auth_user,
600 extra_recipients=extra_recipients,
600 extra_recipients=extra_recipients,
601 send_email=send_email
601 send_email=send_email
602 )
602 )
603 is_inline = bool(comment.f_path and comment.line_no)
603 is_inline = bool(comment.f_path and comment.line_no)
604
604
605 if allowed_to_change_status and status:
605 if allowed_to_change_status and status:
606 old_calculated_status = pull_request.calculated_review_status()
606 old_calculated_status = pull_request.calculated_review_status()
607 ChangesetStatusModel().set_status(
607 ChangesetStatusModel().set_status(
608 pull_request.target_repo.repo_id,
608 pull_request.target_repo.repo_id,
609 status,
609 status,
610 apiuser.user_id,
610 apiuser.user_id,
611 comment,
611 comment,
612 pull_request=pull_request.pull_request_id
612 pull_request=pull_request.pull_request_id
613 )
613 )
614 Session().flush()
614 Session().flush()
615
615
616 Session().commit()
616 Session().commit()
617
617
618 PullRequestModel().trigger_pull_request_hook(
618 PullRequestModel().trigger_pull_request_hook(
619 pull_request, apiuser, 'comment',
619 pull_request, apiuser, 'comment',
620 data={'comment': comment})
620 data={'comment': comment})
621
621
622 if allowed_to_change_status and status:
622 if allowed_to_change_status and status:
623 # we now calculate the status of pull request, and based on that
623 # we now calculate the status of pull request, and based on that
624 # calculation we set the commits status
624 # calculation we set the commits status
625 calculated_status = pull_request.calculated_review_status()
625 calculated_status = pull_request.calculated_review_status()
626 if old_calculated_status != calculated_status:
626 if old_calculated_status != calculated_status:
627 PullRequestModel().trigger_pull_request_hook(
627 PullRequestModel().trigger_pull_request_hook(
628 pull_request, apiuser, 'review_status_change',
628 pull_request, apiuser, 'review_status_change',
629 data={'status': calculated_status})
629 data={'status': calculated_status})
630
630
631 data = {
631 data = {
632 'pull_request_id': pull_request.pull_request_id,
632 'pull_request_id': pull_request.pull_request_id,
633 'comment_id': comment.comment_id if comment else None,
633 'comment_id': comment.comment_id if comment else None,
634 'status': {'given': status, 'was_changed': status_change},
634 'status': {'given': status, 'was_changed': status_change},
635 }
635 }
636
636
637 comment_broadcast_channel = channelstream.comment_channel(
637 comment_broadcast_channel = channelstream.comment_channel(
638 db_repo_name, pull_request_obj=pull_request)
638 db_repo_name, pull_request_obj=pull_request)
639
639
640 comment_data = data
640 comment_data = data
641 comment_type = 'inline' if is_inline else 'general'
641 comment_type = 'inline' if is_inline else 'general'
642 channelstream.comment_channelstream_push(
642 channelstream.comment_channelstream_push(
643 request, comment_broadcast_channel, apiuser,
643 request, comment_broadcast_channel, apiuser,
644 _('posted a new {} comment').format(comment_type),
644 _('posted a new {} comment').format(comment_type),
645 comment_data=comment_data)
645 comment_data=comment_data)
646
646
647 return data
647 return data
648
648
649
649
650 @jsonrpc_method()
650 @jsonrpc_method()
651 def create_pull_request(
651 def create_pull_request(
652 request, apiuser, source_repo, target_repo, source_ref, target_ref,
652 request, apiuser, source_repo, target_repo, source_ref, target_ref,
653 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
653 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
654 description_renderer=Optional(''), reviewers=Optional(None)):
654 description_renderer=Optional(''), reviewers=Optional(None)):
655 """
655 """
656 Creates a new pull request.
656 Creates a new pull request.
657
657
658 Accepts refs in the following formats:
658 Accepts refs in the following formats:
659
659
660 * branch:<branch_name>:<sha>
660 * branch:<branch_name>:<sha>
661 * branch:<branch_name>
661 * branch:<branch_name>
662 * bookmark:<bookmark_name>:<sha> (Mercurial only)
662 * bookmark:<bookmark_name>:<sha> (Mercurial only)
663 * bookmark:<bookmark_name> (Mercurial only)
663 * bookmark:<bookmark_name> (Mercurial only)
664
664
665 :param apiuser: This is filled automatically from the |authtoken|.
665 :param apiuser: This is filled automatically from the |authtoken|.
666 :type apiuser: AuthUser
666 :type apiuser: AuthUser
667 :param source_repo: Set the source repository name.
667 :param source_repo: Set the source repository name.
668 :type source_repo: str
668 :type source_repo: str
669 :param target_repo: Set the target repository name.
669 :param target_repo: Set the target repository name.
670 :type target_repo: str
670 :type target_repo: str
671 :param source_ref: Set the source ref name.
671 :param source_ref: Set the source ref name.
672 :type source_ref: str
672 :type source_ref: str
673 :param target_ref: Set the target ref name.
673 :param target_ref: Set the target ref name.
674 :type target_ref: str
674 :type target_ref: str
675 :param owner: user_id or username
675 :param owner: user_id or username
676 :type owner: Optional(str)
676 :type owner: Optional(str)
677 :param title: Optionally Set the pull request title, it's generated otherwise
677 :param title: Optionally Set the pull request title, it's generated otherwise
678 :type title: str
678 :type title: str
679 :param description: Set the pull request description.
679 :param description: Set the pull request description.
680 :type description: Optional(str)
680 :type description: Optional(str)
681 :type description_renderer: Optional(str)
681 :type description_renderer: Optional(str)
682 :param description_renderer: Set pull request renderer for the description.
682 :param description_renderer: Set pull request renderer for the description.
683 It should be 'rst', 'markdown' or 'plain'. If not give default
683 It should be 'rst', 'markdown' or 'plain'. If not give default
684 system renderer will be used
684 system renderer will be used
685 :param reviewers: Set the new pull request reviewers list.
685 :param reviewers: Set the new pull request reviewers list.
686 Reviewer defined by review rules will be added automatically to the
686 Reviewer defined by review rules will be added automatically to the
687 defined list.
687 defined list.
688 :type reviewers: Optional(list)
688 :type reviewers: Optional(list)
689 Accepts username strings or objects of the format:
689 Accepts username strings or objects of the format:
690
690
691 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
691 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
692 """
692 """
693
693
694 source_db_repo = get_repo_or_error(source_repo)
694 source_db_repo = get_repo_or_error(source_repo)
695 target_db_repo = get_repo_or_error(target_repo)
695 target_db_repo = get_repo_or_error(target_repo)
696 if not has_superadmin_permission(apiuser):
696 if not has_superadmin_permission(apiuser):
697 _perms = ('repository.admin', 'repository.write', 'repository.read',)
697 _perms = ('repository.admin', 'repository.write', 'repository.read',)
698 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
698 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
699
699
700 owner = validate_set_owner_permissions(apiuser, owner)
700 owner = validate_set_owner_permissions(apiuser, owner)
701
701
702 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
702 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
703 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
703 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
704
704
705 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
705 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
706 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
706 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
707
707
708 reviewer_objects = Optional.extract(reviewers) or []
708 reviewer_objects = Optional.extract(reviewers) or []
709
709
710 # serialize and validate passed in given reviewers
710 # serialize and validate passed in given reviewers
711 if reviewer_objects:
711 if reviewer_objects:
712 schema = ReviewerListSchema()
712 schema = ReviewerListSchema()
713 try:
713 try:
714 reviewer_objects = schema.deserialize(reviewer_objects)
714 reviewer_objects = schema.deserialize(reviewer_objects)
715 except Invalid as err:
715 except Invalid as err:
716 raise JSONRPCValidationError(colander_exc=err)
716 raise JSONRPCValidationError(colander_exc=err)
717
717
718 # validate users
718 # validate users
719 for reviewer_object in reviewer_objects:
719 for reviewer_object in reviewer_objects:
720 user = get_user_or_error(reviewer_object['username'])
720 user = get_user_or_error(reviewer_object['username'])
721 reviewer_object['user_id'] = user.user_id
721 reviewer_object['user_id'] = user.user_id
722
722
723 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
723 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
724 PullRequestModel().get_reviewer_functions()
724 PullRequestModel().get_reviewer_functions()
725
725
726 # recalculate reviewers logic, to make sure we can validate this
726 # recalculate reviewers logic, to make sure we can validate this
727 default_reviewers_data = get_default_reviewers_data(
727 default_reviewers_data = get_default_reviewers_data(
728 owner, source_db_repo,
728 owner,
729 source_commit, target_db_repo, target_commit)
729 source_repo,
730 Reference(source_type, source_name, source_commit_id),
731 target_repo,
732 Reference(target_type, target_name, target_commit_id)
733 )
730
734
731 # now MERGE our given with the calculated
735 # now MERGE our given with the calculated
732 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
736 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
733
737
734 try:
738 try:
735 reviewers = validate_default_reviewers(
739 reviewers = validate_default_reviewers(
736 reviewer_objects, default_reviewers_data)
740 reviewer_objects, default_reviewers_data)
737 except ValueError as e:
741 except ValueError as e:
738 raise JSONRPCError('Reviewers Validation: {}'.format(e))
742 raise JSONRPCError('Reviewers Validation: {}'.format(e))
739
743
740 title = Optional.extract(title)
744 title = Optional.extract(title)
741 if not title:
745 if not title:
742 title_source_ref = source_ref.split(':', 2)[1]
746 title_source_ref = source_ref.split(':', 2)[1]
743 title = PullRequestModel().generate_pullrequest_title(
747 title = PullRequestModel().generate_pullrequest_title(
744 source=source_repo,
748 source=source_repo,
745 source_ref=title_source_ref,
749 source_ref=title_source_ref,
746 target=target_repo
750 target=target_repo
747 )
751 )
748
752
749 diff_info = default_reviewers_data['diff_info']
753 diff_info = default_reviewers_data['diff_info']
750 common_ancestor_id = diff_info['ancestor']
754 common_ancestor_id = diff_info['ancestor']
751 commits = diff_info['commits']
755 commits = diff_info['commits']
752
756
753 if not common_ancestor_id:
757 if not common_ancestor_id:
754 raise JSONRPCError('no common ancestor found')
758 raise JSONRPCError('no common ancestor found')
755
759
756 if not commits:
760 if not commits:
757 raise JSONRPCError('no commits found')
761 raise JSONRPCError('no commits found')
758
762
759 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
763 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
760 revisions = [commit.raw_id for commit in reversed(commits)]
764 revisions = [commit.raw_id for commit in reversed(commits)]
761
765
762 # recalculate target ref based on ancestor
766 # recalculate target ref based on ancestor
763 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
767 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
764 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
768 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
765
769
766 # fetch renderer, if set fallback to plain in case of PR
770 # fetch renderer, if set fallback to plain in case of PR
767 rc_config = SettingsModel().get_all_settings()
771 rc_config = SettingsModel().get_all_settings()
768 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
772 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
769 description = Optional.extract(description)
773 description = Optional.extract(description)
770 description_renderer = Optional.extract(description_renderer) or default_system_renderer
774 description_renderer = Optional.extract(description_renderer) or default_system_renderer
771
775
772 pull_request = PullRequestModel().create(
776 pull_request = PullRequestModel().create(
773 created_by=owner.user_id,
777 created_by=owner.user_id,
774 source_repo=source_repo,
778 source_repo=source_repo,
775 source_ref=full_source_ref,
779 source_ref=full_source_ref,
776 target_repo=target_repo,
780 target_repo=target_repo,
777 target_ref=full_target_ref,
781 target_ref=full_target_ref,
778 common_ancestor_id=common_ancestor_id,
782 common_ancestor_id=common_ancestor_id,
779 revisions=revisions,
783 revisions=revisions,
780 reviewers=reviewers,
784 reviewers=reviewers,
781 title=title,
785 title=title,
782 description=description,
786 description=description,
783 description_renderer=description_renderer,
787 description_renderer=description_renderer,
784 reviewer_data=default_reviewers_data,
788 reviewer_data=default_reviewers_data,
785 auth_user=apiuser
789 auth_user=apiuser
786 )
790 )
787
791
788 Session().commit()
792 Session().commit()
789 data = {
793 data = {
790 'msg': 'Created new pull request `{}`'.format(title),
794 'msg': 'Created new pull request `{}`'.format(title),
791 'pull_request_id': pull_request.pull_request_id,
795 'pull_request_id': pull_request.pull_request_id,
792 }
796 }
793 return data
797 return data
794
798
795
799
796 @jsonrpc_method()
800 @jsonrpc_method()
797 def update_pull_request(
801 def update_pull_request(
798 request, apiuser, pullrequestid, repoid=Optional(None),
802 request, apiuser, pullrequestid, repoid=Optional(None),
799 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
803 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
800 reviewers=Optional(None), update_commits=Optional(None)):
804 reviewers=Optional(None), update_commits=Optional(None)):
801 """
805 """
802 Updates a pull request.
806 Updates a pull request.
803
807
804 :param apiuser: This is filled automatically from the |authtoken|.
808 :param apiuser: This is filled automatically from the |authtoken|.
805 :type apiuser: AuthUser
809 :type apiuser: AuthUser
806 :param repoid: Optional repository name or repository ID.
810 :param repoid: Optional repository name or repository ID.
807 :type repoid: str or int
811 :type repoid: str or int
808 :param pullrequestid: The pull request ID.
812 :param pullrequestid: The pull request ID.
809 :type pullrequestid: int
813 :type pullrequestid: int
810 :param title: Set the pull request title.
814 :param title: Set the pull request title.
811 :type title: str
815 :type title: str
812 :param description: Update pull request description.
816 :param description: Update pull request description.
813 :type description: Optional(str)
817 :type description: Optional(str)
814 :type description_renderer: Optional(str)
818 :type description_renderer: Optional(str)
815 :param description_renderer: Update pull request renderer for the description.
819 :param description_renderer: Update pull request renderer for the description.
816 It should be 'rst', 'markdown' or 'plain'
820 It should be 'rst', 'markdown' or 'plain'
817 :param reviewers: Update pull request reviewers list with new value.
821 :param reviewers: Update pull request reviewers list with new value.
818 :type reviewers: Optional(list)
822 :type reviewers: Optional(list)
819 Accepts username strings or objects of the format:
823 Accepts username strings or objects of the format:
820
824
821 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
825 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
822
826
823 :param update_commits: Trigger update of commits for this pull request
827 :param update_commits: Trigger update of commits for this pull request
824 :type: update_commits: Optional(bool)
828 :type: update_commits: Optional(bool)
825
829
826 Example output:
830 Example output:
827
831
828 .. code-block:: bash
832 .. code-block:: bash
829
833
830 id : <id_given_in_input>
834 id : <id_given_in_input>
831 result : {
835 result : {
832 "msg": "Updated pull request `63`",
836 "msg": "Updated pull request `63`",
833 "pull_request": <pull_request_object>,
837 "pull_request": <pull_request_object>,
834 "updated_reviewers": {
838 "updated_reviewers": {
835 "added": [
839 "added": [
836 "username"
840 "username"
837 ],
841 ],
838 "removed": []
842 "removed": []
839 },
843 },
840 "updated_commits": {
844 "updated_commits": {
841 "added": [
845 "added": [
842 "<sha1_hash>"
846 "<sha1_hash>"
843 ],
847 ],
844 "common": [
848 "common": [
845 "<sha1_hash>",
849 "<sha1_hash>",
846 "<sha1_hash>",
850 "<sha1_hash>",
847 ],
851 ],
848 "removed": []
852 "removed": []
849 }
853 }
850 }
854 }
851 error : null
855 error : null
852 """
856 """
853
857
854 pull_request = get_pull_request_or_error(pullrequestid)
858 pull_request = get_pull_request_or_error(pullrequestid)
855 if Optional.extract(repoid):
859 if Optional.extract(repoid):
856 repo = get_repo_or_error(repoid)
860 repo = get_repo_or_error(repoid)
857 else:
861 else:
858 repo = pull_request.target_repo
862 repo = pull_request.target_repo
859
863
860 if not PullRequestModel().check_user_update(
864 if not PullRequestModel().check_user_update(
861 pull_request, apiuser, api=True):
865 pull_request, apiuser, api=True):
862 raise JSONRPCError(
866 raise JSONRPCError(
863 'pull request `%s` update failed, no permission to update.' % (
867 'pull request `%s` update failed, no permission to update.' % (
864 pullrequestid,))
868 pullrequestid,))
865 if pull_request.is_closed():
869 if pull_request.is_closed():
866 raise JSONRPCError(
870 raise JSONRPCError(
867 'pull request `%s` update failed, pull request is closed' % (
871 'pull request `%s` update failed, pull request is closed' % (
868 pullrequestid,))
872 pullrequestid,))
869
873
870 reviewer_objects = Optional.extract(reviewers) or []
874 reviewer_objects = Optional.extract(reviewers) or []
871
875
872 if reviewer_objects:
876 if reviewer_objects:
873 schema = ReviewerListSchema()
877 schema = ReviewerListSchema()
874 try:
878 try:
875 reviewer_objects = schema.deserialize(reviewer_objects)
879 reviewer_objects = schema.deserialize(reviewer_objects)
876 except Invalid as err:
880 except Invalid as err:
877 raise JSONRPCValidationError(colander_exc=err)
881 raise JSONRPCValidationError(colander_exc=err)
878
882
879 # validate users
883 # validate users
880 for reviewer_object in reviewer_objects:
884 for reviewer_object in reviewer_objects:
881 user = get_user_or_error(reviewer_object['username'])
885 user = get_user_or_error(reviewer_object['username'])
882 reviewer_object['user_id'] = user.user_id
886 reviewer_object['user_id'] = user.user_id
883
887
884 get_default_reviewers_data, get_validated_reviewers, validate_observers = \
888 get_default_reviewers_data, get_validated_reviewers, validate_observers = \
885 PullRequestModel().get_reviewer_functions()
889 PullRequestModel().get_reviewer_functions()
886
890
887 # re-use stored rules
891 # re-use stored rules
888 reviewer_rules = pull_request.reviewer_data
892 reviewer_rules = pull_request.reviewer_data
889 try:
893 try:
890 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
894 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
891 except ValueError as e:
895 except ValueError as e:
892 raise JSONRPCError('Reviewers Validation: {}'.format(e))
896 raise JSONRPCError('Reviewers Validation: {}'.format(e))
893 else:
897 else:
894 reviewers = []
898 reviewers = []
895
899
896 title = Optional.extract(title)
900 title = Optional.extract(title)
897 description = Optional.extract(description)
901 description = Optional.extract(description)
898 description_renderer = Optional.extract(description_renderer)
902 description_renderer = Optional.extract(description_renderer)
899
903
900 # Update title/description
904 # Update title/description
901 title_changed = False
905 title_changed = False
902 if title or description:
906 if title or description:
903 PullRequestModel().edit(
907 PullRequestModel().edit(
904 pull_request,
908 pull_request,
905 title or pull_request.title,
909 title or pull_request.title,
906 description or pull_request.description,
910 description or pull_request.description,
907 description_renderer or pull_request.description_renderer,
911 description_renderer or pull_request.description_renderer,
908 apiuser)
912 apiuser)
909 Session().commit()
913 Session().commit()
910 title_changed = True
914 title_changed = True
911
915
912 commit_changes = {"added": [], "common": [], "removed": []}
916 commit_changes = {"added": [], "common": [], "removed": []}
913
917
914 # Update commits
918 # Update commits
915 commits_changed = False
919 commits_changed = False
916 if str2bool(Optional.extract(update_commits)):
920 if str2bool(Optional.extract(update_commits)):
917
921
918 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
922 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
919 raise JSONRPCError(
923 raise JSONRPCError(
920 'Operation forbidden because pull request is in state {}, '
924 'Operation forbidden because pull request is in state {}, '
921 'only state {} is allowed.'.format(
925 'only state {} is allowed.'.format(
922 pull_request.pull_request_state, PullRequest.STATE_CREATED))
926 pull_request.pull_request_state, PullRequest.STATE_CREATED))
923
927
924 with pull_request.set_state(PullRequest.STATE_UPDATING):
928 with pull_request.set_state(PullRequest.STATE_UPDATING):
925 if PullRequestModel().has_valid_update_type(pull_request):
929 if PullRequestModel().has_valid_update_type(pull_request):
926 db_user = apiuser.get_instance()
930 db_user = apiuser.get_instance()
927 update_response = PullRequestModel().update_commits(
931 update_response = PullRequestModel().update_commits(
928 pull_request, db_user)
932 pull_request, db_user)
929 commit_changes = update_response.changes or commit_changes
933 commit_changes = update_response.changes or commit_changes
930 Session().commit()
934 Session().commit()
931 commits_changed = True
935 commits_changed = True
932
936
933 # Update reviewers
937 # Update reviewers
934 reviewers_changed = False
938 reviewers_changed = False
935 reviewers_changes = {"added": [], "removed": []}
939 reviewers_changes = {"added": [], "removed": []}
936 if reviewers:
940 if reviewers:
937 old_calculated_status = pull_request.calculated_review_status()
941 old_calculated_status = pull_request.calculated_review_status()
938 added_reviewers, removed_reviewers = \
942 added_reviewers, removed_reviewers = \
939 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
943 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
940
944
941 reviewers_changes['added'] = sorted(
945 reviewers_changes['added'] = sorted(
942 [get_user_or_error(n).username for n in added_reviewers])
946 [get_user_or_error(n).username for n in added_reviewers])
943 reviewers_changes['removed'] = sorted(
947 reviewers_changes['removed'] = sorted(
944 [get_user_or_error(n).username for n in removed_reviewers])
948 [get_user_or_error(n).username for n in removed_reviewers])
945 Session().commit()
949 Session().commit()
946
950
947 # trigger status changed if change in reviewers changes the status
951 # trigger status changed if change in reviewers changes the status
948 calculated_status = pull_request.calculated_review_status()
952 calculated_status = pull_request.calculated_review_status()
949 if old_calculated_status != calculated_status:
953 if old_calculated_status != calculated_status:
950 PullRequestModel().trigger_pull_request_hook(
954 PullRequestModel().trigger_pull_request_hook(
951 pull_request, apiuser, 'review_status_change',
955 pull_request, apiuser, 'review_status_change',
952 data={'status': calculated_status})
956 data={'status': calculated_status})
953 reviewers_changed = True
957 reviewers_changed = True
954
958
955 observers_changed = False
959 observers_changed = False
956
960
957 # push changed to channelstream
961 # push changed to channelstream
958 if commits_changed or reviewers_changed or observers_changed:
962 if commits_changed or reviewers_changed or observers_changed:
959 pr_broadcast_channel = channelstream.pr_channel(pull_request)
963 pr_broadcast_channel = channelstream.pr_channel(pull_request)
960 msg = 'Pull request was updated.'
964 msg = 'Pull request was updated.'
961 channelstream.pr_update_channelstream_push(
965 channelstream.pr_update_channelstream_push(
962 request, pr_broadcast_channel, apiuser, msg)
966 request, pr_broadcast_channel, apiuser, msg)
963
967
964 data = {
968 data = {
965 'msg': 'Updated pull request `{}`'.format(
969 'msg': 'Updated pull request `{}`'.format(
966 pull_request.pull_request_id),
970 pull_request.pull_request_id),
967 'pull_request': pull_request.get_api_data(),
971 'pull_request': pull_request.get_api_data(),
968 'updated_commits': commit_changes,
972 'updated_commits': commit_changes,
969 'updated_reviewers': reviewers_changes
973 'updated_reviewers': reviewers_changes
970 }
974 }
971
975
972 return data
976 return data
973
977
974
978
975 @jsonrpc_method()
979 @jsonrpc_method()
976 def close_pull_request(
980 def close_pull_request(
977 request, apiuser, pullrequestid, repoid=Optional(None),
981 request, apiuser, pullrequestid, repoid=Optional(None),
978 userid=Optional(OAttr('apiuser')), message=Optional('')):
982 userid=Optional(OAttr('apiuser')), message=Optional('')):
979 """
983 """
980 Close the pull request specified by `pullrequestid`.
984 Close the pull request specified by `pullrequestid`.
981
985
982 :param apiuser: This is filled automatically from the |authtoken|.
986 :param apiuser: This is filled automatically from the |authtoken|.
983 :type apiuser: AuthUser
987 :type apiuser: AuthUser
984 :param repoid: Repository name or repository ID to which the pull
988 :param repoid: Repository name or repository ID to which the pull
985 request belongs.
989 request belongs.
986 :type repoid: str or int
990 :type repoid: str or int
987 :param pullrequestid: ID of the pull request to be closed.
991 :param pullrequestid: ID of the pull request to be closed.
988 :type pullrequestid: int
992 :type pullrequestid: int
989 :param userid: Close the pull request as this user.
993 :param userid: Close the pull request as this user.
990 :type userid: Optional(str or int)
994 :type userid: Optional(str or int)
991 :param message: Optional message to close the Pull Request with. If not
995 :param message: Optional message to close the Pull Request with. If not
992 specified it will be generated automatically.
996 specified it will be generated automatically.
993 :type message: Optional(str)
997 :type message: Optional(str)
994
998
995 Example output:
999 Example output:
996
1000
997 .. code-block:: bash
1001 .. code-block:: bash
998
1002
999 "id": <id_given_in_input>,
1003 "id": <id_given_in_input>,
1000 "result": {
1004 "result": {
1001 "pull_request_id": "<int>",
1005 "pull_request_id": "<int>",
1002 "close_status": "<str:status_lbl>,
1006 "close_status": "<str:status_lbl>,
1003 "closed": "<bool>"
1007 "closed": "<bool>"
1004 },
1008 },
1005 "error": null
1009 "error": null
1006
1010
1007 """
1011 """
1008 _ = request.translate
1012 _ = request.translate
1009
1013
1010 pull_request = get_pull_request_or_error(pullrequestid)
1014 pull_request = get_pull_request_or_error(pullrequestid)
1011 if Optional.extract(repoid):
1015 if Optional.extract(repoid):
1012 repo = get_repo_or_error(repoid)
1016 repo = get_repo_or_error(repoid)
1013 else:
1017 else:
1014 repo = pull_request.target_repo
1018 repo = pull_request.target_repo
1015
1019
1016 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1020 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1017 user=apiuser, repo_name=repo.repo_name)
1021 user=apiuser, repo_name=repo.repo_name)
1018 if not isinstance(userid, Optional):
1022 if not isinstance(userid, Optional):
1019 if has_superadmin_permission(apiuser) or is_repo_admin:
1023 if has_superadmin_permission(apiuser) or is_repo_admin:
1020 apiuser = get_user_or_error(userid)
1024 apiuser = get_user_or_error(userid)
1021 else:
1025 else:
1022 raise JSONRPCError('userid is not the same as your user')
1026 raise JSONRPCError('userid is not the same as your user')
1023
1027
1024 if pull_request.is_closed():
1028 if pull_request.is_closed():
1025 raise JSONRPCError(
1029 raise JSONRPCError(
1026 'pull request `%s` is already closed' % (pullrequestid,))
1030 'pull request `%s` is already closed' % (pullrequestid,))
1027
1031
1028 # only owner or admin or person with write permissions
1032 # only owner or admin or person with write permissions
1029 allowed_to_close = PullRequestModel().check_user_update(
1033 allowed_to_close = PullRequestModel().check_user_update(
1030 pull_request, apiuser, api=True)
1034 pull_request, apiuser, api=True)
1031
1035
1032 if not allowed_to_close:
1036 if not allowed_to_close:
1033 raise JSONRPCError(
1037 raise JSONRPCError(
1034 'pull request `%s` close failed, no permission to close.' % (
1038 'pull request `%s` close failed, no permission to close.' % (
1035 pullrequestid,))
1039 pullrequestid,))
1036
1040
1037 # message we're using to close the PR, else it's automatically generated
1041 # message we're using to close the PR, else it's automatically generated
1038 message = Optional.extract(message)
1042 message = Optional.extract(message)
1039
1043
1040 # finally close the PR, with proper message comment
1044 # finally close the PR, with proper message comment
1041 comment, status = PullRequestModel().close_pull_request_with_comment(
1045 comment, status = PullRequestModel().close_pull_request_with_comment(
1042 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1046 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1043 status_lbl = ChangesetStatus.get_status_lbl(status)
1047 status_lbl = ChangesetStatus.get_status_lbl(status)
1044
1048
1045 Session().commit()
1049 Session().commit()
1046
1050
1047 data = {
1051 data = {
1048 'pull_request_id': pull_request.pull_request_id,
1052 'pull_request_id': pull_request.pull_request_id,
1049 'close_status': status_lbl,
1053 'close_status': status_lbl,
1050 'closed': True,
1054 'closed': True,
1051 }
1055 }
1052 return data
1056 return data
@@ -1,80 +1,82 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24
24
25 from rhodecode.apps._base import RepoAppView
25 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 from rhodecode.lib.vcs.backends.base import Reference
28 from rhodecode.lib.vcs.backends.base import Reference
29 from rhodecode.model.db import Repository
29 from rhodecode.model.db import Repository
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class RepoReviewRulesView(RepoAppView):
34 class RepoReviewRulesView(RepoAppView):
35 def load_default_context(self):
35 def load_default_context(self):
36 c = self._get_local_tmpl_context()
36 c = self._get_local_tmpl_context()
37 return c
37 return c
38
38
39 @LoginRequired()
39 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.admin')
40 @HasRepoPermissionAnyDecorator('repository.admin')
41 @view_config(
41 @view_config(
42 route_name='repo_reviewers', request_method='GET',
42 route_name='repo_reviewers', request_method='GET',
43 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
43 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
44 def repo_review_rules(self):
44 def repo_review_rules(self):
45 c = self.load_default_context()
45 c = self.load_default_context()
46 c.active = 'reviewers'
46 c.active = 'reviewers'
47
47
48 return self._get_template_context(c)
48 return self._get_template_context(c)
49
49
50 @LoginRequired()
50 @LoginRequired()
51 @HasRepoPermissionAnyDecorator(
51 @HasRepoPermissionAnyDecorator(
52 'repository.read', 'repository.write', 'repository.admin')
52 'repository.read', 'repository.write', 'repository.admin')
53 @view_config(
53 @view_config(
54 route_name='repo_default_reviewers_data', request_method='GET',
54 route_name='repo_default_reviewers_data', request_method='GET',
55 renderer='json_ext')
55 renderer='json_ext')
56 def repo_default_reviewers_data(self):
56 def repo_default_reviewers_data(self):
57 self.load_default_context()
57 self.load_default_context()
58
58
59 request = self.request
59 request = self.request
60 source_repo = self.db_repo
60 source_repo = self.db_repo
61 source_repo_name = source_repo.repo_name
61 source_repo_name = source_repo.repo_name
62 target_repo_name = request.GET.get('target_repo', source_repo_name)
62 target_repo_name = request.GET.get('target_repo', source_repo_name)
63 target_repo = Repository.get_by_repo_name(target_repo_name)
63 target_repo = Repository.get_by_repo_name(target_repo_name)
64
64
65 current_user = request.user.get_instance()
65 current_user = request.user.get_instance()
66
66
67 source_commit_id = request.GET['source_ref']
67 source_commit_id = request.GET['source_ref']
68 source_type = request.GET['source_ref_type']
68 source_type = request.GET['source_ref_type']
69 source_name = request.GET['source_ref_name']
69 source_name = request.GET['source_ref_name']
70
70
71 target_commit_id = request.GET['target_ref']
71 target_commit_id = request.GET['target_ref']
72 target_type = request.GET['target_ref_type']
72 target_type = request.GET['target_ref_type']
73 target_name = request.GET['target_ref_name']
73 target_name = request.GET['target_ref_name']
74
74
75 source_ref = Reference(source_type, source_name, source_commit_id)
76 target_ref = Reference(target_type, target_name, target_commit_id)
77
78 review_data = get_default_reviewers_data(
75 review_data = get_default_reviewers_data(
79 current_user, source_repo, source_ref, target_repo, target_ref)
76 current_user,
77 source_repo,
78 Reference(source_type, source_name, source_commit_id),
79 target_repo,
80 Reference(target_type, target_name, target_commit_id)
81 )
80 return review_data
82 return review_data
@@ -1,2205 +1,2229 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 log.debug('Obtaining commit authors from set of commits')
157 log.debug('Obtaining commit authors from set of commits')
158 commits = target_scm.compare(
158 _compare_data = target_scm.compare(
159 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
160 pre_load=["author", "date", "message", "branch", "parents"])
160 pre_load=["author", "date", "message"]
161 )
161
162
162 for commit in commits:
163 for commit in _compare_data:
163 user = User.get_from_cs_author(commit.author)
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
167 author=commit.author,
168 date=commit.date,
169 message=commit.message,
170 )
171 commits.append(serialized_commit)
172 user = User.get_from_cs_author(serialized_commit['author'])
164 if user and user not in commit_authors:
173 if user and user not in commit_authors:
165 commit_authors.append(user)
174 commit_authors.append(user)
166
175
167 # lines
176 # lines
168 if get_authors:
177 if get_authors:
169 log.debug('Calculating authors of changed files')
178 log.debug('Calculating authors of changed files')
170 target_commit = source_repo.get_commit(ancestor_id)
179 target_commit = source_repo.get_commit(ancestor_id)
171
180
172 for fname, lines in changed_lines.items():
181 for fname, lines in changed_lines.items():
182
173 try:
183 try:
174 node = target_commit.get_node(fname)
184 node = target_commit.get_node(fname, pre_load=["is_binary"])
175 except Exception:
185 except Exception:
186 log.exception("Failed to load node with path %s", fname)
176 continue
187 continue
177
188
178 if not isinstance(node, FileNode):
189 if not isinstance(node, FileNode):
179 continue
190 continue
180
191
192 # NOTE(marcink): for binary node we don't do annotation, just use last author
193 if node.is_binary:
194 author = node.last_commit.author
195 email = node.last_commit.author_email
196
197 user = User.get_from_cs_author(author)
198 if user:
199 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
200 author_counts[author] = author_counts.get(author, 0) + 1
201 email_counts[email] = email_counts.get(email, 0) + 1
202
203 continue
204
181 for annotation in node.annotate:
205 for annotation in node.annotate:
182 line_no, commit_id, get_commit_func, line_text = annotation
206 line_no, commit_id, get_commit_func, line_text = annotation
183 if line_no in lines:
207 if line_no in lines:
184 if commit_id not in _commit_cache:
208 if commit_id not in _commit_cache:
185 _commit_cache[commit_id] = get_commit_func()
209 _commit_cache[commit_id] = get_commit_func()
186 commit = _commit_cache[commit_id]
210 commit = _commit_cache[commit_id]
187 author = commit.author
211 author = commit.author
188 email = commit.author_email
212 email = commit.author_email
189 user = User.get_from_cs_author(author)
213 user = User.get_from_cs_author(author)
190 if user:
214 if user:
191 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
215 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
192 author_counts[author] = author_counts.get(author, 0) + 1
216 author_counts[author] = author_counts.get(author, 0) + 1
193 email_counts[email] = email_counts.get(email, 0) + 1
217 email_counts[email] = email_counts.get(email, 0) + 1
194
218
195 log.debug('Default reviewers processing finished')
219 log.debug('Default reviewers processing finished')
196
220
197 return {
221 return {
198 'commits': commits,
222 'commits': commits,
199 'files': all_files_changes,
223 'files': all_files_changes,
200 'stats': stats,
224 'stats': stats,
201 'ancestor': ancestor_id,
225 'ancestor': ancestor_id,
202 # original authors of modified files
226 # original authors of modified files
203 'original_authors': {
227 'original_authors': {
204 'users': user_counts,
228 'users': user_counts,
205 'authors': author_counts,
229 'authors': author_counts,
206 'emails': email_counts,
230 'emails': email_counts,
207 },
231 },
208 'commit_authors': commit_authors
232 'commit_authors': commit_authors
209 }
233 }
210
234
211
235
212 class PullRequestModel(BaseModel):
236 class PullRequestModel(BaseModel):
213
237
214 cls = PullRequest
238 cls = PullRequest
215
239
216 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
240 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
217
241
218 UPDATE_STATUS_MESSAGES = {
242 UPDATE_STATUS_MESSAGES = {
219 UpdateFailureReason.NONE: lazy_ugettext(
243 UpdateFailureReason.NONE: lazy_ugettext(
220 'Pull request update successful.'),
244 'Pull request update successful.'),
221 UpdateFailureReason.UNKNOWN: lazy_ugettext(
245 UpdateFailureReason.UNKNOWN: lazy_ugettext(
222 'Pull request update failed because of an unknown error.'),
246 'Pull request update failed because of an unknown error.'),
223 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
247 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
224 'No update needed because the source and target have not changed.'),
248 'No update needed because the source and target have not changed.'),
225 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
249 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
226 'Pull request cannot be updated because the reference type is '
250 'Pull request cannot be updated because the reference type is '
227 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
251 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
228 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
252 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
229 'This pull request cannot be updated because the target '
253 'This pull request cannot be updated because the target '
230 'reference is missing.'),
254 'reference is missing.'),
231 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
255 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
232 'This pull request cannot be updated because the source '
256 'This pull request cannot be updated because the source '
233 'reference is missing.'),
257 'reference is missing.'),
234 }
258 }
235 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
259 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
236 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
260 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
237
261
238 def __get_pull_request(self, pull_request):
262 def __get_pull_request(self, pull_request):
239 return self._get_instance((
263 return self._get_instance((
240 PullRequest, PullRequestVersion), pull_request)
264 PullRequest, PullRequestVersion), pull_request)
241
265
242 def _check_perms(self, perms, pull_request, user, api=False):
266 def _check_perms(self, perms, pull_request, user, api=False):
243 if not api:
267 if not api:
244 return h.HasRepoPermissionAny(*perms)(
268 return h.HasRepoPermissionAny(*perms)(
245 user=user, repo_name=pull_request.target_repo.repo_name)
269 user=user, repo_name=pull_request.target_repo.repo_name)
246 else:
270 else:
247 return h.HasRepoPermissionAnyApi(*perms)(
271 return h.HasRepoPermissionAnyApi(*perms)(
248 user=user, repo_name=pull_request.target_repo.repo_name)
272 user=user, repo_name=pull_request.target_repo.repo_name)
249
273
250 def check_user_read(self, pull_request, user, api=False):
274 def check_user_read(self, pull_request, user, api=False):
251 _perms = ('repository.admin', 'repository.write', 'repository.read',)
275 _perms = ('repository.admin', 'repository.write', 'repository.read',)
252 return self._check_perms(_perms, pull_request, user, api)
276 return self._check_perms(_perms, pull_request, user, api)
253
277
254 def check_user_merge(self, pull_request, user, api=False):
278 def check_user_merge(self, pull_request, user, api=False):
255 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
279 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
256 return self._check_perms(_perms, pull_request, user, api)
280 return self._check_perms(_perms, pull_request, user, api)
257
281
258 def check_user_update(self, pull_request, user, api=False):
282 def check_user_update(self, pull_request, user, api=False):
259 owner = user.user_id == pull_request.user_id
283 owner = user.user_id == pull_request.user_id
260 return self.check_user_merge(pull_request, user, api) or owner
284 return self.check_user_merge(pull_request, user, api) or owner
261
285
262 def check_user_delete(self, pull_request, user):
286 def check_user_delete(self, pull_request, user):
263 owner = user.user_id == pull_request.user_id
287 owner = user.user_id == pull_request.user_id
264 _perms = ('repository.admin',)
288 _perms = ('repository.admin',)
265 return self._check_perms(_perms, pull_request, user) or owner
289 return self._check_perms(_perms, pull_request, user) or owner
266
290
267 def check_user_change_status(self, pull_request, user, api=False):
291 def check_user_change_status(self, pull_request, user, api=False):
268 reviewer = user.user_id in [x.user_id for x in
292 reviewer = user.user_id in [x.user_id for x in
269 pull_request.reviewers]
293 pull_request.reviewers]
270 return self.check_user_update(pull_request, user, api) or reviewer
294 return self.check_user_update(pull_request, user, api) or reviewer
271
295
272 def check_user_comment(self, pull_request, user):
296 def check_user_comment(self, pull_request, user):
273 owner = user.user_id == pull_request.user_id
297 owner = user.user_id == pull_request.user_id
274 return self.check_user_read(pull_request, user) or owner
298 return self.check_user_read(pull_request, user) or owner
275
299
276 def get(self, pull_request):
300 def get(self, pull_request):
277 return self.__get_pull_request(pull_request)
301 return self.__get_pull_request(pull_request)
278
302
279 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
303 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
280 statuses=None, opened_by=None, order_by=None,
304 statuses=None, opened_by=None, order_by=None,
281 order_dir='desc', only_created=False):
305 order_dir='desc', only_created=False):
282 repo = None
306 repo = None
283 if repo_name:
307 if repo_name:
284 repo = self._get_repo(repo_name)
308 repo = self._get_repo(repo_name)
285
309
286 q = PullRequest.query()
310 q = PullRequest.query()
287
311
288 if search_q:
312 if search_q:
289 like_expression = u'%{}%'.format(safe_unicode(search_q))
313 like_expression = u'%{}%'.format(safe_unicode(search_q))
290 q = q.join(User)
314 q = q.join(User)
291 q = q.filter(or_(
315 q = q.filter(or_(
292 cast(PullRequest.pull_request_id, String).ilike(like_expression),
316 cast(PullRequest.pull_request_id, String).ilike(like_expression),
293 User.username.ilike(like_expression),
317 User.username.ilike(like_expression),
294 PullRequest.title.ilike(like_expression),
318 PullRequest.title.ilike(like_expression),
295 PullRequest.description.ilike(like_expression),
319 PullRequest.description.ilike(like_expression),
296 ))
320 ))
297
321
298 # source or target
322 # source or target
299 if repo and source:
323 if repo and source:
300 q = q.filter(PullRequest.source_repo == repo)
324 q = q.filter(PullRequest.source_repo == repo)
301 elif repo:
325 elif repo:
302 q = q.filter(PullRequest.target_repo == repo)
326 q = q.filter(PullRequest.target_repo == repo)
303
327
304 # closed,opened
328 # closed,opened
305 if statuses:
329 if statuses:
306 q = q.filter(PullRequest.status.in_(statuses))
330 q = q.filter(PullRequest.status.in_(statuses))
307
331
308 # opened by filter
332 # opened by filter
309 if opened_by:
333 if opened_by:
310 q = q.filter(PullRequest.user_id.in_(opened_by))
334 q = q.filter(PullRequest.user_id.in_(opened_by))
311
335
312 # only get those that are in "created" state
336 # only get those that are in "created" state
313 if only_created:
337 if only_created:
314 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
338 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
315
339
316 if order_by:
340 if order_by:
317 order_map = {
341 order_map = {
318 'name_raw': PullRequest.pull_request_id,
342 'name_raw': PullRequest.pull_request_id,
319 'id': PullRequest.pull_request_id,
343 'id': PullRequest.pull_request_id,
320 'title': PullRequest.title,
344 'title': PullRequest.title,
321 'updated_on_raw': PullRequest.updated_on,
345 'updated_on_raw': PullRequest.updated_on,
322 'target_repo': PullRequest.target_repo_id
346 'target_repo': PullRequest.target_repo_id
323 }
347 }
324 if order_dir == 'asc':
348 if order_dir == 'asc':
325 q = q.order_by(order_map[order_by].asc())
349 q = q.order_by(order_map[order_by].asc())
326 else:
350 else:
327 q = q.order_by(order_map[order_by].desc())
351 q = q.order_by(order_map[order_by].desc())
328
352
329 return q
353 return q
330
354
331 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
355 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
332 opened_by=None):
356 opened_by=None):
333 """
357 """
334 Count the number of pull requests for a specific repository.
358 Count the number of pull requests for a specific repository.
335
359
336 :param repo_name: target or source repo
360 :param repo_name: target or source repo
337 :param search_q: filter by text
361 :param search_q: filter by text
338 :param source: boolean flag to specify if repo_name refers to source
362 :param source: boolean flag to specify if repo_name refers to source
339 :param statuses: list of pull request statuses
363 :param statuses: list of pull request statuses
340 :param opened_by: author user of the pull request
364 :param opened_by: author user of the pull request
341 :returns: int number of pull requests
365 :returns: int number of pull requests
342 """
366 """
343 q = self._prepare_get_all_query(
367 q = self._prepare_get_all_query(
344 repo_name, search_q=search_q, source=source, statuses=statuses,
368 repo_name, search_q=search_q, source=source, statuses=statuses,
345 opened_by=opened_by)
369 opened_by=opened_by)
346
370
347 return q.count()
371 return q.count()
348
372
349 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
373 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
350 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
374 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
351 """
375 """
352 Get all pull requests for a specific repository.
376 Get all pull requests for a specific repository.
353
377
354 :param repo_name: target or source repo
378 :param repo_name: target or source repo
355 :param search_q: filter by text
379 :param search_q: filter by text
356 :param source: boolean flag to specify if repo_name refers to source
380 :param source: boolean flag to specify if repo_name refers to source
357 :param statuses: list of pull request statuses
381 :param statuses: list of pull request statuses
358 :param opened_by: author user of the pull request
382 :param opened_by: author user of the pull request
359 :param offset: pagination offset
383 :param offset: pagination offset
360 :param length: length of returned list
384 :param length: length of returned list
361 :param order_by: order of the returned list
385 :param order_by: order of the returned list
362 :param order_dir: 'asc' or 'desc' ordering direction
386 :param order_dir: 'asc' or 'desc' ordering direction
363 :returns: list of pull requests
387 :returns: list of pull requests
364 """
388 """
365 q = self._prepare_get_all_query(
389 q = self._prepare_get_all_query(
366 repo_name, search_q=search_q, source=source, statuses=statuses,
390 repo_name, search_q=search_q, source=source, statuses=statuses,
367 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
391 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
368
392
369 if length:
393 if length:
370 pull_requests = q.limit(length).offset(offset).all()
394 pull_requests = q.limit(length).offset(offset).all()
371 else:
395 else:
372 pull_requests = q.all()
396 pull_requests = q.all()
373
397
374 return pull_requests
398 return pull_requests
375
399
376 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
400 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
377 opened_by=None):
401 opened_by=None):
378 """
402 """
379 Count the number of pull requests for a specific repository that are
403 Count the number of pull requests for a specific repository that are
380 awaiting review.
404 awaiting review.
381
405
382 :param repo_name: target or source repo
406 :param repo_name: target or source repo
383 :param search_q: filter by text
407 :param search_q: filter by text
384 :param source: boolean flag to specify if repo_name refers to source
408 :param source: boolean flag to specify if repo_name refers to source
385 :param statuses: list of pull request statuses
409 :param statuses: list of pull request statuses
386 :param opened_by: author user of the pull request
410 :param opened_by: author user of the pull request
387 :returns: int number of pull requests
411 :returns: int number of pull requests
388 """
412 """
389 pull_requests = self.get_awaiting_review(
413 pull_requests = self.get_awaiting_review(
390 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
414 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
391
415
392 return len(pull_requests)
416 return len(pull_requests)
393
417
394 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
418 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
395 opened_by=None, offset=0, length=None,
419 opened_by=None, offset=0, length=None,
396 order_by=None, order_dir='desc'):
420 order_by=None, order_dir='desc'):
397 """
421 """
398 Get all pull requests for a specific repository that are awaiting
422 Get all pull requests for a specific repository that are awaiting
399 review.
423 review.
400
424
401 :param repo_name: target or source repo
425 :param repo_name: target or source repo
402 :param search_q: filter by text
426 :param search_q: filter by text
403 :param source: boolean flag to specify if repo_name refers to source
427 :param source: boolean flag to specify if repo_name refers to source
404 :param statuses: list of pull request statuses
428 :param statuses: list of pull request statuses
405 :param opened_by: author user of the pull request
429 :param opened_by: author user of the pull request
406 :param offset: pagination offset
430 :param offset: pagination offset
407 :param length: length of returned list
431 :param length: length of returned list
408 :param order_by: order of the returned list
432 :param order_by: order of the returned list
409 :param order_dir: 'asc' or 'desc' ordering direction
433 :param order_dir: 'asc' or 'desc' ordering direction
410 :returns: list of pull requests
434 :returns: list of pull requests
411 """
435 """
412 pull_requests = self.get_all(
436 pull_requests = self.get_all(
413 repo_name, search_q=search_q, source=source, statuses=statuses,
437 repo_name, search_q=search_q, source=source, statuses=statuses,
414 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
438 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
415
439
416 _filtered_pull_requests = []
440 _filtered_pull_requests = []
417 for pr in pull_requests:
441 for pr in pull_requests:
418 status = pr.calculated_review_status()
442 status = pr.calculated_review_status()
419 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
443 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
420 ChangesetStatus.STATUS_UNDER_REVIEW]:
444 ChangesetStatus.STATUS_UNDER_REVIEW]:
421 _filtered_pull_requests.append(pr)
445 _filtered_pull_requests.append(pr)
422 if length:
446 if length:
423 return _filtered_pull_requests[offset:offset+length]
447 return _filtered_pull_requests[offset:offset+length]
424 else:
448 else:
425 return _filtered_pull_requests
449 return _filtered_pull_requests
426
450
427 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
451 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
428 opened_by=None, user_id=None):
452 opened_by=None, user_id=None):
429 """
453 """
430 Count the number of pull requests for a specific repository that are
454 Count the number of pull requests for a specific repository that are
431 awaiting review from a specific user.
455 awaiting review from a specific user.
432
456
433 :param repo_name: target or source repo
457 :param repo_name: target or source repo
434 :param search_q: filter by text
458 :param search_q: filter by text
435 :param source: boolean flag to specify if repo_name refers to source
459 :param source: boolean flag to specify if repo_name refers to source
436 :param statuses: list of pull request statuses
460 :param statuses: list of pull request statuses
437 :param opened_by: author user of the pull request
461 :param opened_by: author user of the pull request
438 :param user_id: reviewer user of the pull request
462 :param user_id: reviewer user of the pull request
439 :returns: int number of pull requests
463 :returns: int number of pull requests
440 """
464 """
441 pull_requests = self.get_awaiting_my_review(
465 pull_requests = self.get_awaiting_my_review(
442 repo_name, search_q=search_q, source=source, statuses=statuses,
466 repo_name, search_q=search_q, source=source, statuses=statuses,
443 opened_by=opened_by, user_id=user_id)
467 opened_by=opened_by, user_id=user_id)
444
468
445 return len(pull_requests)
469 return len(pull_requests)
446
470
447 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
471 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
448 opened_by=None, user_id=None, offset=0,
472 opened_by=None, user_id=None, offset=0,
449 length=None, order_by=None, order_dir='desc'):
473 length=None, order_by=None, order_dir='desc'):
450 """
474 """
451 Get all pull requests for a specific repository that are awaiting
475 Get all pull requests for a specific repository that are awaiting
452 review from a specific user.
476 review from a specific user.
453
477
454 :param repo_name: target or source repo
478 :param repo_name: target or source repo
455 :param search_q: filter by text
479 :param search_q: filter by text
456 :param source: boolean flag to specify if repo_name refers to source
480 :param source: boolean flag to specify if repo_name refers to source
457 :param statuses: list of pull request statuses
481 :param statuses: list of pull request statuses
458 :param opened_by: author user of the pull request
482 :param opened_by: author user of the pull request
459 :param user_id: reviewer user of the pull request
483 :param user_id: reviewer user of the pull request
460 :param offset: pagination offset
484 :param offset: pagination offset
461 :param length: length of returned list
485 :param length: length of returned list
462 :param order_by: order of the returned list
486 :param order_by: order of the returned list
463 :param order_dir: 'asc' or 'desc' ordering direction
487 :param order_dir: 'asc' or 'desc' ordering direction
464 :returns: list of pull requests
488 :returns: list of pull requests
465 """
489 """
466 pull_requests = self.get_all(
490 pull_requests = self.get_all(
467 repo_name, search_q=search_q, source=source, statuses=statuses,
491 repo_name, search_q=search_q, source=source, statuses=statuses,
468 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
492 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
469
493
470 _my = PullRequestModel().get_not_reviewed(user_id)
494 _my = PullRequestModel().get_not_reviewed(user_id)
471 my_participation = []
495 my_participation = []
472 for pr in pull_requests:
496 for pr in pull_requests:
473 if pr in _my:
497 if pr in _my:
474 my_participation.append(pr)
498 my_participation.append(pr)
475 _filtered_pull_requests = my_participation
499 _filtered_pull_requests = my_participation
476 if length:
500 if length:
477 return _filtered_pull_requests[offset:offset+length]
501 return _filtered_pull_requests[offset:offset+length]
478 else:
502 else:
479 return _filtered_pull_requests
503 return _filtered_pull_requests
480
504
481 def get_not_reviewed(self, user_id):
505 def get_not_reviewed(self, user_id):
482 return [
506 return [
483 x.pull_request for x in PullRequestReviewers.query().filter(
507 x.pull_request for x in PullRequestReviewers.query().filter(
484 PullRequestReviewers.user_id == user_id).all()
508 PullRequestReviewers.user_id == user_id).all()
485 ]
509 ]
486
510
487 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
511 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
488 order_by=None, order_dir='desc'):
512 order_by=None, order_dir='desc'):
489 q = PullRequest.query()
513 q = PullRequest.query()
490 if user_id:
514 if user_id:
491 reviewers_subquery = Session().query(
515 reviewers_subquery = Session().query(
492 PullRequestReviewers.pull_request_id).filter(
516 PullRequestReviewers.pull_request_id).filter(
493 PullRequestReviewers.user_id == user_id).subquery()
517 PullRequestReviewers.user_id == user_id).subquery()
494 user_filter = or_(
518 user_filter = or_(
495 PullRequest.user_id == user_id,
519 PullRequest.user_id == user_id,
496 PullRequest.pull_request_id.in_(reviewers_subquery)
520 PullRequest.pull_request_id.in_(reviewers_subquery)
497 )
521 )
498 q = PullRequest.query().filter(user_filter)
522 q = PullRequest.query().filter(user_filter)
499
523
500 # closed,opened
524 # closed,opened
501 if statuses:
525 if statuses:
502 q = q.filter(PullRequest.status.in_(statuses))
526 q = q.filter(PullRequest.status.in_(statuses))
503
527
504 if query:
528 if query:
505 like_expression = u'%{}%'.format(safe_unicode(query))
529 like_expression = u'%{}%'.format(safe_unicode(query))
506 q = q.join(User)
530 q = q.join(User)
507 q = q.filter(or_(
531 q = q.filter(or_(
508 cast(PullRequest.pull_request_id, String).ilike(like_expression),
532 cast(PullRequest.pull_request_id, String).ilike(like_expression),
509 User.username.ilike(like_expression),
533 User.username.ilike(like_expression),
510 PullRequest.title.ilike(like_expression),
534 PullRequest.title.ilike(like_expression),
511 PullRequest.description.ilike(like_expression),
535 PullRequest.description.ilike(like_expression),
512 ))
536 ))
513 if order_by:
537 if order_by:
514 order_map = {
538 order_map = {
515 'name_raw': PullRequest.pull_request_id,
539 'name_raw': PullRequest.pull_request_id,
516 'title': PullRequest.title,
540 'title': PullRequest.title,
517 'updated_on_raw': PullRequest.updated_on,
541 'updated_on_raw': PullRequest.updated_on,
518 'target_repo': PullRequest.target_repo_id
542 'target_repo': PullRequest.target_repo_id
519 }
543 }
520 if order_dir == 'asc':
544 if order_dir == 'asc':
521 q = q.order_by(order_map[order_by].asc())
545 q = q.order_by(order_map[order_by].asc())
522 else:
546 else:
523 q = q.order_by(order_map[order_by].desc())
547 q = q.order_by(order_map[order_by].desc())
524
548
525 return q
549 return q
526
550
527 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
551 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
528 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
552 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
529 return q.count()
553 return q.count()
530
554
531 def get_im_participating_in(
555 def get_im_participating_in(
532 self, user_id=None, statuses=None, query='', offset=0,
556 self, user_id=None, statuses=None, query='', offset=0,
533 length=None, order_by=None, order_dir='desc'):
557 length=None, order_by=None, order_dir='desc'):
534 """
558 """
535 Get all Pull requests that i'm participating in, or i have opened
559 Get all Pull requests that i'm participating in, or i have opened
536 """
560 """
537
561
538 q = self._prepare_participating_query(
562 q = self._prepare_participating_query(
539 user_id, statuses=statuses, query=query, order_by=order_by,
563 user_id, statuses=statuses, query=query, order_by=order_by,
540 order_dir=order_dir)
564 order_dir=order_dir)
541
565
542 if length:
566 if length:
543 pull_requests = q.limit(length).offset(offset).all()
567 pull_requests = q.limit(length).offset(offset).all()
544 else:
568 else:
545 pull_requests = q.all()
569 pull_requests = q.all()
546
570
547 return pull_requests
571 return pull_requests
548
572
549 def get_versions(self, pull_request):
573 def get_versions(self, pull_request):
550 """
574 """
551 returns version of pull request sorted by ID descending
575 returns version of pull request sorted by ID descending
552 """
576 """
553 return PullRequestVersion.query()\
577 return PullRequestVersion.query()\
554 .filter(PullRequestVersion.pull_request == pull_request)\
578 .filter(PullRequestVersion.pull_request == pull_request)\
555 .order_by(PullRequestVersion.pull_request_version_id.asc())\
579 .order_by(PullRequestVersion.pull_request_version_id.asc())\
556 .all()
580 .all()
557
581
558 def get_pr_version(self, pull_request_id, version=None):
582 def get_pr_version(self, pull_request_id, version=None):
559 at_version = None
583 at_version = None
560
584
561 if version and version == 'latest':
585 if version and version == 'latest':
562 pull_request_ver = PullRequest.get(pull_request_id)
586 pull_request_ver = PullRequest.get(pull_request_id)
563 pull_request_obj = pull_request_ver
587 pull_request_obj = pull_request_ver
564 _org_pull_request_obj = pull_request_obj
588 _org_pull_request_obj = pull_request_obj
565 at_version = 'latest'
589 at_version = 'latest'
566 elif version:
590 elif version:
567 pull_request_ver = PullRequestVersion.get_or_404(version)
591 pull_request_ver = PullRequestVersion.get_or_404(version)
568 pull_request_obj = pull_request_ver
592 pull_request_obj = pull_request_ver
569 _org_pull_request_obj = pull_request_ver.pull_request
593 _org_pull_request_obj = pull_request_ver.pull_request
570 at_version = pull_request_ver.pull_request_version_id
594 at_version = pull_request_ver.pull_request_version_id
571 else:
595 else:
572 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
596 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
573 pull_request_id)
597 pull_request_id)
574
598
575 pull_request_display_obj = PullRequest.get_pr_display_object(
599 pull_request_display_obj = PullRequest.get_pr_display_object(
576 pull_request_obj, _org_pull_request_obj)
600 pull_request_obj, _org_pull_request_obj)
577
601
578 return _org_pull_request_obj, pull_request_obj, \
602 return _org_pull_request_obj, pull_request_obj, \
579 pull_request_display_obj, at_version
603 pull_request_display_obj, at_version
580
604
581 def create(self, created_by, source_repo, source_ref, target_repo,
605 def create(self, created_by, source_repo, source_ref, target_repo,
582 target_ref, revisions, reviewers, observers, title, description=None,
606 target_ref, revisions, reviewers, observers, title, description=None,
583 common_ancestor_id=None,
607 common_ancestor_id=None,
584 description_renderer=None,
608 description_renderer=None,
585 reviewer_data=None, translator=None, auth_user=None):
609 reviewer_data=None, translator=None, auth_user=None):
586 translator = translator or get_current_request().translate
610 translator = translator or get_current_request().translate
587
611
588 created_by_user = self._get_user(created_by)
612 created_by_user = self._get_user(created_by)
589 auth_user = auth_user or created_by_user.AuthUser()
613 auth_user = auth_user or created_by_user.AuthUser()
590 source_repo = self._get_repo(source_repo)
614 source_repo = self._get_repo(source_repo)
591 target_repo = self._get_repo(target_repo)
615 target_repo = self._get_repo(target_repo)
592
616
593 pull_request = PullRequest()
617 pull_request = PullRequest()
594 pull_request.source_repo = source_repo
618 pull_request.source_repo = source_repo
595 pull_request.source_ref = source_ref
619 pull_request.source_ref = source_ref
596 pull_request.target_repo = target_repo
620 pull_request.target_repo = target_repo
597 pull_request.target_ref = target_ref
621 pull_request.target_ref = target_ref
598 pull_request.revisions = revisions
622 pull_request.revisions = revisions
599 pull_request.title = title
623 pull_request.title = title
600 pull_request.description = description
624 pull_request.description = description
601 pull_request.description_renderer = description_renderer
625 pull_request.description_renderer = description_renderer
602 pull_request.author = created_by_user
626 pull_request.author = created_by_user
603 pull_request.reviewer_data = reviewer_data
627 pull_request.reviewer_data = reviewer_data
604 pull_request.pull_request_state = pull_request.STATE_CREATING
628 pull_request.pull_request_state = pull_request.STATE_CREATING
605 pull_request.common_ancestor_id = common_ancestor_id
629 pull_request.common_ancestor_id = common_ancestor_id
606
630
607 Session().add(pull_request)
631 Session().add(pull_request)
608 Session().flush()
632 Session().flush()
609
633
610 reviewer_ids = set()
634 reviewer_ids = set()
611 # members / reviewers
635 # members / reviewers
612 for reviewer_object in reviewers:
636 for reviewer_object in reviewers:
613 user_id, reasons, mandatory, role, rules = reviewer_object
637 user_id, reasons, mandatory, role, rules = reviewer_object
614 user = self._get_user(user_id)
638 user = self._get_user(user_id)
615
639
616 # skip duplicates
640 # skip duplicates
617 if user.user_id in reviewer_ids:
641 if user.user_id in reviewer_ids:
618 continue
642 continue
619
643
620 reviewer_ids.add(user.user_id)
644 reviewer_ids.add(user.user_id)
621
645
622 reviewer = PullRequestReviewers()
646 reviewer = PullRequestReviewers()
623 reviewer.user = user
647 reviewer.user = user
624 reviewer.pull_request = pull_request
648 reviewer.pull_request = pull_request
625 reviewer.reasons = reasons
649 reviewer.reasons = reasons
626 reviewer.mandatory = mandatory
650 reviewer.mandatory = mandatory
627 reviewer.role = role
651 reviewer.role = role
628
652
629 # NOTE(marcink): pick only first rule for now
653 # NOTE(marcink): pick only first rule for now
630 rule_id = list(rules)[0] if rules else None
654 rule_id = list(rules)[0] if rules else None
631 rule = RepoReviewRule.get(rule_id) if rule_id else None
655 rule = RepoReviewRule.get(rule_id) if rule_id else None
632 if rule:
656 if rule:
633 review_group = rule.user_group_vote_rule(user_id)
657 review_group = rule.user_group_vote_rule(user_id)
634 # we check if this particular reviewer is member of a voting group
658 # we check if this particular reviewer is member of a voting group
635 if review_group:
659 if review_group:
636 # NOTE(marcink):
660 # NOTE(marcink):
637 # can be that user is member of more but we pick the first same,
661 # can be that user is member of more but we pick the first same,
638 # same as default reviewers algo
662 # same as default reviewers algo
639 review_group = review_group[0]
663 review_group = review_group[0]
640
664
641 rule_data = {
665 rule_data = {
642 'rule_name':
666 'rule_name':
643 rule.review_rule_name,
667 rule.review_rule_name,
644 'rule_user_group_entry_id':
668 'rule_user_group_entry_id':
645 review_group.repo_review_rule_users_group_id,
669 review_group.repo_review_rule_users_group_id,
646 'rule_user_group_name':
670 'rule_user_group_name':
647 review_group.users_group.users_group_name,
671 review_group.users_group.users_group_name,
648 'rule_user_group_members':
672 'rule_user_group_members':
649 [x.user.username for x in review_group.users_group.members],
673 [x.user.username for x in review_group.users_group.members],
650 'rule_user_group_members_id':
674 'rule_user_group_members_id':
651 [x.user.user_id for x in review_group.users_group.members],
675 [x.user.user_id for x in review_group.users_group.members],
652 }
676 }
653 # e.g {'vote_rule': -1, 'mandatory': True}
677 # e.g {'vote_rule': -1, 'mandatory': True}
654 rule_data.update(review_group.rule_data())
678 rule_data.update(review_group.rule_data())
655
679
656 reviewer.rule_data = rule_data
680 reviewer.rule_data = rule_data
657
681
658 Session().add(reviewer)
682 Session().add(reviewer)
659 Session().flush()
683 Session().flush()
660
684
661 for observer_object in observers:
685 for observer_object in observers:
662 user_id, reasons, mandatory, role, rules = observer_object
686 user_id, reasons, mandatory, role, rules = observer_object
663 user = self._get_user(user_id)
687 user = self._get_user(user_id)
664
688
665 # skip duplicates from reviewers
689 # skip duplicates from reviewers
666 if user.user_id in reviewer_ids:
690 if user.user_id in reviewer_ids:
667 continue
691 continue
668
692
669 #reviewer_ids.add(user.user_id)
693 #reviewer_ids.add(user.user_id)
670
694
671 observer = PullRequestReviewers()
695 observer = PullRequestReviewers()
672 observer.user = user
696 observer.user = user
673 observer.pull_request = pull_request
697 observer.pull_request = pull_request
674 observer.reasons = reasons
698 observer.reasons = reasons
675 observer.mandatory = mandatory
699 observer.mandatory = mandatory
676 observer.role = role
700 observer.role = role
677
701
678 # NOTE(marcink): pick only first rule for now
702 # NOTE(marcink): pick only first rule for now
679 rule_id = list(rules)[0] if rules else None
703 rule_id = list(rules)[0] if rules else None
680 rule = RepoReviewRule.get(rule_id) if rule_id else None
704 rule = RepoReviewRule.get(rule_id) if rule_id else None
681 if rule:
705 if rule:
682 # TODO(marcink): do we need this for observers ??
706 # TODO(marcink): do we need this for observers ??
683 pass
707 pass
684
708
685 Session().add(observer)
709 Session().add(observer)
686 Session().flush()
710 Session().flush()
687
711
688 # Set approval status to "Under Review" for all commits which are
712 # Set approval status to "Under Review" for all commits which are
689 # part of this pull request.
713 # part of this pull request.
690 ChangesetStatusModel().set_status(
714 ChangesetStatusModel().set_status(
691 repo=target_repo,
715 repo=target_repo,
692 status=ChangesetStatus.STATUS_UNDER_REVIEW,
716 status=ChangesetStatus.STATUS_UNDER_REVIEW,
693 user=created_by_user,
717 user=created_by_user,
694 pull_request=pull_request
718 pull_request=pull_request
695 )
719 )
696 # we commit early at this point. This has to do with a fact
720 # we commit early at this point. This has to do with a fact
697 # that before queries do some row-locking. And because of that
721 # that before queries do some row-locking. And because of that
698 # we need to commit and finish transaction before below validate call
722 # we need to commit and finish transaction before below validate call
699 # that for large repos could be long resulting in long row locks
723 # that for large repos could be long resulting in long row locks
700 Session().commit()
724 Session().commit()
701
725
702 # prepare workspace, and run initial merge simulation. Set state during that
726 # prepare workspace, and run initial merge simulation. Set state during that
703 # operation
727 # operation
704 pull_request = PullRequest.get(pull_request.pull_request_id)
728 pull_request = PullRequest.get(pull_request.pull_request_id)
705
729
706 # set as merging, for merge simulation, and if finished to created so we mark
730 # set as merging, for merge simulation, and if finished to created so we mark
707 # simulation is working fine
731 # simulation is working fine
708 with pull_request.set_state(PullRequest.STATE_MERGING,
732 with pull_request.set_state(PullRequest.STATE_MERGING,
709 final_state=PullRequest.STATE_CREATED) as state_obj:
733 final_state=PullRequest.STATE_CREATED) as state_obj:
710 MergeCheck.validate(
734 MergeCheck.validate(
711 pull_request, auth_user=auth_user, translator=translator)
735 pull_request, auth_user=auth_user, translator=translator)
712
736
713 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
737 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
714 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
738 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
715
739
716 creation_data = pull_request.get_api_data(with_merge_state=False)
740 creation_data = pull_request.get_api_data(with_merge_state=False)
717 self._log_audit_action(
741 self._log_audit_action(
718 'repo.pull_request.create', {'data': creation_data},
742 'repo.pull_request.create', {'data': creation_data},
719 auth_user, pull_request)
743 auth_user, pull_request)
720
744
721 return pull_request
745 return pull_request
722
746
723 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
747 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
724 pull_request = self.__get_pull_request(pull_request)
748 pull_request = self.__get_pull_request(pull_request)
725 target_scm = pull_request.target_repo.scm_instance()
749 target_scm = pull_request.target_repo.scm_instance()
726 if action == 'create':
750 if action == 'create':
727 trigger_hook = hooks_utils.trigger_create_pull_request_hook
751 trigger_hook = hooks_utils.trigger_create_pull_request_hook
728 elif action == 'merge':
752 elif action == 'merge':
729 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
753 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
730 elif action == 'close':
754 elif action == 'close':
731 trigger_hook = hooks_utils.trigger_close_pull_request_hook
755 trigger_hook = hooks_utils.trigger_close_pull_request_hook
732 elif action == 'review_status_change':
756 elif action == 'review_status_change':
733 trigger_hook = hooks_utils.trigger_review_pull_request_hook
757 trigger_hook = hooks_utils.trigger_review_pull_request_hook
734 elif action == 'update':
758 elif action == 'update':
735 trigger_hook = hooks_utils.trigger_update_pull_request_hook
759 trigger_hook = hooks_utils.trigger_update_pull_request_hook
736 elif action == 'comment':
760 elif action == 'comment':
737 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
761 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
738 elif action == 'comment_edit':
762 elif action == 'comment_edit':
739 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
763 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
740 else:
764 else:
741 return
765 return
742
766
743 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
767 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
744 pull_request, action, trigger_hook)
768 pull_request, action, trigger_hook)
745 trigger_hook(
769 trigger_hook(
746 username=user.username,
770 username=user.username,
747 repo_name=pull_request.target_repo.repo_name,
771 repo_name=pull_request.target_repo.repo_name,
748 repo_type=target_scm.alias,
772 repo_type=target_scm.alias,
749 pull_request=pull_request,
773 pull_request=pull_request,
750 data=data)
774 data=data)
751
775
752 def _get_commit_ids(self, pull_request):
776 def _get_commit_ids(self, pull_request):
753 """
777 """
754 Return the commit ids of the merged pull request.
778 Return the commit ids of the merged pull request.
755
779
756 This method is not dealing correctly yet with the lack of autoupdates
780 This method is not dealing correctly yet with the lack of autoupdates
757 nor with the implicit target updates.
781 nor with the implicit target updates.
758 For example: if a commit in the source repo is already in the target it
782 For example: if a commit in the source repo is already in the target it
759 will be reported anyways.
783 will be reported anyways.
760 """
784 """
761 merge_rev = pull_request.merge_rev
785 merge_rev = pull_request.merge_rev
762 if merge_rev is None:
786 if merge_rev is None:
763 raise ValueError('This pull request was not merged yet')
787 raise ValueError('This pull request was not merged yet')
764
788
765 commit_ids = list(pull_request.revisions)
789 commit_ids = list(pull_request.revisions)
766 if merge_rev not in commit_ids:
790 if merge_rev not in commit_ids:
767 commit_ids.append(merge_rev)
791 commit_ids.append(merge_rev)
768
792
769 return commit_ids
793 return commit_ids
770
794
771 def merge_repo(self, pull_request, user, extras):
795 def merge_repo(self, pull_request, user, extras):
772 log.debug("Merging pull request %s", pull_request.pull_request_id)
796 log.debug("Merging pull request %s", pull_request.pull_request_id)
773 extras['user_agent'] = 'internal-merge'
797 extras['user_agent'] = 'internal-merge'
774 merge_state = self._merge_pull_request(pull_request, user, extras)
798 merge_state = self._merge_pull_request(pull_request, user, extras)
775 if merge_state.executed:
799 if merge_state.executed:
776 log.debug("Merge was successful, updating the pull request comments.")
800 log.debug("Merge was successful, updating the pull request comments.")
777 self._comment_and_close_pr(pull_request, user, merge_state)
801 self._comment_and_close_pr(pull_request, user, merge_state)
778
802
779 self._log_audit_action(
803 self._log_audit_action(
780 'repo.pull_request.merge',
804 'repo.pull_request.merge',
781 {'merge_state': merge_state.__dict__},
805 {'merge_state': merge_state.__dict__},
782 user, pull_request)
806 user, pull_request)
783
807
784 else:
808 else:
785 log.warn("Merge failed, not updating the pull request.")
809 log.warn("Merge failed, not updating the pull request.")
786 return merge_state
810 return merge_state
787
811
788 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
812 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
789 target_vcs = pull_request.target_repo.scm_instance()
813 target_vcs = pull_request.target_repo.scm_instance()
790 source_vcs = pull_request.source_repo.scm_instance()
814 source_vcs = pull_request.source_repo.scm_instance()
791
815
792 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
816 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
793 pr_id=pull_request.pull_request_id,
817 pr_id=pull_request.pull_request_id,
794 pr_title=pull_request.title,
818 pr_title=pull_request.title,
795 source_repo=source_vcs.name,
819 source_repo=source_vcs.name,
796 source_ref_name=pull_request.source_ref_parts.name,
820 source_ref_name=pull_request.source_ref_parts.name,
797 target_repo=target_vcs.name,
821 target_repo=target_vcs.name,
798 target_ref_name=pull_request.target_ref_parts.name,
822 target_ref_name=pull_request.target_ref_parts.name,
799 )
823 )
800
824
801 workspace_id = self._workspace_id(pull_request)
825 workspace_id = self._workspace_id(pull_request)
802 repo_id = pull_request.target_repo.repo_id
826 repo_id = pull_request.target_repo.repo_id
803 use_rebase = self._use_rebase_for_merging(pull_request)
827 use_rebase = self._use_rebase_for_merging(pull_request)
804 close_branch = self._close_branch_before_merging(pull_request)
828 close_branch = self._close_branch_before_merging(pull_request)
805 user_name = self._user_name_for_merging(pull_request, user)
829 user_name = self._user_name_for_merging(pull_request, user)
806
830
807 target_ref = self._refresh_reference(
831 target_ref = self._refresh_reference(
808 pull_request.target_ref_parts, target_vcs)
832 pull_request.target_ref_parts, target_vcs)
809
833
810 callback_daemon, extras = prepare_callback_daemon(
834 callback_daemon, extras = prepare_callback_daemon(
811 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
835 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
812 host=vcs_settings.HOOKS_HOST,
836 host=vcs_settings.HOOKS_HOST,
813 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
837 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
814
838
815 with callback_daemon:
839 with callback_daemon:
816 # TODO: johbo: Implement a clean way to run a config_override
840 # TODO: johbo: Implement a clean way to run a config_override
817 # for a single call.
841 # for a single call.
818 target_vcs.config.set(
842 target_vcs.config.set(
819 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
843 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
820
844
821 merge_state = target_vcs.merge(
845 merge_state = target_vcs.merge(
822 repo_id, workspace_id, target_ref, source_vcs,
846 repo_id, workspace_id, target_ref, source_vcs,
823 pull_request.source_ref_parts,
847 pull_request.source_ref_parts,
824 user_name=user_name, user_email=user.email,
848 user_name=user_name, user_email=user.email,
825 message=message, use_rebase=use_rebase,
849 message=message, use_rebase=use_rebase,
826 close_branch=close_branch)
850 close_branch=close_branch)
827 return merge_state
851 return merge_state
828
852
829 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
853 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
830 pull_request.merge_rev = merge_state.merge_ref.commit_id
854 pull_request.merge_rev = merge_state.merge_ref.commit_id
831 pull_request.updated_on = datetime.datetime.now()
855 pull_request.updated_on = datetime.datetime.now()
832 close_msg = close_msg or 'Pull request merged and closed'
856 close_msg = close_msg or 'Pull request merged and closed'
833
857
834 CommentsModel().create(
858 CommentsModel().create(
835 text=safe_unicode(close_msg),
859 text=safe_unicode(close_msg),
836 repo=pull_request.target_repo.repo_id,
860 repo=pull_request.target_repo.repo_id,
837 user=user.user_id,
861 user=user.user_id,
838 pull_request=pull_request.pull_request_id,
862 pull_request=pull_request.pull_request_id,
839 f_path=None,
863 f_path=None,
840 line_no=None,
864 line_no=None,
841 closing_pr=True
865 closing_pr=True
842 )
866 )
843
867
844 Session().add(pull_request)
868 Session().add(pull_request)
845 Session().flush()
869 Session().flush()
846 # TODO: paris: replace invalidation with less radical solution
870 # TODO: paris: replace invalidation with less radical solution
847 ScmModel().mark_for_invalidation(
871 ScmModel().mark_for_invalidation(
848 pull_request.target_repo.repo_name)
872 pull_request.target_repo.repo_name)
849 self.trigger_pull_request_hook(pull_request, user, 'merge')
873 self.trigger_pull_request_hook(pull_request, user, 'merge')
850
874
851 def has_valid_update_type(self, pull_request):
875 def has_valid_update_type(self, pull_request):
852 source_ref_type = pull_request.source_ref_parts.type
876 source_ref_type = pull_request.source_ref_parts.type
853 return source_ref_type in self.REF_TYPES
877 return source_ref_type in self.REF_TYPES
854
878
855 def get_flow_commits(self, pull_request):
879 def get_flow_commits(self, pull_request):
856
880
857 # source repo
881 # source repo
858 source_ref_name = pull_request.source_ref_parts.name
882 source_ref_name = pull_request.source_ref_parts.name
859 source_ref_type = pull_request.source_ref_parts.type
883 source_ref_type = pull_request.source_ref_parts.type
860 source_ref_id = pull_request.source_ref_parts.commit_id
884 source_ref_id = pull_request.source_ref_parts.commit_id
861 source_repo = pull_request.source_repo.scm_instance()
885 source_repo = pull_request.source_repo.scm_instance()
862
886
863 try:
887 try:
864 if source_ref_type in self.REF_TYPES:
888 if source_ref_type in self.REF_TYPES:
865 source_commit = source_repo.get_commit(source_ref_name)
889 source_commit = source_repo.get_commit(source_ref_name)
866 else:
890 else:
867 source_commit = source_repo.get_commit(source_ref_id)
891 source_commit = source_repo.get_commit(source_ref_id)
868 except CommitDoesNotExistError:
892 except CommitDoesNotExistError:
869 raise SourceRefMissing()
893 raise SourceRefMissing()
870
894
871 # target repo
895 # target repo
872 target_ref_name = pull_request.target_ref_parts.name
896 target_ref_name = pull_request.target_ref_parts.name
873 target_ref_type = pull_request.target_ref_parts.type
897 target_ref_type = pull_request.target_ref_parts.type
874 target_ref_id = pull_request.target_ref_parts.commit_id
898 target_ref_id = pull_request.target_ref_parts.commit_id
875 target_repo = pull_request.target_repo.scm_instance()
899 target_repo = pull_request.target_repo.scm_instance()
876
900
877 try:
901 try:
878 if target_ref_type in self.REF_TYPES:
902 if target_ref_type in self.REF_TYPES:
879 target_commit = target_repo.get_commit(target_ref_name)
903 target_commit = target_repo.get_commit(target_ref_name)
880 else:
904 else:
881 target_commit = target_repo.get_commit(target_ref_id)
905 target_commit = target_repo.get_commit(target_ref_id)
882 except CommitDoesNotExistError:
906 except CommitDoesNotExistError:
883 raise TargetRefMissing()
907 raise TargetRefMissing()
884
908
885 return source_commit, target_commit
909 return source_commit, target_commit
886
910
887 def update_commits(self, pull_request, updating_user):
911 def update_commits(self, pull_request, updating_user):
888 """
912 """
889 Get the updated list of commits for the pull request
913 Get the updated list of commits for the pull request
890 and return the new pull request version and the list
914 and return the new pull request version and the list
891 of commits processed by this update action
915 of commits processed by this update action
892
916
893 updating_user is the user_object who triggered the update
917 updating_user is the user_object who triggered the update
894 """
918 """
895 pull_request = self.__get_pull_request(pull_request)
919 pull_request = self.__get_pull_request(pull_request)
896 source_ref_type = pull_request.source_ref_parts.type
920 source_ref_type = pull_request.source_ref_parts.type
897 source_ref_name = pull_request.source_ref_parts.name
921 source_ref_name = pull_request.source_ref_parts.name
898 source_ref_id = pull_request.source_ref_parts.commit_id
922 source_ref_id = pull_request.source_ref_parts.commit_id
899
923
900 target_ref_type = pull_request.target_ref_parts.type
924 target_ref_type = pull_request.target_ref_parts.type
901 target_ref_name = pull_request.target_ref_parts.name
925 target_ref_name = pull_request.target_ref_parts.name
902 target_ref_id = pull_request.target_ref_parts.commit_id
926 target_ref_id = pull_request.target_ref_parts.commit_id
903
927
904 if not self.has_valid_update_type(pull_request):
928 if not self.has_valid_update_type(pull_request):
905 log.debug("Skipping update of pull request %s due to ref type: %s",
929 log.debug("Skipping update of pull request %s due to ref type: %s",
906 pull_request, source_ref_type)
930 pull_request, source_ref_type)
907 return UpdateResponse(
931 return UpdateResponse(
908 executed=False,
932 executed=False,
909 reason=UpdateFailureReason.WRONG_REF_TYPE,
933 reason=UpdateFailureReason.WRONG_REF_TYPE,
910 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
934 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
911 source_changed=False, target_changed=False)
935 source_changed=False, target_changed=False)
912
936
913 try:
937 try:
914 source_commit, target_commit = self.get_flow_commits(pull_request)
938 source_commit, target_commit = self.get_flow_commits(pull_request)
915 except SourceRefMissing:
939 except SourceRefMissing:
916 return UpdateResponse(
940 return UpdateResponse(
917 executed=False,
941 executed=False,
918 reason=UpdateFailureReason.MISSING_SOURCE_REF,
942 reason=UpdateFailureReason.MISSING_SOURCE_REF,
919 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
943 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
920 source_changed=False, target_changed=False)
944 source_changed=False, target_changed=False)
921 except TargetRefMissing:
945 except TargetRefMissing:
922 return UpdateResponse(
946 return UpdateResponse(
923 executed=False,
947 executed=False,
924 reason=UpdateFailureReason.MISSING_TARGET_REF,
948 reason=UpdateFailureReason.MISSING_TARGET_REF,
925 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
949 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
926 source_changed=False, target_changed=False)
950 source_changed=False, target_changed=False)
927
951
928 source_changed = source_ref_id != source_commit.raw_id
952 source_changed = source_ref_id != source_commit.raw_id
929 target_changed = target_ref_id != target_commit.raw_id
953 target_changed = target_ref_id != target_commit.raw_id
930
954
931 if not (source_changed or target_changed):
955 if not (source_changed or target_changed):
932 log.debug("Nothing changed in pull request %s", pull_request)
956 log.debug("Nothing changed in pull request %s", pull_request)
933 return UpdateResponse(
957 return UpdateResponse(
934 executed=False,
958 executed=False,
935 reason=UpdateFailureReason.NO_CHANGE,
959 reason=UpdateFailureReason.NO_CHANGE,
936 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
960 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
937 source_changed=target_changed, target_changed=source_changed)
961 source_changed=target_changed, target_changed=source_changed)
938
962
939 change_in_found = 'target repo' if target_changed else 'source repo'
963 change_in_found = 'target repo' if target_changed else 'source repo'
940 log.debug('Updating pull request because of change in %s detected',
964 log.debug('Updating pull request because of change in %s detected',
941 change_in_found)
965 change_in_found)
942
966
943 # Finally there is a need for an update, in case of source change
967 # Finally there is a need for an update, in case of source change
944 # we create a new version, else just an update
968 # we create a new version, else just an update
945 if source_changed:
969 if source_changed:
946 pull_request_version = self._create_version_from_snapshot(pull_request)
970 pull_request_version = self._create_version_from_snapshot(pull_request)
947 self._link_comments_to_version(pull_request_version)
971 self._link_comments_to_version(pull_request_version)
948 else:
972 else:
949 try:
973 try:
950 ver = pull_request.versions[-1]
974 ver = pull_request.versions[-1]
951 except IndexError:
975 except IndexError:
952 ver = None
976 ver = None
953
977
954 pull_request.pull_request_version_id = \
978 pull_request.pull_request_version_id = \
955 ver.pull_request_version_id if ver else None
979 ver.pull_request_version_id if ver else None
956 pull_request_version = pull_request
980 pull_request_version = pull_request
957
981
958 source_repo = pull_request.source_repo.scm_instance()
982 source_repo = pull_request.source_repo.scm_instance()
959 target_repo = pull_request.target_repo.scm_instance()
983 target_repo = pull_request.target_repo.scm_instance()
960
984
961 # re-compute commit ids
985 # re-compute commit ids
962 old_commit_ids = pull_request.revisions
986 old_commit_ids = pull_request.revisions
963 pre_load = ["author", "date", "message", "branch"]
987 pre_load = ["author", "date", "message", "branch"]
964 commit_ranges = target_repo.compare(
988 commit_ranges = target_repo.compare(
965 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
989 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
966 pre_load=pre_load)
990 pre_load=pre_load)
967
991
968 target_ref = target_commit.raw_id
992 target_ref = target_commit.raw_id
969 source_ref = source_commit.raw_id
993 source_ref = source_commit.raw_id
970 ancestor_commit_id = target_repo.get_common_ancestor(
994 ancestor_commit_id = target_repo.get_common_ancestor(
971 target_ref, source_ref, source_repo)
995 target_ref, source_ref, source_repo)
972
996
973 if not ancestor_commit_id:
997 if not ancestor_commit_id:
974 raise ValueError(
998 raise ValueError(
975 'cannot calculate diff info without a common ancestor. '
999 'cannot calculate diff info without a common ancestor. '
976 'Make sure both repositories are related, and have a common forking commit.')
1000 'Make sure both repositories are related, and have a common forking commit.')
977
1001
978 pull_request.common_ancestor_id = ancestor_commit_id
1002 pull_request.common_ancestor_id = ancestor_commit_id
979
1003
980 pull_request.source_ref = '%s:%s:%s' % (
1004 pull_request.source_ref = '%s:%s:%s' % (
981 source_ref_type, source_ref_name, source_commit.raw_id)
1005 source_ref_type, source_ref_name, source_commit.raw_id)
982 pull_request.target_ref = '%s:%s:%s' % (
1006 pull_request.target_ref = '%s:%s:%s' % (
983 target_ref_type, target_ref_name, ancestor_commit_id)
1007 target_ref_type, target_ref_name, ancestor_commit_id)
984
1008
985 pull_request.revisions = [
1009 pull_request.revisions = [
986 commit.raw_id for commit in reversed(commit_ranges)]
1010 commit.raw_id for commit in reversed(commit_ranges)]
987 pull_request.updated_on = datetime.datetime.now()
1011 pull_request.updated_on = datetime.datetime.now()
988 Session().add(pull_request)
1012 Session().add(pull_request)
989 new_commit_ids = pull_request.revisions
1013 new_commit_ids = pull_request.revisions
990
1014
991 old_diff_data, new_diff_data = self._generate_update_diffs(
1015 old_diff_data, new_diff_data = self._generate_update_diffs(
992 pull_request, pull_request_version)
1016 pull_request, pull_request_version)
993
1017
994 # calculate commit and file changes
1018 # calculate commit and file changes
995 commit_changes = self._calculate_commit_id_changes(
1019 commit_changes = self._calculate_commit_id_changes(
996 old_commit_ids, new_commit_ids)
1020 old_commit_ids, new_commit_ids)
997 file_changes = self._calculate_file_changes(
1021 file_changes = self._calculate_file_changes(
998 old_diff_data, new_diff_data)
1022 old_diff_data, new_diff_data)
999
1023
1000 # set comments as outdated if DIFFS changed
1024 # set comments as outdated if DIFFS changed
1001 CommentsModel().outdate_comments(
1025 CommentsModel().outdate_comments(
1002 pull_request, old_diff_data=old_diff_data,
1026 pull_request, old_diff_data=old_diff_data,
1003 new_diff_data=new_diff_data)
1027 new_diff_data=new_diff_data)
1004
1028
1005 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1029 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1006 file_node_changes = (
1030 file_node_changes = (
1007 file_changes.added or file_changes.modified or file_changes.removed)
1031 file_changes.added or file_changes.modified or file_changes.removed)
1008 pr_has_changes = valid_commit_changes or file_node_changes
1032 pr_has_changes = valid_commit_changes or file_node_changes
1009
1033
1010 # Add an automatic comment to the pull request, in case
1034 # Add an automatic comment to the pull request, in case
1011 # anything has changed
1035 # anything has changed
1012 if pr_has_changes:
1036 if pr_has_changes:
1013 update_comment = CommentsModel().create(
1037 update_comment = CommentsModel().create(
1014 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1038 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1015 repo=pull_request.target_repo,
1039 repo=pull_request.target_repo,
1016 user=pull_request.author,
1040 user=pull_request.author,
1017 pull_request=pull_request,
1041 pull_request=pull_request,
1018 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1042 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1019
1043
1020 # Update status to "Under Review" for added commits
1044 # Update status to "Under Review" for added commits
1021 for commit_id in commit_changes.added:
1045 for commit_id in commit_changes.added:
1022 ChangesetStatusModel().set_status(
1046 ChangesetStatusModel().set_status(
1023 repo=pull_request.source_repo,
1047 repo=pull_request.source_repo,
1024 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1048 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1025 comment=update_comment,
1049 comment=update_comment,
1026 user=pull_request.author,
1050 user=pull_request.author,
1027 pull_request=pull_request,
1051 pull_request=pull_request,
1028 revision=commit_id)
1052 revision=commit_id)
1029
1053
1030 # send update email to users
1054 # send update email to users
1031 try:
1055 try:
1032 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1056 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1033 ancestor_commit_id=ancestor_commit_id,
1057 ancestor_commit_id=ancestor_commit_id,
1034 commit_changes=commit_changes,
1058 commit_changes=commit_changes,
1035 file_changes=file_changes)
1059 file_changes=file_changes)
1036 except Exception:
1060 except Exception:
1037 log.exception('Failed to send email notification to users')
1061 log.exception('Failed to send email notification to users')
1038
1062
1039 log.debug(
1063 log.debug(
1040 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1064 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1041 'removed_ids: %s', pull_request.pull_request_id,
1065 'removed_ids: %s', pull_request.pull_request_id,
1042 commit_changes.added, commit_changes.common, commit_changes.removed)
1066 commit_changes.added, commit_changes.common, commit_changes.removed)
1043 log.debug(
1067 log.debug(
1044 'Updated pull request with the following file changes: %s',
1068 'Updated pull request with the following file changes: %s',
1045 file_changes)
1069 file_changes)
1046
1070
1047 log.info(
1071 log.info(
1048 "Updated pull request %s from commit %s to commit %s, "
1072 "Updated pull request %s from commit %s to commit %s, "
1049 "stored new version %s of this pull request.",
1073 "stored new version %s of this pull request.",
1050 pull_request.pull_request_id, source_ref_id,
1074 pull_request.pull_request_id, source_ref_id,
1051 pull_request.source_ref_parts.commit_id,
1075 pull_request.source_ref_parts.commit_id,
1052 pull_request_version.pull_request_version_id)
1076 pull_request_version.pull_request_version_id)
1053 Session().commit()
1077 Session().commit()
1054 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1078 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1055
1079
1056 return UpdateResponse(
1080 return UpdateResponse(
1057 executed=True, reason=UpdateFailureReason.NONE,
1081 executed=True, reason=UpdateFailureReason.NONE,
1058 old=pull_request, new=pull_request_version,
1082 old=pull_request, new=pull_request_version,
1059 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1083 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1060 source_changed=source_changed, target_changed=target_changed)
1084 source_changed=source_changed, target_changed=target_changed)
1061
1085
1062 def _create_version_from_snapshot(self, pull_request):
1086 def _create_version_from_snapshot(self, pull_request):
1063 version = PullRequestVersion()
1087 version = PullRequestVersion()
1064 version.title = pull_request.title
1088 version.title = pull_request.title
1065 version.description = pull_request.description
1089 version.description = pull_request.description
1066 version.status = pull_request.status
1090 version.status = pull_request.status
1067 version.pull_request_state = pull_request.pull_request_state
1091 version.pull_request_state = pull_request.pull_request_state
1068 version.created_on = datetime.datetime.now()
1092 version.created_on = datetime.datetime.now()
1069 version.updated_on = pull_request.updated_on
1093 version.updated_on = pull_request.updated_on
1070 version.user_id = pull_request.user_id
1094 version.user_id = pull_request.user_id
1071 version.source_repo = pull_request.source_repo
1095 version.source_repo = pull_request.source_repo
1072 version.source_ref = pull_request.source_ref
1096 version.source_ref = pull_request.source_ref
1073 version.target_repo = pull_request.target_repo
1097 version.target_repo = pull_request.target_repo
1074 version.target_ref = pull_request.target_ref
1098 version.target_ref = pull_request.target_ref
1075
1099
1076 version._last_merge_source_rev = pull_request._last_merge_source_rev
1100 version._last_merge_source_rev = pull_request._last_merge_source_rev
1077 version._last_merge_target_rev = pull_request._last_merge_target_rev
1101 version._last_merge_target_rev = pull_request._last_merge_target_rev
1078 version.last_merge_status = pull_request.last_merge_status
1102 version.last_merge_status = pull_request.last_merge_status
1079 version.last_merge_metadata = pull_request.last_merge_metadata
1103 version.last_merge_metadata = pull_request.last_merge_metadata
1080 version.shadow_merge_ref = pull_request.shadow_merge_ref
1104 version.shadow_merge_ref = pull_request.shadow_merge_ref
1081 version.merge_rev = pull_request.merge_rev
1105 version.merge_rev = pull_request.merge_rev
1082 version.reviewer_data = pull_request.reviewer_data
1106 version.reviewer_data = pull_request.reviewer_data
1083
1107
1084 version.revisions = pull_request.revisions
1108 version.revisions = pull_request.revisions
1085 version.common_ancestor_id = pull_request.common_ancestor_id
1109 version.common_ancestor_id = pull_request.common_ancestor_id
1086 version.pull_request = pull_request
1110 version.pull_request = pull_request
1087 Session().add(version)
1111 Session().add(version)
1088 Session().flush()
1112 Session().flush()
1089
1113
1090 return version
1114 return version
1091
1115
1092 def _generate_update_diffs(self, pull_request, pull_request_version):
1116 def _generate_update_diffs(self, pull_request, pull_request_version):
1093
1117
1094 diff_context = (
1118 diff_context = (
1095 self.DIFF_CONTEXT +
1119 self.DIFF_CONTEXT +
1096 CommentsModel.needed_extra_diff_context())
1120 CommentsModel.needed_extra_diff_context())
1097 hide_whitespace_changes = False
1121 hide_whitespace_changes = False
1098 source_repo = pull_request_version.source_repo
1122 source_repo = pull_request_version.source_repo
1099 source_ref_id = pull_request_version.source_ref_parts.commit_id
1123 source_ref_id = pull_request_version.source_ref_parts.commit_id
1100 target_ref_id = pull_request_version.target_ref_parts.commit_id
1124 target_ref_id = pull_request_version.target_ref_parts.commit_id
1101 old_diff = self._get_diff_from_pr_or_version(
1125 old_diff = self._get_diff_from_pr_or_version(
1102 source_repo, source_ref_id, target_ref_id,
1126 source_repo, source_ref_id, target_ref_id,
1103 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1127 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1104
1128
1105 source_repo = pull_request.source_repo
1129 source_repo = pull_request.source_repo
1106 source_ref_id = pull_request.source_ref_parts.commit_id
1130 source_ref_id = pull_request.source_ref_parts.commit_id
1107 target_ref_id = pull_request.target_ref_parts.commit_id
1131 target_ref_id = pull_request.target_ref_parts.commit_id
1108
1132
1109 new_diff = self._get_diff_from_pr_or_version(
1133 new_diff = self._get_diff_from_pr_or_version(
1110 source_repo, source_ref_id, target_ref_id,
1134 source_repo, source_ref_id, target_ref_id,
1111 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1135 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1112
1136
1113 old_diff_data = diffs.DiffProcessor(old_diff)
1137 old_diff_data = diffs.DiffProcessor(old_diff)
1114 old_diff_data.prepare()
1138 old_diff_data.prepare()
1115 new_diff_data = diffs.DiffProcessor(new_diff)
1139 new_diff_data = diffs.DiffProcessor(new_diff)
1116 new_diff_data.prepare()
1140 new_diff_data.prepare()
1117
1141
1118 return old_diff_data, new_diff_data
1142 return old_diff_data, new_diff_data
1119
1143
1120 def _link_comments_to_version(self, pull_request_version):
1144 def _link_comments_to_version(self, pull_request_version):
1121 """
1145 """
1122 Link all unlinked comments of this pull request to the given version.
1146 Link all unlinked comments of this pull request to the given version.
1123
1147
1124 :param pull_request_version: The `PullRequestVersion` to which
1148 :param pull_request_version: The `PullRequestVersion` to which
1125 the comments shall be linked.
1149 the comments shall be linked.
1126
1150
1127 """
1151 """
1128 pull_request = pull_request_version.pull_request
1152 pull_request = pull_request_version.pull_request
1129 comments = ChangesetComment.query()\
1153 comments = ChangesetComment.query()\
1130 .filter(
1154 .filter(
1131 # TODO: johbo: Should we query for the repo at all here?
1155 # TODO: johbo: Should we query for the repo at all here?
1132 # Pending decision on how comments of PRs are to be related
1156 # Pending decision on how comments of PRs are to be related
1133 # to either the source repo, the target repo or no repo at all.
1157 # to either the source repo, the target repo or no repo at all.
1134 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1158 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1135 ChangesetComment.pull_request == pull_request,
1159 ChangesetComment.pull_request == pull_request,
1136 ChangesetComment.pull_request_version == None)\
1160 ChangesetComment.pull_request_version == None)\
1137 .order_by(ChangesetComment.comment_id.asc())
1161 .order_by(ChangesetComment.comment_id.asc())
1138
1162
1139 # TODO: johbo: Find out why this breaks if it is done in a bulk
1163 # TODO: johbo: Find out why this breaks if it is done in a bulk
1140 # operation.
1164 # operation.
1141 for comment in comments:
1165 for comment in comments:
1142 comment.pull_request_version_id = (
1166 comment.pull_request_version_id = (
1143 pull_request_version.pull_request_version_id)
1167 pull_request_version.pull_request_version_id)
1144 Session().add(comment)
1168 Session().add(comment)
1145
1169
1146 def _calculate_commit_id_changes(self, old_ids, new_ids):
1170 def _calculate_commit_id_changes(self, old_ids, new_ids):
1147 added = [x for x in new_ids if x not in old_ids]
1171 added = [x for x in new_ids if x not in old_ids]
1148 common = [x for x in new_ids if x in old_ids]
1172 common = [x for x in new_ids if x in old_ids]
1149 removed = [x for x in old_ids if x not in new_ids]
1173 removed = [x for x in old_ids if x not in new_ids]
1150 total = new_ids
1174 total = new_ids
1151 return ChangeTuple(added, common, removed, total)
1175 return ChangeTuple(added, common, removed, total)
1152
1176
1153 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1177 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1154
1178
1155 old_files = OrderedDict()
1179 old_files = OrderedDict()
1156 for diff_data in old_diff_data.parsed_diff:
1180 for diff_data in old_diff_data.parsed_diff:
1157 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1181 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1158
1182
1159 added_files = []
1183 added_files = []
1160 modified_files = []
1184 modified_files = []
1161 removed_files = []
1185 removed_files = []
1162 for diff_data in new_diff_data.parsed_diff:
1186 for diff_data in new_diff_data.parsed_diff:
1163 new_filename = diff_data['filename']
1187 new_filename = diff_data['filename']
1164 new_hash = md5_safe(diff_data['raw_diff'])
1188 new_hash = md5_safe(diff_data['raw_diff'])
1165
1189
1166 old_hash = old_files.get(new_filename)
1190 old_hash = old_files.get(new_filename)
1167 if not old_hash:
1191 if not old_hash:
1168 # file is not present in old diff, we have to figure out from parsed diff
1192 # file is not present in old diff, we have to figure out from parsed diff
1169 # operation ADD/REMOVE
1193 # operation ADD/REMOVE
1170 operations_dict = diff_data['stats']['ops']
1194 operations_dict = diff_data['stats']['ops']
1171 if diffs.DEL_FILENODE in operations_dict:
1195 if diffs.DEL_FILENODE in operations_dict:
1172 removed_files.append(new_filename)
1196 removed_files.append(new_filename)
1173 else:
1197 else:
1174 added_files.append(new_filename)
1198 added_files.append(new_filename)
1175 else:
1199 else:
1176 if new_hash != old_hash:
1200 if new_hash != old_hash:
1177 modified_files.append(new_filename)
1201 modified_files.append(new_filename)
1178 # now remove a file from old, since we have seen it already
1202 # now remove a file from old, since we have seen it already
1179 del old_files[new_filename]
1203 del old_files[new_filename]
1180
1204
1181 # removed files is when there are present in old, but not in NEW,
1205 # removed files is when there are present in old, but not in NEW,
1182 # since we remove old files that are present in new diff, left-overs
1206 # since we remove old files that are present in new diff, left-overs
1183 # if any should be the removed files
1207 # if any should be the removed files
1184 removed_files.extend(old_files.keys())
1208 removed_files.extend(old_files.keys())
1185
1209
1186 return FileChangeTuple(added_files, modified_files, removed_files)
1210 return FileChangeTuple(added_files, modified_files, removed_files)
1187
1211
1188 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1212 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1189 """
1213 """
1190 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1214 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1191 so it's always looking the same disregarding on which default
1215 so it's always looking the same disregarding on which default
1192 renderer system is using.
1216 renderer system is using.
1193
1217
1194 :param ancestor_commit_id: ancestor raw_id
1218 :param ancestor_commit_id: ancestor raw_id
1195 :param changes: changes named tuple
1219 :param changes: changes named tuple
1196 :param file_changes: file changes named tuple
1220 :param file_changes: file changes named tuple
1197
1221
1198 """
1222 """
1199 new_status = ChangesetStatus.get_status_lbl(
1223 new_status = ChangesetStatus.get_status_lbl(
1200 ChangesetStatus.STATUS_UNDER_REVIEW)
1224 ChangesetStatus.STATUS_UNDER_REVIEW)
1201
1225
1202 changed_files = (
1226 changed_files = (
1203 file_changes.added + file_changes.modified + file_changes.removed)
1227 file_changes.added + file_changes.modified + file_changes.removed)
1204
1228
1205 params = {
1229 params = {
1206 'under_review_label': new_status,
1230 'under_review_label': new_status,
1207 'added_commits': changes.added,
1231 'added_commits': changes.added,
1208 'removed_commits': changes.removed,
1232 'removed_commits': changes.removed,
1209 'changed_files': changed_files,
1233 'changed_files': changed_files,
1210 'added_files': file_changes.added,
1234 'added_files': file_changes.added,
1211 'modified_files': file_changes.modified,
1235 'modified_files': file_changes.modified,
1212 'removed_files': file_changes.removed,
1236 'removed_files': file_changes.removed,
1213 'ancestor_commit_id': ancestor_commit_id
1237 'ancestor_commit_id': ancestor_commit_id
1214 }
1238 }
1215 renderer = RstTemplateRenderer()
1239 renderer = RstTemplateRenderer()
1216 return renderer.render('pull_request_update.mako', **params)
1240 return renderer.render('pull_request_update.mako', **params)
1217
1241
1218 def edit(self, pull_request, title, description, description_renderer, user):
1242 def edit(self, pull_request, title, description, description_renderer, user):
1219 pull_request = self.__get_pull_request(pull_request)
1243 pull_request = self.__get_pull_request(pull_request)
1220 old_data = pull_request.get_api_data(with_merge_state=False)
1244 old_data = pull_request.get_api_data(with_merge_state=False)
1221 if pull_request.is_closed():
1245 if pull_request.is_closed():
1222 raise ValueError('This pull request is closed')
1246 raise ValueError('This pull request is closed')
1223 if title:
1247 if title:
1224 pull_request.title = title
1248 pull_request.title = title
1225 pull_request.description = description
1249 pull_request.description = description
1226 pull_request.updated_on = datetime.datetime.now()
1250 pull_request.updated_on = datetime.datetime.now()
1227 pull_request.description_renderer = description_renderer
1251 pull_request.description_renderer = description_renderer
1228 Session().add(pull_request)
1252 Session().add(pull_request)
1229 self._log_audit_action(
1253 self._log_audit_action(
1230 'repo.pull_request.edit', {'old_data': old_data},
1254 'repo.pull_request.edit', {'old_data': old_data},
1231 user, pull_request)
1255 user, pull_request)
1232
1256
1233 def update_reviewers(self, pull_request, reviewer_data, user):
1257 def update_reviewers(self, pull_request, reviewer_data, user):
1234 """
1258 """
1235 Update the reviewers in the pull request
1259 Update the reviewers in the pull request
1236
1260
1237 :param pull_request: the pr to update
1261 :param pull_request: the pr to update
1238 :param reviewer_data: list of tuples
1262 :param reviewer_data: list of tuples
1239 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1263 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1240 :param user: current use who triggers this action
1264 :param user: current use who triggers this action
1241 """
1265 """
1242
1266
1243 pull_request = self.__get_pull_request(pull_request)
1267 pull_request = self.__get_pull_request(pull_request)
1244 if pull_request.is_closed():
1268 if pull_request.is_closed():
1245 raise ValueError('This pull request is closed')
1269 raise ValueError('This pull request is closed')
1246
1270
1247 reviewers = {}
1271 reviewers = {}
1248 for user_id, reasons, mandatory, role, rules in reviewer_data:
1272 for user_id, reasons, mandatory, role, rules in reviewer_data:
1249 if isinstance(user_id, (int, compat.string_types)):
1273 if isinstance(user_id, (int, compat.string_types)):
1250 user_id = self._get_user(user_id).user_id
1274 user_id = self._get_user(user_id).user_id
1251 reviewers[user_id] = {
1275 reviewers[user_id] = {
1252 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1276 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1253
1277
1254 reviewers_ids = set(reviewers.keys())
1278 reviewers_ids = set(reviewers.keys())
1255 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1279 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1256 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1280 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1257
1281
1258 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1282 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1259
1283
1260 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1284 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1261 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1285 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1262
1286
1263 log.debug("Adding %s reviewers", ids_to_add)
1287 log.debug("Adding %s reviewers", ids_to_add)
1264 log.debug("Removing %s reviewers", ids_to_remove)
1288 log.debug("Removing %s reviewers", ids_to_remove)
1265 changed = False
1289 changed = False
1266 added_audit_reviewers = []
1290 added_audit_reviewers = []
1267 removed_audit_reviewers = []
1291 removed_audit_reviewers = []
1268
1292
1269 for uid in ids_to_add:
1293 for uid in ids_to_add:
1270 changed = True
1294 changed = True
1271 _usr = self._get_user(uid)
1295 _usr = self._get_user(uid)
1272 reviewer = PullRequestReviewers()
1296 reviewer = PullRequestReviewers()
1273 reviewer.user = _usr
1297 reviewer.user = _usr
1274 reviewer.pull_request = pull_request
1298 reviewer.pull_request = pull_request
1275 reviewer.reasons = reviewers[uid]['reasons']
1299 reviewer.reasons = reviewers[uid]['reasons']
1276 # NOTE(marcink): mandatory shouldn't be changed now
1300 # NOTE(marcink): mandatory shouldn't be changed now
1277 # reviewer.mandatory = reviewers[uid]['reasons']
1301 # reviewer.mandatory = reviewers[uid]['reasons']
1278 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1302 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1279 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1303 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1280 Session().add(reviewer)
1304 Session().add(reviewer)
1281 added_audit_reviewers.append(reviewer.get_dict())
1305 added_audit_reviewers.append(reviewer.get_dict())
1282
1306
1283 for uid in ids_to_remove:
1307 for uid in ids_to_remove:
1284 changed = True
1308 changed = True
1285 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1309 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1286 # This is an edge case that handles previous state of having the same reviewer twice.
1310 # This is an edge case that handles previous state of having the same reviewer twice.
1287 # this CAN happen due to the lack of DB checks
1311 # this CAN happen due to the lack of DB checks
1288 reviewers = PullRequestReviewers.query()\
1312 reviewers = PullRequestReviewers.query()\
1289 .filter(PullRequestReviewers.user_id == uid,
1313 .filter(PullRequestReviewers.user_id == uid,
1290 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1314 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1291 PullRequestReviewers.pull_request == pull_request)\
1315 PullRequestReviewers.pull_request == pull_request)\
1292 .all()
1316 .all()
1293
1317
1294 for obj in reviewers:
1318 for obj in reviewers:
1295 added_audit_reviewers.append(obj.get_dict())
1319 added_audit_reviewers.append(obj.get_dict())
1296 Session().delete(obj)
1320 Session().delete(obj)
1297
1321
1298 if changed:
1322 if changed:
1299 Session().expire_all()
1323 Session().expire_all()
1300 pull_request.updated_on = datetime.datetime.now()
1324 pull_request.updated_on = datetime.datetime.now()
1301 Session().add(pull_request)
1325 Session().add(pull_request)
1302
1326
1303 # finally store audit logs
1327 # finally store audit logs
1304 for user_data in added_audit_reviewers:
1328 for user_data in added_audit_reviewers:
1305 self._log_audit_action(
1329 self._log_audit_action(
1306 'repo.pull_request.reviewer.add', {'data': user_data},
1330 'repo.pull_request.reviewer.add', {'data': user_data},
1307 user, pull_request)
1331 user, pull_request)
1308 for user_data in removed_audit_reviewers:
1332 for user_data in removed_audit_reviewers:
1309 self._log_audit_action(
1333 self._log_audit_action(
1310 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1334 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1311 user, pull_request)
1335 user, pull_request)
1312
1336
1313 self.notify_reviewers(pull_request, ids_to_add, user.get_instance())
1337 self.notify_reviewers(pull_request, ids_to_add, user.get_instance())
1314 return ids_to_add, ids_to_remove
1338 return ids_to_add, ids_to_remove
1315
1339
1316 def update_observers(self, pull_request, observer_data, user):
1340 def update_observers(self, pull_request, observer_data, user):
1317 """
1341 """
1318 Update the observers in the pull request
1342 Update the observers in the pull request
1319
1343
1320 :param pull_request: the pr to update
1344 :param pull_request: the pr to update
1321 :param observer_data: list of tuples
1345 :param observer_data: list of tuples
1322 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1346 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1323 :param user: current use who triggers this action
1347 :param user: current use who triggers this action
1324 """
1348 """
1325 pull_request = self.__get_pull_request(pull_request)
1349 pull_request = self.__get_pull_request(pull_request)
1326 if pull_request.is_closed():
1350 if pull_request.is_closed():
1327 raise ValueError('This pull request is closed')
1351 raise ValueError('This pull request is closed')
1328
1352
1329 observers = {}
1353 observers = {}
1330 for user_id, reasons, mandatory, role, rules in observer_data:
1354 for user_id, reasons, mandatory, role, rules in observer_data:
1331 if isinstance(user_id, (int, compat.string_types)):
1355 if isinstance(user_id, (int, compat.string_types)):
1332 user_id = self._get_user(user_id).user_id
1356 user_id = self._get_user(user_id).user_id
1333 observers[user_id] = {
1357 observers[user_id] = {
1334 'reasons': reasons, 'observers': mandatory, 'role': role}
1358 'reasons': reasons, 'observers': mandatory, 'role': role}
1335
1359
1336 observers_ids = set(observers.keys())
1360 observers_ids = set(observers.keys())
1337 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1361 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1338 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1362 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1339
1363
1340 current_observers_ids = set([x.user.user_id for x in current_observers])
1364 current_observers_ids = set([x.user.user_id for x in current_observers])
1341
1365
1342 ids_to_add = observers_ids.difference(current_observers_ids)
1366 ids_to_add = observers_ids.difference(current_observers_ids)
1343 ids_to_remove = current_observers_ids.difference(observers_ids)
1367 ids_to_remove = current_observers_ids.difference(observers_ids)
1344
1368
1345 log.debug("Adding %s observer", ids_to_add)
1369 log.debug("Adding %s observer", ids_to_add)
1346 log.debug("Removing %s observer", ids_to_remove)
1370 log.debug("Removing %s observer", ids_to_remove)
1347 changed = False
1371 changed = False
1348 added_audit_observers = []
1372 added_audit_observers = []
1349 removed_audit_observers = []
1373 removed_audit_observers = []
1350
1374
1351 for uid in ids_to_add:
1375 for uid in ids_to_add:
1352 changed = True
1376 changed = True
1353 _usr = self._get_user(uid)
1377 _usr = self._get_user(uid)
1354 observer = PullRequestReviewers()
1378 observer = PullRequestReviewers()
1355 observer.user = _usr
1379 observer.user = _usr
1356 observer.pull_request = pull_request
1380 observer.pull_request = pull_request
1357 observer.reasons = observers[uid]['reasons']
1381 observer.reasons = observers[uid]['reasons']
1358 # NOTE(marcink): mandatory shouldn't be changed now
1382 # NOTE(marcink): mandatory shouldn't be changed now
1359 # observer.mandatory = observer[uid]['reasons']
1383 # observer.mandatory = observer[uid]['reasons']
1360
1384
1361 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1385 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1362 observer.role = PullRequestReviewers.ROLE_OBSERVER
1386 observer.role = PullRequestReviewers.ROLE_OBSERVER
1363 Session().add(observer)
1387 Session().add(observer)
1364 added_audit_observers.append(observer.get_dict())
1388 added_audit_observers.append(observer.get_dict())
1365
1389
1366 for uid in ids_to_remove:
1390 for uid in ids_to_remove:
1367 changed = True
1391 changed = True
1368 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1392 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1369 # This is an edge case that handles previous state of having the same reviewer twice.
1393 # This is an edge case that handles previous state of having the same reviewer twice.
1370 # this CAN happen due to the lack of DB checks
1394 # this CAN happen due to the lack of DB checks
1371 observers = PullRequestReviewers.query()\
1395 observers = PullRequestReviewers.query()\
1372 .filter(PullRequestReviewers.user_id == uid,
1396 .filter(PullRequestReviewers.user_id == uid,
1373 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1397 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1374 PullRequestReviewers.pull_request == pull_request)\
1398 PullRequestReviewers.pull_request == pull_request)\
1375 .all()
1399 .all()
1376
1400
1377 for obj in observers:
1401 for obj in observers:
1378 added_audit_observers.append(obj.get_dict())
1402 added_audit_observers.append(obj.get_dict())
1379 Session().delete(obj)
1403 Session().delete(obj)
1380
1404
1381 if changed:
1405 if changed:
1382 Session().expire_all()
1406 Session().expire_all()
1383 pull_request.updated_on = datetime.datetime.now()
1407 pull_request.updated_on = datetime.datetime.now()
1384 Session().add(pull_request)
1408 Session().add(pull_request)
1385
1409
1386 # finally store audit logs
1410 # finally store audit logs
1387 for user_data in added_audit_observers:
1411 for user_data in added_audit_observers:
1388 self._log_audit_action(
1412 self._log_audit_action(
1389 'repo.pull_request.observer.add', {'data': user_data},
1413 'repo.pull_request.observer.add', {'data': user_data},
1390 user, pull_request)
1414 user, pull_request)
1391 for user_data in removed_audit_observers:
1415 for user_data in removed_audit_observers:
1392 self._log_audit_action(
1416 self._log_audit_action(
1393 'repo.pull_request.observer.delete', {'old_data': user_data},
1417 'repo.pull_request.observer.delete', {'old_data': user_data},
1394 user, pull_request)
1418 user, pull_request)
1395
1419
1396 self.notify_observers(pull_request, ids_to_add, user.get_instance())
1420 self.notify_observers(pull_request, ids_to_add, user.get_instance())
1397 return ids_to_add, ids_to_remove
1421 return ids_to_add, ids_to_remove
1398
1422
1399 def get_url(self, pull_request, request=None, permalink=False):
1423 def get_url(self, pull_request, request=None, permalink=False):
1400 if not request:
1424 if not request:
1401 request = get_current_request()
1425 request = get_current_request()
1402
1426
1403 if permalink:
1427 if permalink:
1404 return request.route_url(
1428 return request.route_url(
1405 'pull_requests_global',
1429 'pull_requests_global',
1406 pull_request_id=pull_request.pull_request_id,)
1430 pull_request_id=pull_request.pull_request_id,)
1407 else:
1431 else:
1408 return request.route_url('pullrequest_show',
1432 return request.route_url('pullrequest_show',
1409 repo_name=safe_str(pull_request.target_repo.repo_name),
1433 repo_name=safe_str(pull_request.target_repo.repo_name),
1410 pull_request_id=pull_request.pull_request_id,)
1434 pull_request_id=pull_request.pull_request_id,)
1411
1435
1412 def get_shadow_clone_url(self, pull_request, request=None):
1436 def get_shadow_clone_url(self, pull_request, request=None):
1413 """
1437 """
1414 Returns qualified url pointing to the shadow repository. If this pull
1438 Returns qualified url pointing to the shadow repository. If this pull
1415 request is closed there is no shadow repository and ``None`` will be
1439 request is closed there is no shadow repository and ``None`` will be
1416 returned.
1440 returned.
1417 """
1441 """
1418 if pull_request.is_closed():
1442 if pull_request.is_closed():
1419 return None
1443 return None
1420 else:
1444 else:
1421 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1445 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1422 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1446 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1423
1447
1424 def _notify_reviewers(self, pull_request, user_ids, role, user):
1448 def _notify_reviewers(self, pull_request, user_ids, role, user):
1425 # notification to reviewers/observers
1449 # notification to reviewers/observers
1426 if not user_ids:
1450 if not user_ids:
1427 return
1451 return
1428
1452
1429 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1453 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1430
1454
1431 pull_request_obj = pull_request
1455 pull_request_obj = pull_request
1432 # get the current participants of this pull request
1456 # get the current participants of this pull request
1433 recipients = user_ids
1457 recipients = user_ids
1434 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1458 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1435
1459
1436 pr_source_repo = pull_request_obj.source_repo
1460 pr_source_repo = pull_request_obj.source_repo
1437 pr_target_repo = pull_request_obj.target_repo
1461 pr_target_repo = pull_request_obj.target_repo
1438
1462
1439 pr_url = h.route_url('pullrequest_show',
1463 pr_url = h.route_url('pullrequest_show',
1440 repo_name=pr_target_repo.repo_name,
1464 repo_name=pr_target_repo.repo_name,
1441 pull_request_id=pull_request_obj.pull_request_id,)
1465 pull_request_id=pull_request_obj.pull_request_id,)
1442
1466
1443 # set some variables for email notification
1467 # set some variables for email notification
1444 pr_target_repo_url = h.route_url(
1468 pr_target_repo_url = h.route_url(
1445 'repo_summary', repo_name=pr_target_repo.repo_name)
1469 'repo_summary', repo_name=pr_target_repo.repo_name)
1446
1470
1447 pr_source_repo_url = h.route_url(
1471 pr_source_repo_url = h.route_url(
1448 'repo_summary', repo_name=pr_source_repo.repo_name)
1472 'repo_summary', repo_name=pr_source_repo.repo_name)
1449
1473
1450 # pull request specifics
1474 # pull request specifics
1451 pull_request_commits = [
1475 pull_request_commits = [
1452 (x.raw_id, x.message)
1476 (x.raw_id, x.message)
1453 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1477 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1454
1478
1455 current_rhodecode_user = user
1479 current_rhodecode_user = user
1456 kwargs = {
1480 kwargs = {
1457 'user': current_rhodecode_user,
1481 'user': current_rhodecode_user,
1458 'pull_request_author': pull_request.author,
1482 'pull_request_author': pull_request.author,
1459 'pull_request': pull_request_obj,
1483 'pull_request': pull_request_obj,
1460 'pull_request_commits': pull_request_commits,
1484 'pull_request_commits': pull_request_commits,
1461
1485
1462 'pull_request_target_repo': pr_target_repo,
1486 'pull_request_target_repo': pr_target_repo,
1463 'pull_request_target_repo_url': pr_target_repo_url,
1487 'pull_request_target_repo_url': pr_target_repo_url,
1464
1488
1465 'pull_request_source_repo': pr_source_repo,
1489 'pull_request_source_repo': pr_source_repo,
1466 'pull_request_source_repo_url': pr_source_repo_url,
1490 'pull_request_source_repo_url': pr_source_repo_url,
1467
1491
1468 'pull_request_url': pr_url,
1492 'pull_request_url': pr_url,
1469 'thread_ids': [pr_url],
1493 'thread_ids': [pr_url],
1470 'user_role': role
1494 'user_role': role
1471 }
1495 }
1472
1496
1473 # pre-generate the subject for notification itself
1497 # pre-generate the subject for notification itself
1474 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1498 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1475 notification_type, **kwargs)
1499 notification_type, **kwargs)
1476
1500
1477 # create notification objects, and emails
1501 # create notification objects, and emails
1478 NotificationModel().create(
1502 NotificationModel().create(
1479 created_by=current_rhodecode_user,
1503 created_by=current_rhodecode_user,
1480 notification_subject=subject,
1504 notification_subject=subject,
1481 notification_body=body_plaintext,
1505 notification_body=body_plaintext,
1482 notification_type=notification_type,
1506 notification_type=notification_type,
1483 recipients=recipients,
1507 recipients=recipients,
1484 email_kwargs=kwargs,
1508 email_kwargs=kwargs,
1485 )
1509 )
1486
1510
1487 def notify_reviewers(self, pull_request, reviewers_ids, user):
1511 def notify_reviewers(self, pull_request, reviewers_ids, user):
1488 return self._notify_reviewers(pull_request, reviewers_ids,
1512 return self._notify_reviewers(pull_request, reviewers_ids,
1489 PullRequestReviewers.ROLE_REVIEWER, user)
1513 PullRequestReviewers.ROLE_REVIEWER, user)
1490
1514
1491 def notify_observers(self, pull_request, observers_ids, user):
1515 def notify_observers(self, pull_request, observers_ids, user):
1492 return self._notify_reviewers(pull_request, observers_ids,
1516 return self._notify_reviewers(pull_request, observers_ids,
1493 PullRequestReviewers.ROLE_OBSERVER, user)
1517 PullRequestReviewers.ROLE_OBSERVER, user)
1494
1518
1495 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1519 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1496 commit_changes, file_changes):
1520 commit_changes, file_changes):
1497
1521
1498 updating_user_id = updating_user.user_id
1522 updating_user_id = updating_user.user_id
1499 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1523 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1500 # NOTE(marcink): send notification to all other users except to
1524 # NOTE(marcink): send notification to all other users except to
1501 # person who updated the PR
1525 # person who updated the PR
1502 recipients = reviewers.difference(set([updating_user_id]))
1526 recipients = reviewers.difference(set([updating_user_id]))
1503
1527
1504 log.debug('Notify following recipients about pull-request update %s', recipients)
1528 log.debug('Notify following recipients about pull-request update %s', recipients)
1505
1529
1506 pull_request_obj = pull_request
1530 pull_request_obj = pull_request
1507
1531
1508 # send email about the update
1532 # send email about the update
1509 changed_files = (
1533 changed_files = (
1510 file_changes.added + file_changes.modified + file_changes.removed)
1534 file_changes.added + file_changes.modified + file_changes.removed)
1511
1535
1512 pr_source_repo = pull_request_obj.source_repo
1536 pr_source_repo = pull_request_obj.source_repo
1513 pr_target_repo = pull_request_obj.target_repo
1537 pr_target_repo = pull_request_obj.target_repo
1514
1538
1515 pr_url = h.route_url('pullrequest_show',
1539 pr_url = h.route_url('pullrequest_show',
1516 repo_name=pr_target_repo.repo_name,
1540 repo_name=pr_target_repo.repo_name,
1517 pull_request_id=pull_request_obj.pull_request_id,)
1541 pull_request_id=pull_request_obj.pull_request_id,)
1518
1542
1519 # set some variables for email notification
1543 # set some variables for email notification
1520 pr_target_repo_url = h.route_url(
1544 pr_target_repo_url = h.route_url(
1521 'repo_summary', repo_name=pr_target_repo.repo_name)
1545 'repo_summary', repo_name=pr_target_repo.repo_name)
1522
1546
1523 pr_source_repo_url = h.route_url(
1547 pr_source_repo_url = h.route_url(
1524 'repo_summary', repo_name=pr_source_repo.repo_name)
1548 'repo_summary', repo_name=pr_source_repo.repo_name)
1525
1549
1526 email_kwargs = {
1550 email_kwargs = {
1527 'date': datetime.datetime.now(),
1551 'date': datetime.datetime.now(),
1528 'updating_user': updating_user,
1552 'updating_user': updating_user,
1529
1553
1530 'pull_request': pull_request_obj,
1554 'pull_request': pull_request_obj,
1531
1555
1532 'pull_request_target_repo': pr_target_repo,
1556 'pull_request_target_repo': pr_target_repo,
1533 'pull_request_target_repo_url': pr_target_repo_url,
1557 'pull_request_target_repo_url': pr_target_repo_url,
1534
1558
1535 'pull_request_source_repo': pr_source_repo,
1559 'pull_request_source_repo': pr_source_repo,
1536 'pull_request_source_repo_url': pr_source_repo_url,
1560 'pull_request_source_repo_url': pr_source_repo_url,
1537
1561
1538 'pull_request_url': pr_url,
1562 'pull_request_url': pr_url,
1539
1563
1540 'ancestor_commit_id': ancestor_commit_id,
1564 'ancestor_commit_id': ancestor_commit_id,
1541 'added_commits': commit_changes.added,
1565 'added_commits': commit_changes.added,
1542 'removed_commits': commit_changes.removed,
1566 'removed_commits': commit_changes.removed,
1543 'changed_files': changed_files,
1567 'changed_files': changed_files,
1544 'added_files': file_changes.added,
1568 'added_files': file_changes.added,
1545 'modified_files': file_changes.modified,
1569 'modified_files': file_changes.modified,
1546 'removed_files': file_changes.removed,
1570 'removed_files': file_changes.removed,
1547 'thread_ids': [pr_url],
1571 'thread_ids': [pr_url],
1548 }
1572 }
1549
1573
1550 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1574 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1551 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1575 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1552
1576
1553 # create notification objects, and emails
1577 # create notification objects, and emails
1554 NotificationModel().create(
1578 NotificationModel().create(
1555 created_by=updating_user,
1579 created_by=updating_user,
1556 notification_subject=subject,
1580 notification_subject=subject,
1557 notification_body=body_plaintext,
1581 notification_body=body_plaintext,
1558 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1582 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1559 recipients=recipients,
1583 recipients=recipients,
1560 email_kwargs=email_kwargs,
1584 email_kwargs=email_kwargs,
1561 )
1585 )
1562
1586
1563 def delete(self, pull_request, user=None):
1587 def delete(self, pull_request, user=None):
1564 if not user:
1588 if not user:
1565 user = getattr(get_current_rhodecode_user(), 'username', None)
1589 user = getattr(get_current_rhodecode_user(), 'username', None)
1566
1590
1567 pull_request = self.__get_pull_request(pull_request)
1591 pull_request = self.__get_pull_request(pull_request)
1568 old_data = pull_request.get_api_data(with_merge_state=False)
1592 old_data = pull_request.get_api_data(with_merge_state=False)
1569 self._cleanup_merge_workspace(pull_request)
1593 self._cleanup_merge_workspace(pull_request)
1570 self._log_audit_action(
1594 self._log_audit_action(
1571 'repo.pull_request.delete', {'old_data': old_data},
1595 'repo.pull_request.delete', {'old_data': old_data},
1572 user, pull_request)
1596 user, pull_request)
1573 Session().delete(pull_request)
1597 Session().delete(pull_request)
1574
1598
1575 def close_pull_request(self, pull_request, user):
1599 def close_pull_request(self, pull_request, user):
1576 pull_request = self.__get_pull_request(pull_request)
1600 pull_request = self.__get_pull_request(pull_request)
1577 self._cleanup_merge_workspace(pull_request)
1601 self._cleanup_merge_workspace(pull_request)
1578 pull_request.status = PullRequest.STATUS_CLOSED
1602 pull_request.status = PullRequest.STATUS_CLOSED
1579 pull_request.updated_on = datetime.datetime.now()
1603 pull_request.updated_on = datetime.datetime.now()
1580 Session().add(pull_request)
1604 Session().add(pull_request)
1581 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1605 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1582
1606
1583 pr_data = pull_request.get_api_data(with_merge_state=False)
1607 pr_data = pull_request.get_api_data(with_merge_state=False)
1584 self._log_audit_action(
1608 self._log_audit_action(
1585 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1609 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1586
1610
1587 def close_pull_request_with_comment(
1611 def close_pull_request_with_comment(
1588 self, pull_request, user, repo, message=None, auth_user=None):
1612 self, pull_request, user, repo, message=None, auth_user=None):
1589
1613
1590 pull_request_review_status = pull_request.calculated_review_status()
1614 pull_request_review_status = pull_request.calculated_review_status()
1591
1615
1592 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1616 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1593 # approved only if we have voting consent
1617 # approved only if we have voting consent
1594 status = ChangesetStatus.STATUS_APPROVED
1618 status = ChangesetStatus.STATUS_APPROVED
1595 else:
1619 else:
1596 status = ChangesetStatus.STATUS_REJECTED
1620 status = ChangesetStatus.STATUS_REJECTED
1597 status_lbl = ChangesetStatus.get_status_lbl(status)
1621 status_lbl = ChangesetStatus.get_status_lbl(status)
1598
1622
1599 default_message = (
1623 default_message = (
1600 'Closing with status change {transition_icon} {status}.'
1624 'Closing with status change {transition_icon} {status}.'
1601 ).format(transition_icon='>', status=status_lbl)
1625 ).format(transition_icon='>', status=status_lbl)
1602 text = message or default_message
1626 text = message or default_message
1603
1627
1604 # create a comment, and link it to new status
1628 # create a comment, and link it to new status
1605 comment = CommentsModel().create(
1629 comment = CommentsModel().create(
1606 text=text,
1630 text=text,
1607 repo=repo.repo_id,
1631 repo=repo.repo_id,
1608 user=user.user_id,
1632 user=user.user_id,
1609 pull_request=pull_request.pull_request_id,
1633 pull_request=pull_request.pull_request_id,
1610 status_change=status_lbl,
1634 status_change=status_lbl,
1611 status_change_type=status,
1635 status_change_type=status,
1612 closing_pr=True,
1636 closing_pr=True,
1613 auth_user=auth_user,
1637 auth_user=auth_user,
1614 )
1638 )
1615
1639
1616 # calculate old status before we change it
1640 # calculate old status before we change it
1617 old_calculated_status = pull_request.calculated_review_status()
1641 old_calculated_status = pull_request.calculated_review_status()
1618 ChangesetStatusModel().set_status(
1642 ChangesetStatusModel().set_status(
1619 repo.repo_id,
1643 repo.repo_id,
1620 status,
1644 status,
1621 user.user_id,
1645 user.user_id,
1622 comment=comment,
1646 comment=comment,
1623 pull_request=pull_request.pull_request_id
1647 pull_request=pull_request.pull_request_id
1624 )
1648 )
1625
1649
1626 Session().flush()
1650 Session().flush()
1627
1651
1628 self.trigger_pull_request_hook(pull_request, user, 'comment',
1652 self.trigger_pull_request_hook(pull_request, user, 'comment',
1629 data={'comment': comment})
1653 data={'comment': comment})
1630
1654
1631 # we now calculate the status of pull request again, and based on that
1655 # we now calculate the status of pull request again, and based on that
1632 # calculation trigger status change. This might happen in cases
1656 # calculation trigger status change. This might happen in cases
1633 # that non-reviewer admin closes a pr, which means his vote doesn't
1657 # that non-reviewer admin closes a pr, which means his vote doesn't
1634 # change the status, while if he's a reviewer this might change it.
1658 # change the status, while if he's a reviewer this might change it.
1635 calculated_status = pull_request.calculated_review_status()
1659 calculated_status = pull_request.calculated_review_status()
1636 if old_calculated_status != calculated_status:
1660 if old_calculated_status != calculated_status:
1637 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1661 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1638 data={'status': calculated_status})
1662 data={'status': calculated_status})
1639
1663
1640 # finally close the PR
1664 # finally close the PR
1641 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1665 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1642
1666
1643 return comment, status
1667 return comment, status
1644
1668
1645 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1669 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1646 _ = translator or get_current_request().translate
1670 _ = translator or get_current_request().translate
1647
1671
1648 if not self._is_merge_enabled(pull_request):
1672 if not self._is_merge_enabled(pull_request):
1649 return None, False, _('Server-side pull request merging is disabled.')
1673 return None, False, _('Server-side pull request merging is disabled.')
1650
1674
1651 if pull_request.is_closed():
1675 if pull_request.is_closed():
1652 return None, False, _('This pull request is closed.')
1676 return None, False, _('This pull request is closed.')
1653
1677
1654 merge_possible, msg = self._check_repo_requirements(
1678 merge_possible, msg = self._check_repo_requirements(
1655 target=pull_request.target_repo, source=pull_request.source_repo,
1679 target=pull_request.target_repo, source=pull_request.source_repo,
1656 translator=_)
1680 translator=_)
1657 if not merge_possible:
1681 if not merge_possible:
1658 return None, merge_possible, msg
1682 return None, merge_possible, msg
1659
1683
1660 try:
1684 try:
1661 merge_response = self._try_merge(
1685 merge_response = self._try_merge(
1662 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1686 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1663 log.debug("Merge response: %s", merge_response)
1687 log.debug("Merge response: %s", merge_response)
1664 return merge_response, merge_response.possible, merge_response.merge_status_message
1688 return merge_response, merge_response.possible, merge_response.merge_status_message
1665 except NotImplementedError:
1689 except NotImplementedError:
1666 return None, False, _('Pull request merging is not supported.')
1690 return None, False, _('Pull request merging is not supported.')
1667
1691
1668 def _check_repo_requirements(self, target, source, translator):
1692 def _check_repo_requirements(self, target, source, translator):
1669 """
1693 """
1670 Check if `target` and `source` have compatible requirements.
1694 Check if `target` and `source` have compatible requirements.
1671
1695
1672 Currently this is just checking for largefiles.
1696 Currently this is just checking for largefiles.
1673 """
1697 """
1674 _ = translator
1698 _ = translator
1675 target_has_largefiles = self._has_largefiles(target)
1699 target_has_largefiles = self._has_largefiles(target)
1676 source_has_largefiles = self._has_largefiles(source)
1700 source_has_largefiles = self._has_largefiles(source)
1677 merge_possible = True
1701 merge_possible = True
1678 message = u''
1702 message = u''
1679
1703
1680 if target_has_largefiles != source_has_largefiles:
1704 if target_has_largefiles != source_has_largefiles:
1681 merge_possible = False
1705 merge_possible = False
1682 if source_has_largefiles:
1706 if source_has_largefiles:
1683 message = _(
1707 message = _(
1684 'Target repository large files support is disabled.')
1708 'Target repository large files support is disabled.')
1685 else:
1709 else:
1686 message = _(
1710 message = _(
1687 'Source repository large files support is disabled.')
1711 'Source repository large files support is disabled.')
1688
1712
1689 return merge_possible, message
1713 return merge_possible, message
1690
1714
1691 def _has_largefiles(self, repo):
1715 def _has_largefiles(self, repo):
1692 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1716 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1693 'extensions', 'largefiles')
1717 'extensions', 'largefiles')
1694 return largefiles_ui and largefiles_ui[0].active
1718 return largefiles_ui and largefiles_ui[0].active
1695
1719
1696 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1720 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1697 """
1721 """
1698 Try to merge the pull request and return the merge status.
1722 Try to merge the pull request and return the merge status.
1699 """
1723 """
1700 log.debug(
1724 log.debug(
1701 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1725 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1702 pull_request.pull_request_id, force_shadow_repo_refresh)
1726 pull_request.pull_request_id, force_shadow_repo_refresh)
1703 target_vcs = pull_request.target_repo.scm_instance()
1727 target_vcs = pull_request.target_repo.scm_instance()
1704 # Refresh the target reference.
1728 # Refresh the target reference.
1705 try:
1729 try:
1706 target_ref = self._refresh_reference(
1730 target_ref = self._refresh_reference(
1707 pull_request.target_ref_parts, target_vcs)
1731 pull_request.target_ref_parts, target_vcs)
1708 except CommitDoesNotExistError:
1732 except CommitDoesNotExistError:
1709 merge_state = MergeResponse(
1733 merge_state = MergeResponse(
1710 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1734 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1711 metadata={'target_ref': pull_request.target_ref_parts})
1735 metadata={'target_ref': pull_request.target_ref_parts})
1712 return merge_state
1736 return merge_state
1713
1737
1714 target_locked = pull_request.target_repo.locked
1738 target_locked = pull_request.target_repo.locked
1715 if target_locked and target_locked[0]:
1739 if target_locked and target_locked[0]:
1716 locked_by = 'user:{}'.format(target_locked[0])
1740 locked_by = 'user:{}'.format(target_locked[0])
1717 log.debug("The target repository is locked by %s.", locked_by)
1741 log.debug("The target repository is locked by %s.", locked_by)
1718 merge_state = MergeResponse(
1742 merge_state = MergeResponse(
1719 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1743 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1720 metadata={'locked_by': locked_by})
1744 metadata={'locked_by': locked_by})
1721 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1745 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1722 pull_request, target_ref):
1746 pull_request, target_ref):
1723 log.debug("Refreshing the merge status of the repository.")
1747 log.debug("Refreshing the merge status of the repository.")
1724 merge_state = self._refresh_merge_state(
1748 merge_state = self._refresh_merge_state(
1725 pull_request, target_vcs, target_ref)
1749 pull_request, target_vcs, target_ref)
1726 else:
1750 else:
1727 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1751 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1728 metadata = {
1752 metadata = {
1729 'unresolved_files': '',
1753 'unresolved_files': '',
1730 'target_ref': pull_request.target_ref_parts,
1754 'target_ref': pull_request.target_ref_parts,
1731 'source_ref': pull_request.source_ref_parts,
1755 'source_ref': pull_request.source_ref_parts,
1732 }
1756 }
1733 if pull_request.last_merge_metadata:
1757 if pull_request.last_merge_metadata:
1734 metadata.update(pull_request.last_merge_metadata_parsed)
1758 metadata.update(pull_request.last_merge_metadata_parsed)
1735
1759
1736 if not possible and target_ref.type == 'branch':
1760 if not possible and target_ref.type == 'branch':
1737 # NOTE(marcink): case for mercurial multiple heads on branch
1761 # NOTE(marcink): case for mercurial multiple heads on branch
1738 heads = target_vcs._heads(target_ref.name)
1762 heads = target_vcs._heads(target_ref.name)
1739 if len(heads) != 1:
1763 if len(heads) != 1:
1740 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1764 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1741 metadata.update({
1765 metadata.update({
1742 'heads': heads
1766 'heads': heads
1743 })
1767 })
1744
1768
1745 merge_state = MergeResponse(
1769 merge_state = MergeResponse(
1746 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1770 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1747
1771
1748 return merge_state
1772 return merge_state
1749
1773
1750 def _refresh_reference(self, reference, vcs_repository):
1774 def _refresh_reference(self, reference, vcs_repository):
1751 if reference.type in self.UPDATABLE_REF_TYPES:
1775 if reference.type in self.UPDATABLE_REF_TYPES:
1752 name_or_id = reference.name
1776 name_or_id = reference.name
1753 else:
1777 else:
1754 name_or_id = reference.commit_id
1778 name_or_id = reference.commit_id
1755
1779
1756 refreshed_commit = vcs_repository.get_commit(name_or_id)
1780 refreshed_commit = vcs_repository.get_commit(name_or_id)
1757 refreshed_reference = Reference(
1781 refreshed_reference = Reference(
1758 reference.type, reference.name, refreshed_commit.raw_id)
1782 reference.type, reference.name, refreshed_commit.raw_id)
1759 return refreshed_reference
1783 return refreshed_reference
1760
1784
1761 def _needs_merge_state_refresh(self, pull_request, target_reference):
1785 def _needs_merge_state_refresh(self, pull_request, target_reference):
1762 return not(
1786 return not(
1763 pull_request.revisions and
1787 pull_request.revisions and
1764 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1788 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1765 target_reference.commit_id == pull_request._last_merge_target_rev)
1789 target_reference.commit_id == pull_request._last_merge_target_rev)
1766
1790
1767 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1791 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1768 workspace_id = self._workspace_id(pull_request)
1792 workspace_id = self._workspace_id(pull_request)
1769 source_vcs = pull_request.source_repo.scm_instance()
1793 source_vcs = pull_request.source_repo.scm_instance()
1770 repo_id = pull_request.target_repo.repo_id
1794 repo_id = pull_request.target_repo.repo_id
1771 use_rebase = self._use_rebase_for_merging(pull_request)
1795 use_rebase = self._use_rebase_for_merging(pull_request)
1772 close_branch = self._close_branch_before_merging(pull_request)
1796 close_branch = self._close_branch_before_merging(pull_request)
1773 merge_state = target_vcs.merge(
1797 merge_state = target_vcs.merge(
1774 repo_id, workspace_id,
1798 repo_id, workspace_id,
1775 target_reference, source_vcs, pull_request.source_ref_parts,
1799 target_reference, source_vcs, pull_request.source_ref_parts,
1776 dry_run=True, use_rebase=use_rebase,
1800 dry_run=True, use_rebase=use_rebase,
1777 close_branch=close_branch)
1801 close_branch=close_branch)
1778
1802
1779 # Do not store the response if there was an unknown error.
1803 # Do not store the response if there was an unknown error.
1780 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1804 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1781 pull_request._last_merge_source_rev = \
1805 pull_request._last_merge_source_rev = \
1782 pull_request.source_ref_parts.commit_id
1806 pull_request.source_ref_parts.commit_id
1783 pull_request._last_merge_target_rev = target_reference.commit_id
1807 pull_request._last_merge_target_rev = target_reference.commit_id
1784 pull_request.last_merge_status = merge_state.failure_reason
1808 pull_request.last_merge_status = merge_state.failure_reason
1785 pull_request.last_merge_metadata = merge_state.metadata
1809 pull_request.last_merge_metadata = merge_state.metadata
1786
1810
1787 pull_request.shadow_merge_ref = merge_state.merge_ref
1811 pull_request.shadow_merge_ref = merge_state.merge_ref
1788 Session().add(pull_request)
1812 Session().add(pull_request)
1789 Session().commit()
1813 Session().commit()
1790
1814
1791 return merge_state
1815 return merge_state
1792
1816
1793 def _workspace_id(self, pull_request):
1817 def _workspace_id(self, pull_request):
1794 workspace_id = 'pr-%s' % pull_request.pull_request_id
1818 workspace_id = 'pr-%s' % pull_request.pull_request_id
1795 return workspace_id
1819 return workspace_id
1796
1820
1797 def generate_repo_data(self, repo, commit_id=None, branch=None,
1821 def generate_repo_data(self, repo, commit_id=None, branch=None,
1798 bookmark=None, translator=None):
1822 bookmark=None, translator=None):
1799 from rhodecode.model.repo import RepoModel
1823 from rhodecode.model.repo import RepoModel
1800
1824
1801 all_refs, selected_ref = \
1825 all_refs, selected_ref = \
1802 self._get_repo_pullrequest_sources(
1826 self._get_repo_pullrequest_sources(
1803 repo.scm_instance(), commit_id=commit_id,
1827 repo.scm_instance(), commit_id=commit_id,
1804 branch=branch, bookmark=bookmark, translator=translator)
1828 branch=branch, bookmark=bookmark, translator=translator)
1805
1829
1806 refs_select2 = []
1830 refs_select2 = []
1807 for element in all_refs:
1831 for element in all_refs:
1808 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1832 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1809 refs_select2.append({'text': element[1], 'children': children})
1833 refs_select2.append({'text': element[1], 'children': children})
1810
1834
1811 return {
1835 return {
1812 'user': {
1836 'user': {
1813 'user_id': repo.user.user_id,
1837 'user_id': repo.user.user_id,
1814 'username': repo.user.username,
1838 'username': repo.user.username,
1815 'firstname': repo.user.first_name,
1839 'firstname': repo.user.first_name,
1816 'lastname': repo.user.last_name,
1840 'lastname': repo.user.last_name,
1817 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1841 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1818 },
1842 },
1819 'name': repo.repo_name,
1843 'name': repo.repo_name,
1820 'link': RepoModel().get_url(repo),
1844 'link': RepoModel().get_url(repo),
1821 'description': h.chop_at_smart(repo.description_safe, '\n'),
1845 'description': h.chop_at_smart(repo.description_safe, '\n'),
1822 'refs': {
1846 'refs': {
1823 'all_refs': all_refs,
1847 'all_refs': all_refs,
1824 'selected_ref': selected_ref,
1848 'selected_ref': selected_ref,
1825 'select2_refs': refs_select2
1849 'select2_refs': refs_select2
1826 }
1850 }
1827 }
1851 }
1828
1852
1829 def generate_pullrequest_title(self, source, source_ref, target):
1853 def generate_pullrequest_title(self, source, source_ref, target):
1830 return u'{source}#{at_ref} to {target}'.format(
1854 return u'{source}#{at_ref} to {target}'.format(
1831 source=source,
1855 source=source,
1832 at_ref=source_ref,
1856 at_ref=source_ref,
1833 target=target,
1857 target=target,
1834 )
1858 )
1835
1859
1836 def _cleanup_merge_workspace(self, pull_request):
1860 def _cleanup_merge_workspace(self, pull_request):
1837 # Merging related cleanup
1861 # Merging related cleanup
1838 repo_id = pull_request.target_repo.repo_id
1862 repo_id = pull_request.target_repo.repo_id
1839 target_scm = pull_request.target_repo.scm_instance()
1863 target_scm = pull_request.target_repo.scm_instance()
1840 workspace_id = self._workspace_id(pull_request)
1864 workspace_id = self._workspace_id(pull_request)
1841
1865
1842 try:
1866 try:
1843 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1867 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1844 except NotImplementedError:
1868 except NotImplementedError:
1845 pass
1869 pass
1846
1870
1847 def _get_repo_pullrequest_sources(
1871 def _get_repo_pullrequest_sources(
1848 self, repo, commit_id=None, branch=None, bookmark=None,
1872 self, repo, commit_id=None, branch=None, bookmark=None,
1849 translator=None):
1873 translator=None):
1850 """
1874 """
1851 Return a structure with repo's interesting commits, suitable for
1875 Return a structure with repo's interesting commits, suitable for
1852 the selectors in pullrequest controller
1876 the selectors in pullrequest controller
1853
1877
1854 :param commit_id: a commit that must be in the list somehow
1878 :param commit_id: a commit that must be in the list somehow
1855 and selected by default
1879 and selected by default
1856 :param branch: a branch that must be in the list and selected
1880 :param branch: a branch that must be in the list and selected
1857 by default - even if closed
1881 by default - even if closed
1858 :param bookmark: a bookmark that must be in the list and selected
1882 :param bookmark: a bookmark that must be in the list and selected
1859 """
1883 """
1860 _ = translator or get_current_request().translate
1884 _ = translator or get_current_request().translate
1861
1885
1862 commit_id = safe_str(commit_id) if commit_id else None
1886 commit_id = safe_str(commit_id) if commit_id else None
1863 branch = safe_unicode(branch) if branch else None
1887 branch = safe_unicode(branch) if branch else None
1864 bookmark = safe_unicode(bookmark) if bookmark else None
1888 bookmark = safe_unicode(bookmark) if bookmark else None
1865
1889
1866 selected = None
1890 selected = None
1867
1891
1868 # order matters: first source that has commit_id in it will be selected
1892 # order matters: first source that has commit_id in it will be selected
1869 sources = []
1893 sources = []
1870 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1894 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1871 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1895 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1872
1896
1873 if commit_id:
1897 if commit_id:
1874 ref_commit = (h.short_id(commit_id), commit_id)
1898 ref_commit = (h.short_id(commit_id), commit_id)
1875 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1899 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1876
1900
1877 sources.append(
1901 sources.append(
1878 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1902 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1879 )
1903 )
1880
1904
1881 groups = []
1905 groups = []
1882
1906
1883 for group_key, ref_list, group_name, match in sources:
1907 for group_key, ref_list, group_name, match in sources:
1884 group_refs = []
1908 group_refs = []
1885 for ref_name, ref_id in ref_list:
1909 for ref_name, ref_id in ref_list:
1886 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1910 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1887 group_refs.append((ref_key, ref_name))
1911 group_refs.append((ref_key, ref_name))
1888
1912
1889 if not selected:
1913 if not selected:
1890 if set([commit_id, match]) & set([ref_id, ref_name]):
1914 if set([commit_id, match]) & set([ref_id, ref_name]):
1891 selected = ref_key
1915 selected = ref_key
1892
1916
1893 if group_refs:
1917 if group_refs:
1894 groups.append((group_refs, group_name))
1918 groups.append((group_refs, group_name))
1895
1919
1896 if not selected:
1920 if not selected:
1897 ref = commit_id or branch or bookmark
1921 ref = commit_id or branch or bookmark
1898 if ref:
1922 if ref:
1899 raise CommitDoesNotExistError(
1923 raise CommitDoesNotExistError(
1900 u'No commit refs could be found matching: {}'.format(ref))
1924 u'No commit refs could be found matching: {}'.format(ref))
1901 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1925 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1902 selected = u'branch:{}:{}'.format(
1926 selected = u'branch:{}:{}'.format(
1903 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1927 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1904 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1928 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1905 )
1929 )
1906 elif repo.commit_ids:
1930 elif repo.commit_ids:
1907 # make the user select in this case
1931 # make the user select in this case
1908 selected = None
1932 selected = None
1909 else:
1933 else:
1910 raise EmptyRepositoryError()
1934 raise EmptyRepositoryError()
1911 return groups, selected
1935 return groups, selected
1912
1936
1913 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1937 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1914 hide_whitespace_changes, diff_context):
1938 hide_whitespace_changes, diff_context):
1915
1939
1916 return self._get_diff_from_pr_or_version(
1940 return self._get_diff_from_pr_or_version(
1917 source_repo, source_ref_id, target_ref_id,
1941 source_repo, source_ref_id, target_ref_id,
1918 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1942 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1919
1943
1920 def _get_diff_from_pr_or_version(
1944 def _get_diff_from_pr_or_version(
1921 self, source_repo, source_ref_id, target_ref_id,
1945 self, source_repo, source_ref_id, target_ref_id,
1922 hide_whitespace_changes, diff_context):
1946 hide_whitespace_changes, diff_context):
1923
1947
1924 target_commit = source_repo.get_commit(
1948 target_commit = source_repo.get_commit(
1925 commit_id=safe_str(target_ref_id))
1949 commit_id=safe_str(target_ref_id))
1926 source_commit = source_repo.get_commit(
1950 source_commit = source_repo.get_commit(
1927 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1951 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1928 if isinstance(source_repo, Repository):
1952 if isinstance(source_repo, Repository):
1929 vcs_repo = source_repo.scm_instance()
1953 vcs_repo = source_repo.scm_instance()
1930 else:
1954 else:
1931 vcs_repo = source_repo
1955 vcs_repo = source_repo
1932
1956
1933 # TODO: johbo: In the context of an update, we cannot reach
1957 # TODO: johbo: In the context of an update, we cannot reach
1934 # the old commit anymore with our normal mechanisms. It needs
1958 # the old commit anymore with our normal mechanisms. It needs
1935 # some sort of special support in the vcs layer to avoid this
1959 # some sort of special support in the vcs layer to avoid this
1936 # workaround.
1960 # workaround.
1937 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1961 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1938 vcs_repo.alias == 'git'):
1962 vcs_repo.alias == 'git'):
1939 source_commit.raw_id = safe_str(source_ref_id)
1963 source_commit.raw_id = safe_str(source_ref_id)
1940
1964
1941 log.debug('calculating diff between '
1965 log.debug('calculating diff between '
1942 'source_ref:%s and target_ref:%s for repo `%s`',
1966 'source_ref:%s and target_ref:%s for repo `%s`',
1943 target_ref_id, source_ref_id,
1967 target_ref_id, source_ref_id,
1944 safe_unicode(vcs_repo.path))
1968 safe_unicode(vcs_repo.path))
1945
1969
1946 vcs_diff = vcs_repo.get_diff(
1970 vcs_diff = vcs_repo.get_diff(
1947 commit1=target_commit, commit2=source_commit,
1971 commit1=target_commit, commit2=source_commit,
1948 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1972 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1949 return vcs_diff
1973 return vcs_diff
1950
1974
1951 def _is_merge_enabled(self, pull_request):
1975 def _is_merge_enabled(self, pull_request):
1952 return self._get_general_setting(
1976 return self._get_general_setting(
1953 pull_request, 'rhodecode_pr_merge_enabled')
1977 pull_request, 'rhodecode_pr_merge_enabled')
1954
1978
1955 def _use_rebase_for_merging(self, pull_request):
1979 def _use_rebase_for_merging(self, pull_request):
1956 repo_type = pull_request.target_repo.repo_type
1980 repo_type = pull_request.target_repo.repo_type
1957 if repo_type == 'hg':
1981 if repo_type == 'hg':
1958 return self._get_general_setting(
1982 return self._get_general_setting(
1959 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1983 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1960 elif repo_type == 'git':
1984 elif repo_type == 'git':
1961 return self._get_general_setting(
1985 return self._get_general_setting(
1962 pull_request, 'rhodecode_git_use_rebase_for_merging')
1986 pull_request, 'rhodecode_git_use_rebase_for_merging')
1963
1987
1964 return False
1988 return False
1965
1989
1966 def _user_name_for_merging(self, pull_request, user):
1990 def _user_name_for_merging(self, pull_request, user):
1967 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1991 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1968 if env_user_name_attr and hasattr(user, env_user_name_attr):
1992 if env_user_name_attr and hasattr(user, env_user_name_attr):
1969 user_name_attr = env_user_name_attr
1993 user_name_attr = env_user_name_attr
1970 else:
1994 else:
1971 user_name_attr = 'short_contact'
1995 user_name_attr = 'short_contact'
1972
1996
1973 user_name = getattr(user, user_name_attr)
1997 user_name = getattr(user, user_name_attr)
1974 return user_name
1998 return user_name
1975
1999
1976 def _close_branch_before_merging(self, pull_request):
2000 def _close_branch_before_merging(self, pull_request):
1977 repo_type = pull_request.target_repo.repo_type
2001 repo_type = pull_request.target_repo.repo_type
1978 if repo_type == 'hg':
2002 if repo_type == 'hg':
1979 return self._get_general_setting(
2003 return self._get_general_setting(
1980 pull_request, 'rhodecode_hg_close_branch_before_merging')
2004 pull_request, 'rhodecode_hg_close_branch_before_merging')
1981 elif repo_type == 'git':
2005 elif repo_type == 'git':
1982 return self._get_general_setting(
2006 return self._get_general_setting(
1983 pull_request, 'rhodecode_git_close_branch_before_merging')
2007 pull_request, 'rhodecode_git_close_branch_before_merging')
1984
2008
1985 return False
2009 return False
1986
2010
1987 def _get_general_setting(self, pull_request, settings_key, default=False):
2011 def _get_general_setting(self, pull_request, settings_key, default=False):
1988 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2012 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1989 settings = settings_model.get_general_settings()
2013 settings = settings_model.get_general_settings()
1990 return settings.get(settings_key, default)
2014 return settings.get(settings_key, default)
1991
2015
1992 def _log_audit_action(self, action, action_data, user, pull_request):
2016 def _log_audit_action(self, action, action_data, user, pull_request):
1993 audit_logger.store(
2017 audit_logger.store(
1994 action=action,
2018 action=action,
1995 action_data=action_data,
2019 action_data=action_data,
1996 user=user,
2020 user=user,
1997 repo=pull_request.target_repo)
2021 repo=pull_request.target_repo)
1998
2022
1999 def get_reviewer_functions(self):
2023 def get_reviewer_functions(self):
2000 """
2024 """
2001 Fetches functions for validation and fetching default reviewers.
2025 Fetches functions for validation and fetching default reviewers.
2002 If available we use the EE package, else we fallback to CE
2026 If available we use the EE package, else we fallback to CE
2003 package functions
2027 package functions
2004 """
2028 """
2005 try:
2029 try:
2006 from rc_reviewers.utils import get_default_reviewers_data
2030 from rc_reviewers.utils import get_default_reviewers_data
2007 from rc_reviewers.utils import validate_default_reviewers
2031 from rc_reviewers.utils import validate_default_reviewers
2008 from rc_reviewers.utils import validate_observers
2032 from rc_reviewers.utils import validate_observers
2009 except ImportError:
2033 except ImportError:
2010 from rhodecode.apps.repository.utils import get_default_reviewers_data
2034 from rhodecode.apps.repository.utils import get_default_reviewers_data
2011 from rhodecode.apps.repository.utils import validate_default_reviewers
2035 from rhodecode.apps.repository.utils import validate_default_reviewers
2012 from rhodecode.apps.repository.utils import validate_observers
2036 from rhodecode.apps.repository.utils import validate_observers
2013
2037
2014 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2038 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2015
2039
2016
2040
2017 class MergeCheck(object):
2041 class MergeCheck(object):
2018 """
2042 """
2019 Perform Merge Checks and returns a check object which stores information
2043 Perform Merge Checks and returns a check object which stores information
2020 about merge errors, and merge conditions
2044 about merge errors, and merge conditions
2021 """
2045 """
2022 TODO_CHECK = 'todo'
2046 TODO_CHECK = 'todo'
2023 PERM_CHECK = 'perm'
2047 PERM_CHECK = 'perm'
2024 REVIEW_CHECK = 'review'
2048 REVIEW_CHECK = 'review'
2025 MERGE_CHECK = 'merge'
2049 MERGE_CHECK = 'merge'
2026 WIP_CHECK = 'wip'
2050 WIP_CHECK = 'wip'
2027
2051
2028 def __init__(self):
2052 def __init__(self):
2029 self.review_status = None
2053 self.review_status = None
2030 self.merge_possible = None
2054 self.merge_possible = None
2031 self.merge_msg = ''
2055 self.merge_msg = ''
2032 self.merge_response = None
2056 self.merge_response = None
2033 self.failed = None
2057 self.failed = None
2034 self.errors = []
2058 self.errors = []
2035 self.error_details = OrderedDict()
2059 self.error_details = OrderedDict()
2036 self.source_commit = AttributeDict()
2060 self.source_commit = AttributeDict()
2037 self.target_commit = AttributeDict()
2061 self.target_commit = AttributeDict()
2038
2062
2039 def __repr__(self):
2063 def __repr__(self):
2040 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2064 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2041 self.merge_possible, self.failed, self.errors)
2065 self.merge_possible, self.failed, self.errors)
2042
2066
2043 def push_error(self, error_type, message, error_key, details):
2067 def push_error(self, error_type, message, error_key, details):
2044 self.failed = True
2068 self.failed = True
2045 self.errors.append([error_type, message])
2069 self.errors.append([error_type, message])
2046 self.error_details[error_key] = dict(
2070 self.error_details[error_key] = dict(
2047 details=details,
2071 details=details,
2048 error_type=error_type,
2072 error_type=error_type,
2049 message=message
2073 message=message
2050 )
2074 )
2051
2075
2052 @classmethod
2076 @classmethod
2053 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2077 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2054 force_shadow_repo_refresh=False):
2078 force_shadow_repo_refresh=False):
2055 _ = translator
2079 _ = translator
2056 merge_check = cls()
2080 merge_check = cls()
2057
2081
2058 # title has WIP:
2082 # title has WIP:
2059 if pull_request.work_in_progress:
2083 if pull_request.work_in_progress:
2060 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2084 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2061
2085
2062 msg = _('WIP marker in title prevents from accidental merge.')
2086 msg = _('WIP marker in title prevents from accidental merge.')
2063 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2087 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2064 if fail_early:
2088 if fail_early:
2065 return merge_check
2089 return merge_check
2066
2090
2067 # permissions to merge
2091 # permissions to merge
2068 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2092 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2069 if not user_allowed_to_merge:
2093 if not user_allowed_to_merge:
2070 log.debug("MergeCheck: cannot merge, approval is pending.")
2094 log.debug("MergeCheck: cannot merge, approval is pending.")
2071
2095
2072 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2096 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2073 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2097 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2074 if fail_early:
2098 if fail_early:
2075 return merge_check
2099 return merge_check
2076
2100
2077 # permission to merge into the target branch
2101 # permission to merge into the target branch
2078 target_commit_id = pull_request.target_ref_parts.commit_id
2102 target_commit_id = pull_request.target_ref_parts.commit_id
2079 if pull_request.target_ref_parts.type == 'branch':
2103 if pull_request.target_ref_parts.type == 'branch':
2080 branch_name = pull_request.target_ref_parts.name
2104 branch_name = pull_request.target_ref_parts.name
2081 else:
2105 else:
2082 # for mercurial we can always figure out the branch from the commit
2106 # for mercurial we can always figure out the branch from the commit
2083 # in case of bookmark
2107 # in case of bookmark
2084 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2108 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2085 branch_name = target_commit.branch
2109 branch_name = target_commit.branch
2086
2110
2087 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2111 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2088 pull_request.target_repo.repo_name, branch_name)
2112 pull_request.target_repo.repo_name, branch_name)
2089 if branch_perm and branch_perm == 'branch.none':
2113 if branch_perm and branch_perm == 'branch.none':
2090 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2114 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2091 branch_name, rule)
2115 branch_name, rule)
2092 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2116 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2093 if fail_early:
2117 if fail_early:
2094 return merge_check
2118 return merge_check
2095
2119
2096 # review status, must be always present
2120 # review status, must be always present
2097 review_status = pull_request.calculated_review_status()
2121 review_status = pull_request.calculated_review_status()
2098 merge_check.review_status = review_status
2122 merge_check.review_status = review_status
2099
2123
2100 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2124 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2101 if not status_approved:
2125 if not status_approved:
2102 log.debug("MergeCheck: cannot merge, approval is pending.")
2126 log.debug("MergeCheck: cannot merge, approval is pending.")
2103
2127
2104 msg = _('Pull request reviewer approval is pending.')
2128 msg = _('Pull request reviewer approval is pending.')
2105
2129
2106 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2130 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2107
2131
2108 if fail_early:
2132 if fail_early:
2109 return merge_check
2133 return merge_check
2110
2134
2111 # left over TODOs
2135 # left over TODOs
2112 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2136 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2113 if todos:
2137 if todos:
2114 log.debug("MergeCheck: cannot merge, {} "
2138 log.debug("MergeCheck: cannot merge, {} "
2115 "unresolved TODOs left.".format(len(todos)))
2139 "unresolved TODOs left.".format(len(todos)))
2116
2140
2117 if len(todos) == 1:
2141 if len(todos) == 1:
2118 msg = _('Cannot merge, {} TODO still not resolved.').format(
2142 msg = _('Cannot merge, {} TODO still not resolved.').format(
2119 len(todos))
2143 len(todos))
2120 else:
2144 else:
2121 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2145 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2122 len(todos))
2146 len(todos))
2123
2147
2124 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2148 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2125
2149
2126 if fail_early:
2150 if fail_early:
2127 return merge_check
2151 return merge_check
2128
2152
2129 # merge possible, here is the filesystem simulation + shadow repo
2153 # merge possible, here is the filesystem simulation + shadow repo
2130 merge_response, merge_status, msg = PullRequestModel().merge_status(
2154 merge_response, merge_status, msg = PullRequestModel().merge_status(
2131 pull_request, translator=translator,
2155 pull_request, translator=translator,
2132 force_shadow_repo_refresh=force_shadow_repo_refresh)
2156 force_shadow_repo_refresh=force_shadow_repo_refresh)
2133
2157
2134 merge_check.merge_possible = merge_status
2158 merge_check.merge_possible = merge_status
2135 merge_check.merge_msg = msg
2159 merge_check.merge_msg = msg
2136 merge_check.merge_response = merge_response
2160 merge_check.merge_response = merge_response
2137
2161
2138 source_ref_id = pull_request.source_ref_parts.commit_id
2162 source_ref_id = pull_request.source_ref_parts.commit_id
2139 target_ref_id = pull_request.target_ref_parts.commit_id
2163 target_ref_id = pull_request.target_ref_parts.commit_id
2140
2164
2141 try:
2165 try:
2142 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2166 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2143 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2167 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2144 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2168 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2145 merge_check.source_commit.current_raw_id = source_commit.raw_id
2169 merge_check.source_commit.current_raw_id = source_commit.raw_id
2146 merge_check.source_commit.previous_raw_id = source_ref_id
2170 merge_check.source_commit.previous_raw_id = source_ref_id
2147
2171
2148 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2172 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2149 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2173 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2150 merge_check.target_commit.current_raw_id = target_commit.raw_id
2174 merge_check.target_commit.current_raw_id = target_commit.raw_id
2151 merge_check.target_commit.previous_raw_id = target_ref_id
2175 merge_check.target_commit.previous_raw_id = target_ref_id
2152 except (SourceRefMissing, TargetRefMissing):
2176 except (SourceRefMissing, TargetRefMissing):
2153 pass
2177 pass
2154
2178
2155 if not merge_status:
2179 if not merge_status:
2156 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2180 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2157 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2181 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2158
2182
2159 if fail_early:
2183 if fail_early:
2160 return merge_check
2184 return merge_check
2161
2185
2162 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2186 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2163 return merge_check
2187 return merge_check
2164
2188
2165 @classmethod
2189 @classmethod
2166 def get_merge_conditions(cls, pull_request, translator):
2190 def get_merge_conditions(cls, pull_request, translator):
2167 _ = translator
2191 _ = translator
2168 merge_details = {}
2192 merge_details = {}
2169
2193
2170 model = PullRequestModel()
2194 model = PullRequestModel()
2171 use_rebase = model._use_rebase_for_merging(pull_request)
2195 use_rebase = model._use_rebase_for_merging(pull_request)
2172
2196
2173 if use_rebase:
2197 if use_rebase:
2174 merge_details['merge_strategy'] = dict(
2198 merge_details['merge_strategy'] = dict(
2175 details={},
2199 details={},
2176 message=_('Merge strategy: rebase')
2200 message=_('Merge strategy: rebase')
2177 )
2201 )
2178 else:
2202 else:
2179 merge_details['merge_strategy'] = dict(
2203 merge_details['merge_strategy'] = dict(
2180 details={},
2204 details={},
2181 message=_('Merge strategy: explicit merge commit')
2205 message=_('Merge strategy: explicit merge commit')
2182 )
2206 )
2183
2207
2184 close_branch = model._close_branch_before_merging(pull_request)
2208 close_branch = model._close_branch_before_merging(pull_request)
2185 if close_branch:
2209 if close_branch:
2186 repo_type = pull_request.target_repo.repo_type
2210 repo_type = pull_request.target_repo.repo_type
2187 close_msg = ''
2211 close_msg = ''
2188 if repo_type == 'hg':
2212 if repo_type == 'hg':
2189 close_msg = _('Source branch will be closed before the merge.')
2213 close_msg = _('Source branch will be closed before the merge.')
2190 elif repo_type == 'git':
2214 elif repo_type == 'git':
2191 close_msg = _('Source branch will be deleted after the merge.')
2215 close_msg = _('Source branch will be deleted after the merge.')
2192
2216
2193 merge_details['close_branch'] = dict(
2217 merge_details['close_branch'] = dict(
2194 details={},
2218 details={},
2195 message=close_msg
2219 message=close_msg
2196 )
2220 )
2197
2221
2198 return merge_details
2222 return merge_details
2199
2223
2200
2224
2201 ChangeTuple = collections.namedtuple(
2225 ChangeTuple = collections.namedtuple(
2202 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2226 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2203
2227
2204 FileChangeTuple = collections.namedtuple(
2228 FileChangeTuple = collections.namedtuple(
2205 'FileChangeTuple', ['added', 'modified', 'removed'])
2229 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now