##// END OF EJS Templates
pull-request-events: add audit data for pull_request.close action
marcink -
r2082:db993005 default
parent child Browse files
Show More
@@ -1,1609 +1,1611 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
78
78
79 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
84 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
89 ' failed.'),
89 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
92 ' head.'),
92 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
95 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
98 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
101 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
104 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
110 'reference is missing.'),
110 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
113 'to sub repositories.'),
114 }
114 }
115
115
116 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
118 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
128 'reference is missing.'),
128 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
131 'reference is missing.'),
131 'reference is missing.'),
132 }
132 }
133
133
134 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
135 return self._get_instance((
135 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
137
137
138 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
139 if not api:
140 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
142 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
145
145
146 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
149
149
150 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
153
153
154 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
157
157
158 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
162
162
163 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
165 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
167
167
168 def get(self, pull_request):
168 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
170
170
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
173 order_dir='desc'):
173 order_dir='desc'):
174 repo = None
174 repo = None
175 if repo_name:
175 if repo_name:
176 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
177
177
178 q = PullRequest.query()
178 q = PullRequest.query()
179
179
180 # source or target
180 # source or target
181 if repo and source:
181 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
183 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
185
185
186 # closed,opened
186 # closed,opened
187 if statuses:
187 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
189
189
190 # opened by filter
190 # opened by filter
191 if opened_by:
191 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
193
193
194 if order_by:
194 if order_by:
195 order_map = {
195 order_map = {
196 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
197 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
200 }
200 }
201 if order_dir == 'asc':
201 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
203 else:
203 else:
204 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
205
205
206 return q
206 return q
207
207
208 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
209 opened_by=None):
210 """
210 """
211 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
212
212
213 :param repo_name: target or source repo
213 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
217 :returns: int number of pull requests
218 """
218 """
219 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
221
222 return q.count()
222 return q.count()
223
223
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
226 """
227 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :param offset: pagination offset
233 :param offset: pagination offset
234 :param length: length of returned list
234 :param length: length of returned list
235 :param order_by: order of the returned list
235 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
237 :returns: list of pull requests
238 """
238 """
239 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242
242
243 if length:
243 if length:
244 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
245 else:
245 else:
246 pull_requests = q.all()
246 pull_requests = q.all()
247
247
248 return pull_requests
248 return pull_requests
249
249
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
251 opened_by=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review.
254 awaiting review.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
260 :returns: int number of pull requests
261 """
261 """
262 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
264
265 return len(pull_requests)
265 return len(pull_requests)
266
266
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
270 """
270 """
271 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
272 review.
272 review.
273
273
274 :param repo_name: target or source repo
274 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
278 :param offset: pagination offset
278 :param offset: pagination offset
279 :param length: length of returned list
279 :param length: length of returned list
280 :param order_by: order of the returned list
280 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
282 :returns: list of pull requests
283 """
283 """
284 pull_requests = self.get_all(
284 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
287
287
288 _filtered_pull_requests = []
288 _filtered_pull_requests = []
289 for pr in pull_requests:
289 for pr in pull_requests:
290 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
294 if length:
294 if length:
295 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
296 else:
296 else:
297 return _filtered_pull_requests
297 return _filtered_pull_requests
298
298
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
301 """
301 """
302 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
303 awaiting review from a specific user.
304
304
305 :param repo_name: target or source repo
305 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
314 user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
330 :param offset: pagination offset
331 :param length: length of returned list
331 :param length: length of returned list
332 :param order_by: order of the returned list
332 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
334 :returns: list of pull requests
335 """
335 """
336 pull_requests = self.get_all(
336 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
339
339
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
341 my_participation = []
342 for pr in pull_requests:
342 for pr in pull_requests:
343 if pr in _my:
343 if pr in _my:
344 my_participation.append(pr)
344 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
346 if length:
346 if length:
347 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
348 else:
348 else:
349 return _filtered_pull_requests
349 return _filtered_pull_requests
350
350
351 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
352 return [
352 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
355 ]
355 ]
356
356
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
359 q = PullRequest.query()
360 if user_id:
360 if user_id:
361 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
364 user_filter= or_(
365 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
367 )
368 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
369
369
370 # closed,opened
370 # closed,opened
371 if statuses:
371 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
373
373
374 if order_by:
374 if order_by:
375 order_map = {
375 order_map = {
376 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
377 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
380 }
380 }
381 if order_dir == 'asc':
381 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
383 else:
383 else:
384 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
385
385
386 return q
386 return q
387
387
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
390 return q.count()
391
391
392 def get_im_participating_in(
392 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
395 """
395 """
396 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
397 """
397 """
398
398
399 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
401 order_dir=order_dir)
402
402
403 if length:
403 if length:
404 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
405 else:
405 else:
406 pull_requests = q.all()
406 pull_requests = q.all()
407
407
408 return pull_requests
408 return pull_requests
409
409
410 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
411 """
411 """
412 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
413 """
413 """
414 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
417 .all()
418
418
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
421 reviewer_data=None):
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 self.notify_reviewers(pull_request, reviewer_ids)
469 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
476 created_by_user, pull_request)
477
477
478 return pull_request
478 return pull_request
479
479
480 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 pull_request = self.__get_pull_request(pull_request)
481 pull_request = self.__get_pull_request(pull_request)
482 target_scm = pull_request.target_repo.scm_instance()
482 target_scm = pull_request.target_repo.scm_instance()
483 if action == 'create':
483 if action == 'create':
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 elif action == 'merge':
485 elif action == 'merge':
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 elif action == 'close':
487 elif action == 'close':
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 elif action == 'review_status_change':
489 elif action == 'review_status_change':
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 elif action == 'update':
491 elif action == 'update':
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 else:
493 else:
494 return
494 return
495
495
496 trigger_hook(
496 trigger_hook(
497 username=user.username,
497 username=user.username,
498 repo_name=pull_request.target_repo.repo_name,
498 repo_name=pull_request.target_repo.repo_name,
499 repo_alias=target_scm.alias,
499 repo_alias=target_scm.alias,
500 pull_request=pull_request)
500 pull_request=pull_request)
501
501
502 def _get_commit_ids(self, pull_request):
502 def _get_commit_ids(self, pull_request):
503 """
503 """
504 Return the commit ids of the merged pull request.
504 Return the commit ids of the merged pull request.
505
505
506 This method is not dealing correctly yet with the lack of autoupdates
506 This method is not dealing correctly yet with the lack of autoupdates
507 nor with the implicit target updates.
507 nor with the implicit target updates.
508 For example: if a commit in the source repo is already in the target it
508 For example: if a commit in the source repo is already in the target it
509 will be reported anyways.
509 will be reported anyways.
510 """
510 """
511 merge_rev = pull_request.merge_rev
511 merge_rev = pull_request.merge_rev
512 if merge_rev is None:
512 if merge_rev is None:
513 raise ValueError('This pull request was not merged yet')
513 raise ValueError('This pull request was not merged yet')
514
514
515 commit_ids = list(pull_request.revisions)
515 commit_ids = list(pull_request.revisions)
516 if merge_rev not in commit_ids:
516 if merge_rev not in commit_ids:
517 commit_ids.append(merge_rev)
517 commit_ids.append(merge_rev)
518
518
519 return commit_ids
519 return commit_ids
520
520
521 def merge(self, pull_request, user, extras):
521 def merge(self, pull_request, user, extras):
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 if merge_state.executed:
524 if merge_state.executed:
525 log.debug(
525 log.debug(
526 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
527 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
528
528
529 self._log_audit_action(
529 self._log_audit_action(
530 'repo.pull_request.merge',
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
532 user, pull_request)
533
533
534 else:
534 else:
535 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
536 return merge_state
537
537
538 def _merge_pull_request(self, pull_request, user, extras):
538 def _merge_pull_request(self, pull_request, user, extras):
539 target_vcs = pull_request.target_repo.scm_instance()
539 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
541 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
542 pull_request.target_ref_parts, target_vcs)
543
543
544 message = _(
544 message = _(
545 'Merge pull request #%(pr_id)s from '
545 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
547 'pr_id': pull_request.pull_request_id,
548 'source_repo': source_vcs.name,
548 'source_repo': source_vcs.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
550 'pr_title': pull_request.title
550 'pr_title': pull_request.title
551 }
551 }
552
552
553 workspace_id = self._workspace_id(pull_request)
553 workspace_id = self._workspace_id(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
555 close_branch = self._close_branch_before_merging(pull_request)
555 close_branch = self._close_branch_before_merging(pull_request)
556
556
557 callback_daemon, extras = prepare_callback_daemon(
557 callback_daemon, extras = prepare_callback_daemon(
558 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
559 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
560
560
561 with callback_daemon:
561 with callback_daemon:
562 # TODO: johbo: Implement a clean way to run a config_override
562 # TODO: johbo: Implement a clean way to run a config_override
563 # for a single call.
563 # for a single call.
564 target_vcs.config.set(
564 target_vcs.config.set(
565 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
566 merge_state = target_vcs.merge(
566 merge_state = target_vcs.merge(
567 target_ref, source_vcs, pull_request.source_ref_parts,
567 target_ref, source_vcs, pull_request.source_ref_parts,
568 workspace_id, user_name=user.username,
568 workspace_id, user_name=user.username,
569 user_email=user.email, message=message, use_rebase=use_rebase,
569 user_email=user.email, message=message, use_rebase=use_rebase,
570 close_branch=close_branch)
570 close_branch=close_branch)
571 return merge_state
571 return merge_state
572
572
573 def _comment_and_close_pr(self, pull_request, user, merge_state):
573 def _comment_and_close_pr(self, pull_request, user, merge_state):
574 pull_request.merge_rev = merge_state.merge_ref.commit_id
574 pull_request.merge_rev = merge_state.merge_ref.commit_id
575 pull_request.updated_on = datetime.datetime.now()
575 pull_request.updated_on = datetime.datetime.now()
576
576
577 CommentsModel().create(
577 CommentsModel().create(
578 text=unicode(_('Pull request merged and closed')),
578 text=unicode(_('Pull request merged and closed')),
579 repo=pull_request.target_repo.repo_id,
579 repo=pull_request.target_repo.repo_id,
580 user=user.user_id,
580 user=user.user_id,
581 pull_request=pull_request.pull_request_id,
581 pull_request=pull_request.pull_request_id,
582 f_path=None,
582 f_path=None,
583 line_no=None,
583 line_no=None,
584 closing_pr=True
584 closing_pr=True
585 )
585 )
586
586
587 Session().add(pull_request)
587 Session().add(pull_request)
588 Session().flush()
588 Session().flush()
589 # TODO: paris: replace invalidation with less radical solution
589 # TODO: paris: replace invalidation with less radical solution
590 ScmModel().mark_for_invalidation(
590 ScmModel().mark_for_invalidation(
591 pull_request.target_repo.repo_name)
591 pull_request.target_repo.repo_name)
592 self._trigger_pull_request_hook(pull_request, user, 'merge')
592 self._trigger_pull_request_hook(pull_request, user, 'merge')
593
593
594 def has_valid_update_type(self, pull_request):
594 def has_valid_update_type(self, pull_request):
595 source_ref_type = pull_request.source_ref_parts.type
595 source_ref_type = pull_request.source_ref_parts.type
596 return source_ref_type in ['book', 'branch', 'tag']
596 return source_ref_type in ['book', 'branch', 'tag']
597
597
598 def update_commits(self, pull_request):
598 def update_commits(self, pull_request):
599 """
599 """
600 Get the updated list of commits for the pull request
600 Get the updated list of commits for the pull request
601 and return the new pull request version and the list
601 and return the new pull request version and the list
602 of commits processed by this update action
602 of commits processed by this update action
603 """
603 """
604 pull_request = self.__get_pull_request(pull_request)
604 pull_request = self.__get_pull_request(pull_request)
605 source_ref_type = pull_request.source_ref_parts.type
605 source_ref_type = pull_request.source_ref_parts.type
606 source_ref_name = pull_request.source_ref_parts.name
606 source_ref_name = pull_request.source_ref_parts.name
607 source_ref_id = pull_request.source_ref_parts.commit_id
607 source_ref_id = pull_request.source_ref_parts.commit_id
608
608
609 target_ref_type = pull_request.target_ref_parts.type
609 target_ref_type = pull_request.target_ref_parts.type
610 target_ref_name = pull_request.target_ref_parts.name
610 target_ref_name = pull_request.target_ref_parts.name
611 target_ref_id = pull_request.target_ref_parts.commit_id
611 target_ref_id = pull_request.target_ref_parts.commit_id
612
612
613 if not self.has_valid_update_type(pull_request):
613 if not self.has_valid_update_type(pull_request):
614 log.debug(
614 log.debug(
615 "Skipping update of pull request %s due to ref type: %s",
615 "Skipping update of pull request %s due to ref type: %s",
616 pull_request, source_ref_type)
616 pull_request, source_ref_type)
617 return UpdateResponse(
617 return UpdateResponse(
618 executed=False,
618 executed=False,
619 reason=UpdateFailureReason.WRONG_REF_TYPE,
619 reason=UpdateFailureReason.WRONG_REF_TYPE,
620 old=pull_request, new=None, changes=None,
620 old=pull_request, new=None, changes=None,
621 source_changed=False, target_changed=False)
621 source_changed=False, target_changed=False)
622
622
623 # source repo
623 # source repo
624 source_repo = pull_request.source_repo.scm_instance()
624 source_repo = pull_request.source_repo.scm_instance()
625 try:
625 try:
626 source_commit = source_repo.get_commit(commit_id=source_ref_name)
626 source_commit = source_repo.get_commit(commit_id=source_ref_name)
627 except CommitDoesNotExistError:
627 except CommitDoesNotExistError:
628 return UpdateResponse(
628 return UpdateResponse(
629 executed=False,
629 executed=False,
630 reason=UpdateFailureReason.MISSING_SOURCE_REF,
630 reason=UpdateFailureReason.MISSING_SOURCE_REF,
631 old=pull_request, new=None, changes=None,
631 old=pull_request, new=None, changes=None,
632 source_changed=False, target_changed=False)
632 source_changed=False, target_changed=False)
633
633
634 source_changed = source_ref_id != source_commit.raw_id
634 source_changed = source_ref_id != source_commit.raw_id
635
635
636 # target repo
636 # target repo
637 target_repo = pull_request.target_repo.scm_instance()
637 target_repo = pull_request.target_repo.scm_instance()
638 try:
638 try:
639 target_commit = target_repo.get_commit(commit_id=target_ref_name)
639 target_commit = target_repo.get_commit(commit_id=target_ref_name)
640 except CommitDoesNotExistError:
640 except CommitDoesNotExistError:
641 return UpdateResponse(
641 return UpdateResponse(
642 executed=False,
642 executed=False,
643 reason=UpdateFailureReason.MISSING_TARGET_REF,
643 reason=UpdateFailureReason.MISSING_TARGET_REF,
644 old=pull_request, new=None, changes=None,
644 old=pull_request, new=None, changes=None,
645 source_changed=False, target_changed=False)
645 source_changed=False, target_changed=False)
646 target_changed = target_ref_id != target_commit.raw_id
646 target_changed = target_ref_id != target_commit.raw_id
647
647
648 if not (source_changed or target_changed):
648 if not (source_changed or target_changed):
649 log.debug("Nothing changed in pull request %s", pull_request)
649 log.debug("Nothing changed in pull request %s", pull_request)
650 return UpdateResponse(
650 return UpdateResponse(
651 executed=False,
651 executed=False,
652 reason=UpdateFailureReason.NO_CHANGE,
652 reason=UpdateFailureReason.NO_CHANGE,
653 old=pull_request, new=None, changes=None,
653 old=pull_request, new=None, changes=None,
654 source_changed=target_changed, target_changed=source_changed)
654 source_changed=target_changed, target_changed=source_changed)
655
655
656 change_in_found = 'target repo' if target_changed else 'source repo'
656 change_in_found = 'target repo' if target_changed else 'source repo'
657 log.debug('Updating pull request because of change in %s detected',
657 log.debug('Updating pull request because of change in %s detected',
658 change_in_found)
658 change_in_found)
659
659
660 # Finally there is a need for an update, in case of source change
660 # Finally there is a need for an update, in case of source change
661 # we create a new version, else just an update
661 # we create a new version, else just an update
662 if source_changed:
662 if source_changed:
663 pull_request_version = self._create_version_from_snapshot(pull_request)
663 pull_request_version = self._create_version_from_snapshot(pull_request)
664 self._link_comments_to_version(pull_request_version)
664 self._link_comments_to_version(pull_request_version)
665 else:
665 else:
666 try:
666 try:
667 ver = pull_request.versions[-1]
667 ver = pull_request.versions[-1]
668 except IndexError:
668 except IndexError:
669 ver = None
669 ver = None
670
670
671 pull_request.pull_request_version_id = \
671 pull_request.pull_request_version_id = \
672 ver.pull_request_version_id if ver else None
672 ver.pull_request_version_id if ver else None
673 pull_request_version = pull_request
673 pull_request_version = pull_request
674
674
675 try:
675 try:
676 if target_ref_type in ('tag', 'branch', 'book'):
676 if target_ref_type in ('tag', 'branch', 'book'):
677 target_commit = target_repo.get_commit(target_ref_name)
677 target_commit = target_repo.get_commit(target_ref_name)
678 else:
678 else:
679 target_commit = target_repo.get_commit(target_ref_id)
679 target_commit = target_repo.get_commit(target_ref_id)
680 except CommitDoesNotExistError:
680 except CommitDoesNotExistError:
681 return UpdateResponse(
681 return UpdateResponse(
682 executed=False,
682 executed=False,
683 reason=UpdateFailureReason.MISSING_TARGET_REF,
683 reason=UpdateFailureReason.MISSING_TARGET_REF,
684 old=pull_request, new=None, changes=None,
684 old=pull_request, new=None, changes=None,
685 source_changed=source_changed, target_changed=target_changed)
685 source_changed=source_changed, target_changed=target_changed)
686
686
687 # re-compute commit ids
687 # re-compute commit ids
688 old_commit_ids = pull_request.revisions
688 old_commit_ids = pull_request.revisions
689 pre_load = ["author", "branch", "date", "message"]
689 pre_load = ["author", "branch", "date", "message"]
690 commit_ranges = target_repo.compare(
690 commit_ranges = target_repo.compare(
691 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
691 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
692 pre_load=pre_load)
692 pre_load=pre_load)
693
693
694 ancestor = target_repo.get_common_ancestor(
694 ancestor = target_repo.get_common_ancestor(
695 target_commit.raw_id, source_commit.raw_id, source_repo)
695 target_commit.raw_id, source_commit.raw_id, source_repo)
696
696
697 pull_request.source_ref = '%s:%s:%s' % (
697 pull_request.source_ref = '%s:%s:%s' % (
698 source_ref_type, source_ref_name, source_commit.raw_id)
698 source_ref_type, source_ref_name, source_commit.raw_id)
699 pull_request.target_ref = '%s:%s:%s' % (
699 pull_request.target_ref = '%s:%s:%s' % (
700 target_ref_type, target_ref_name, ancestor)
700 target_ref_type, target_ref_name, ancestor)
701
701
702 pull_request.revisions = [
702 pull_request.revisions = [
703 commit.raw_id for commit in reversed(commit_ranges)]
703 commit.raw_id for commit in reversed(commit_ranges)]
704 pull_request.updated_on = datetime.datetime.now()
704 pull_request.updated_on = datetime.datetime.now()
705 Session().add(pull_request)
705 Session().add(pull_request)
706 new_commit_ids = pull_request.revisions
706 new_commit_ids = pull_request.revisions
707
707
708 old_diff_data, new_diff_data = self._generate_update_diffs(
708 old_diff_data, new_diff_data = self._generate_update_diffs(
709 pull_request, pull_request_version)
709 pull_request, pull_request_version)
710
710
711 # calculate commit and file changes
711 # calculate commit and file changes
712 changes = self._calculate_commit_id_changes(
712 changes = self._calculate_commit_id_changes(
713 old_commit_ids, new_commit_ids)
713 old_commit_ids, new_commit_ids)
714 file_changes = self._calculate_file_changes(
714 file_changes = self._calculate_file_changes(
715 old_diff_data, new_diff_data)
715 old_diff_data, new_diff_data)
716
716
717 # set comments as outdated if DIFFS changed
717 # set comments as outdated if DIFFS changed
718 CommentsModel().outdate_comments(
718 CommentsModel().outdate_comments(
719 pull_request, old_diff_data=old_diff_data,
719 pull_request, old_diff_data=old_diff_data,
720 new_diff_data=new_diff_data)
720 new_diff_data=new_diff_data)
721
721
722 commit_changes = (changes.added or changes.removed)
722 commit_changes = (changes.added or changes.removed)
723 file_node_changes = (
723 file_node_changes = (
724 file_changes.added or file_changes.modified or file_changes.removed)
724 file_changes.added or file_changes.modified or file_changes.removed)
725 pr_has_changes = commit_changes or file_node_changes
725 pr_has_changes = commit_changes or file_node_changes
726
726
727 # Add an automatic comment to the pull request, in case
727 # Add an automatic comment to the pull request, in case
728 # anything has changed
728 # anything has changed
729 if pr_has_changes:
729 if pr_has_changes:
730 update_comment = CommentsModel().create(
730 update_comment = CommentsModel().create(
731 text=self._render_update_message(changes, file_changes),
731 text=self._render_update_message(changes, file_changes),
732 repo=pull_request.target_repo,
732 repo=pull_request.target_repo,
733 user=pull_request.author,
733 user=pull_request.author,
734 pull_request=pull_request,
734 pull_request=pull_request,
735 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
735 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
736
736
737 # Update status to "Under Review" for added commits
737 # Update status to "Under Review" for added commits
738 for commit_id in changes.added:
738 for commit_id in changes.added:
739 ChangesetStatusModel().set_status(
739 ChangesetStatusModel().set_status(
740 repo=pull_request.source_repo,
740 repo=pull_request.source_repo,
741 status=ChangesetStatus.STATUS_UNDER_REVIEW,
741 status=ChangesetStatus.STATUS_UNDER_REVIEW,
742 comment=update_comment,
742 comment=update_comment,
743 user=pull_request.author,
743 user=pull_request.author,
744 pull_request=pull_request,
744 pull_request=pull_request,
745 revision=commit_id)
745 revision=commit_id)
746
746
747 log.debug(
747 log.debug(
748 'Updated pull request %s, added_ids: %s, common_ids: %s, '
748 'Updated pull request %s, added_ids: %s, common_ids: %s, '
749 'removed_ids: %s', pull_request.pull_request_id,
749 'removed_ids: %s', pull_request.pull_request_id,
750 changes.added, changes.common, changes.removed)
750 changes.added, changes.common, changes.removed)
751 log.debug(
751 log.debug(
752 'Updated pull request with the following file changes: %s',
752 'Updated pull request with the following file changes: %s',
753 file_changes)
753 file_changes)
754
754
755 log.info(
755 log.info(
756 "Updated pull request %s from commit %s to commit %s, "
756 "Updated pull request %s from commit %s to commit %s, "
757 "stored new version %s of this pull request.",
757 "stored new version %s of this pull request.",
758 pull_request.pull_request_id, source_ref_id,
758 pull_request.pull_request_id, source_ref_id,
759 pull_request.source_ref_parts.commit_id,
759 pull_request.source_ref_parts.commit_id,
760 pull_request_version.pull_request_version_id)
760 pull_request_version.pull_request_version_id)
761 Session().commit()
761 Session().commit()
762 self._trigger_pull_request_hook(
762 self._trigger_pull_request_hook(
763 pull_request, pull_request.author, 'update')
763 pull_request, pull_request.author, 'update')
764
764
765 return UpdateResponse(
765 return UpdateResponse(
766 executed=True, reason=UpdateFailureReason.NONE,
766 executed=True, reason=UpdateFailureReason.NONE,
767 old=pull_request, new=pull_request_version, changes=changes,
767 old=pull_request, new=pull_request_version, changes=changes,
768 source_changed=source_changed, target_changed=target_changed)
768 source_changed=source_changed, target_changed=target_changed)
769
769
770 def _create_version_from_snapshot(self, pull_request):
770 def _create_version_from_snapshot(self, pull_request):
771 version = PullRequestVersion()
771 version = PullRequestVersion()
772 version.title = pull_request.title
772 version.title = pull_request.title
773 version.description = pull_request.description
773 version.description = pull_request.description
774 version.status = pull_request.status
774 version.status = pull_request.status
775 version.created_on = datetime.datetime.now()
775 version.created_on = datetime.datetime.now()
776 version.updated_on = pull_request.updated_on
776 version.updated_on = pull_request.updated_on
777 version.user_id = pull_request.user_id
777 version.user_id = pull_request.user_id
778 version.source_repo = pull_request.source_repo
778 version.source_repo = pull_request.source_repo
779 version.source_ref = pull_request.source_ref
779 version.source_ref = pull_request.source_ref
780 version.target_repo = pull_request.target_repo
780 version.target_repo = pull_request.target_repo
781 version.target_ref = pull_request.target_ref
781 version.target_ref = pull_request.target_ref
782
782
783 version._last_merge_source_rev = pull_request._last_merge_source_rev
783 version._last_merge_source_rev = pull_request._last_merge_source_rev
784 version._last_merge_target_rev = pull_request._last_merge_target_rev
784 version._last_merge_target_rev = pull_request._last_merge_target_rev
785 version.last_merge_status = pull_request.last_merge_status
785 version.last_merge_status = pull_request.last_merge_status
786 version.shadow_merge_ref = pull_request.shadow_merge_ref
786 version.shadow_merge_ref = pull_request.shadow_merge_ref
787 version.merge_rev = pull_request.merge_rev
787 version.merge_rev = pull_request.merge_rev
788 version.reviewer_data = pull_request.reviewer_data
788 version.reviewer_data = pull_request.reviewer_data
789
789
790 version.revisions = pull_request.revisions
790 version.revisions = pull_request.revisions
791 version.pull_request = pull_request
791 version.pull_request = pull_request
792 Session().add(version)
792 Session().add(version)
793 Session().flush()
793 Session().flush()
794
794
795 return version
795 return version
796
796
797 def _generate_update_diffs(self, pull_request, pull_request_version):
797 def _generate_update_diffs(self, pull_request, pull_request_version):
798
798
799 diff_context = (
799 diff_context = (
800 self.DIFF_CONTEXT +
800 self.DIFF_CONTEXT +
801 CommentsModel.needed_extra_diff_context())
801 CommentsModel.needed_extra_diff_context())
802
802
803 source_repo = pull_request_version.source_repo
803 source_repo = pull_request_version.source_repo
804 source_ref_id = pull_request_version.source_ref_parts.commit_id
804 source_ref_id = pull_request_version.source_ref_parts.commit_id
805 target_ref_id = pull_request_version.target_ref_parts.commit_id
805 target_ref_id = pull_request_version.target_ref_parts.commit_id
806 old_diff = self._get_diff_from_pr_or_version(
806 old_diff = self._get_diff_from_pr_or_version(
807 source_repo, source_ref_id, target_ref_id, context=diff_context)
807 source_repo, source_ref_id, target_ref_id, context=diff_context)
808
808
809 source_repo = pull_request.source_repo
809 source_repo = pull_request.source_repo
810 source_ref_id = pull_request.source_ref_parts.commit_id
810 source_ref_id = pull_request.source_ref_parts.commit_id
811 target_ref_id = pull_request.target_ref_parts.commit_id
811 target_ref_id = pull_request.target_ref_parts.commit_id
812
812
813 new_diff = self._get_diff_from_pr_or_version(
813 new_diff = self._get_diff_from_pr_or_version(
814 source_repo, source_ref_id, target_ref_id, context=diff_context)
814 source_repo, source_ref_id, target_ref_id, context=diff_context)
815
815
816 old_diff_data = diffs.DiffProcessor(old_diff)
816 old_diff_data = diffs.DiffProcessor(old_diff)
817 old_diff_data.prepare()
817 old_diff_data.prepare()
818 new_diff_data = diffs.DiffProcessor(new_diff)
818 new_diff_data = diffs.DiffProcessor(new_diff)
819 new_diff_data.prepare()
819 new_diff_data.prepare()
820
820
821 return old_diff_data, new_diff_data
821 return old_diff_data, new_diff_data
822
822
823 def _link_comments_to_version(self, pull_request_version):
823 def _link_comments_to_version(self, pull_request_version):
824 """
824 """
825 Link all unlinked comments of this pull request to the given version.
825 Link all unlinked comments of this pull request to the given version.
826
826
827 :param pull_request_version: The `PullRequestVersion` to which
827 :param pull_request_version: The `PullRequestVersion` to which
828 the comments shall be linked.
828 the comments shall be linked.
829
829
830 """
830 """
831 pull_request = pull_request_version.pull_request
831 pull_request = pull_request_version.pull_request
832 comments = ChangesetComment.query()\
832 comments = ChangesetComment.query()\
833 .filter(
833 .filter(
834 # TODO: johbo: Should we query for the repo at all here?
834 # TODO: johbo: Should we query for the repo at all here?
835 # Pending decision on how comments of PRs are to be related
835 # Pending decision on how comments of PRs are to be related
836 # to either the source repo, the target repo or no repo at all.
836 # to either the source repo, the target repo or no repo at all.
837 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
837 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
838 ChangesetComment.pull_request == pull_request,
838 ChangesetComment.pull_request == pull_request,
839 ChangesetComment.pull_request_version == None)\
839 ChangesetComment.pull_request_version == None)\
840 .order_by(ChangesetComment.comment_id.asc())
840 .order_by(ChangesetComment.comment_id.asc())
841
841
842 # TODO: johbo: Find out why this breaks if it is done in a bulk
842 # TODO: johbo: Find out why this breaks if it is done in a bulk
843 # operation.
843 # operation.
844 for comment in comments:
844 for comment in comments:
845 comment.pull_request_version_id = (
845 comment.pull_request_version_id = (
846 pull_request_version.pull_request_version_id)
846 pull_request_version.pull_request_version_id)
847 Session().add(comment)
847 Session().add(comment)
848
848
849 def _calculate_commit_id_changes(self, old_ids, new_ids):
849 def _calculate_commit_id_changes(self, old_ids, new_ids):
850 added = [x for x in new_ids if x not in old_ids]
850 added = [x for x in new_ids if x not in old_ids]
851 common = [x for x in new_ids if x in old_ids]
851 common = [x for x in new_ids if x in old_ids]
852 removed = [x for x in old_ids if x not in new_ids]
852 removed = [x for x in old_ids if x not in new_ids]
853 total = new_ids
853 total = new_ids
854 return ChangeTuple(added, common, removed, total)
854 return ChangeTuple(added, common, removed, total)
855
855
856 def _calculate_file_changes(self, old_diff_data, new_diff_data):
856 def _calculate_file_changes(self, old_diff_data, new_diff_data):
857
857
858 old_files = OrderedDict()
858 old_files = OrderedDict()
859 for diff_data in old_diff_data.parsed_diff:
859 for diff_data in old_diff_data.parsed_diff:
860 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
860 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
861
861
862 added_files = []
862 added_files = []
863 modified_files = []
863 modified_files = []
864 removed_files = []
864 removed_files = []
865 for diff_data in new_diff_data.parsed_diff:
865 for diff_data in new_diff_data.parsed_diff:
866 new_filename = diff_data['filename']
866 new_filename = diff_data['filename']
867 new_hash = md5_safe(diff_data['raw_diff'])
867 new_hash = md5_safe(diff_data['raw_diff'])
868
868
869 old_hash = old_files.get(new_filename)
869 old_hash = old_files.get(new_filename)
870 if not old_hash:
870 if not old_hash:
871 # file is not present in old diff, means it's added
871 # file is not present in old diff, means it's added
872 added_files.append(new_filename)
872 added_files.append(new_filename)
873 else:
873 else:
874 if new_hash != old_hash:
874 if new_hash != old_hash:
875 modified_files.append(new_filename)
875 modified_files.append(new_filename)
876 # now remove a file from old, since we have seen it already
876 # now remove a file from old, since we have seen it already
877 del old_files[new_filename]
877 del old_files[new_filename]
878
878
879 # removed files is when there are present in old, but not in NEW,
879 # removed files is when there are present in old, but not in NEW,
880 # since we remove old files that are present in new diff, left-overs
880 # since we remove old files that are present in new diff, left-overs
881 # if any should be the removed files
881 # if any should be the removed files
882 removed_files.extend(old_files.keys())
882 removed_files.extend(old_files.keys())
883
883
884 return FileChangeTuple(added_files, modified_files, removed_files)
884 return FileChangeTuple(added_files, modified_files, removed_files)
885
885
886 def _render_update_message(self, changes, file_changes):
886 def _render_update_message(self, changes, file_changes):
887 """
887 """
888 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
888 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
889 so it's always looking the same disregarding on which default
889 so it's always looking the same disregarding on which default
890 renderer system is using.
890 renderer system is using.
891
891
892 :param changes: changes named tuple
892 :param changes: changes named tuple
893 :param file_changes: file changes named tuple
893 :param file_changes: file changes named tuple
894
894
895 """
895 """
896 new_status = ChangesetStatus.get_status_lbl(
896 new_status = ChangesetStatus.get_status_lbl(
897 ChangesetStatus.STATUS_UNDER_REVIEW)
897 ChangesetStatus.STATUS_UNDER_REVIEW)
898
898
899 changed_files = (
899 changed_files = (
900 file_changes.added + file_changes.modified + file_changes.removed)
900 file_changes.added + file_changes.modified + file_changes.removed)
901
901
902 params = {
902 params = {
903 'under_review_label': new_status,
903 'under_review_label': new_status,
904 'added_commits': changes.added,
904 'added_commits': changes.added,
905 'removed_commits': changes.removed,
905 'removed_commits': changes.removed,
906 'changed_files': changed_files,
906 'changed_files': changed_files,
907 'added_files': file_changes.added,
907 'added_files': file_changes.added,
908 'modified_files': file_changes.modified,
908 'modified_files': file_changes.modified,
909 'removed_files': file_changes.removed,
909 'removed_files': file_changes.removed,
910 }
910 }
911 renderer = RstTemplateRenderer()
911 renderer = RstTemplateRenderer()
912 return renderer.render('pull_request_update.mako', **params)
912 return renderer.render('pull_request_update.mako', **params)
913
913
914 def edit(self, pull_request, title, description, user):
914 def edit(self, pull_request, title, description, user):
915 pull_request = self.__get_pull_request(pull_request)
915 pull_request = self.__get_pull_request(pull_request)
916 old_data = pull_request.get_api_data(with_merge_state=False)
916 old_data = pull_request.get_api_data(with_merge_state=False)
917 if pull_request.is_closed():
917 if pull_request.is_closed():
918 raise ValueError('This pull request is closed')
918 raise ValueError('This pull request is closed')
919 if title:
919 if title:
920 pull_request.title = title
920 pull_request.title = title
921 pull_request.description = description
921 pull_request.description = description
922 pull_request.updated_on = datetime.datetime.now()
922 pull_request.updated_on = datetime.datetime.now()
923 Session().add(pull_request)
923 Session().add(pull_request)
924 self._log_audit_action(
924 self._log_audit_action(
925 'repo.pull_request.edit', {'old_data': old_data},
925 'repo.pull_request.edit', {'old_data': old_data},
926 user, pull_request)
926 user, pull_request)
927
927
928 def update_reviewers(self, pull_request, reviewer_data, user):
928 def update_reviewers(self, pull_request, reviewer_data, user):
929 """
929 """
930 Update the reviewers in the pull request
930 Update the reviewers in the pull request
931
931
932 :param pull_request: the pr to update
932 :param pull_request: the pr to update
933 :param reviewer_data: list of tuples
933 :param reviewer_data: list of tuples
934 [(user, ['reason1', 'reason2'], mandatory_flag)]
934 [(user, ['reason1', 'reason2'], mandatory_flag)]
935 """
935 """
936
936
937 reviewers = {}
937 reviewers = {}
938 for user_id, reasons, mandatory in reviewer_data:
938 for user_id, reasons, mandatory in reviewer_data:
939 if isinstance(user_id, (int, basestring)):
939 if isinstance(user_id, (int, basestring)):
940 user_id = self._get_user(user_id).user_id
940 user_id = self._get_user(user_id).user_id
941 reviewers[user_id] = {
941 reviewers[user_id] = {
942 'reasons': reasons, 'mandatory': mandatory}
942 'reasons': reasons, 'mandatory': mandatory}
943
943
944 reviewers_ids = set(reviewers.keys())
944 reviewers_ids = set(reviewers.keys())
945 pull_request = self.__get_pull_request(pull_request)
945 pull_request = self.__get_pull_request(pull_request)
946 current_reviewers = PullRequestReviewers.query()\
946 current_reviewers = PullRequestReviewers.query()\
947 .filter(PullRequestReviewers.pull_request ==
947 .filter(PullRequestReviewers.pull_request ==
948 pull_request).all()
948 pull_request).all()
949 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
949 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
950
950
951 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
951 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
952 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
952 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
953
953
954 log.debug("Adding %s reviewers", ids_to_add)
954 log.debug("Adding %s reviewers", ids_to_add)
955 log.debug("Removing %s reviewers", ids_to_remove)
955 log.debug("Removing %s reviewers", ids_to_remove)
956 changed = False
956 changed = False
957 for uid in ids_to_add:
957 for uid in ids_to_add:
958 changed = True
958 changed = True
959 _usr = self._get_user(uid)
959 _usr = self._get_user(uid)
960 reviewer = PullRequestReviewers()
960 reviewer = PullRequestReviewers()
961 reviewer.user = _usr
961 reviewer.user = _usr
962 reviewer.pull_request = pull_request
962 reviewer.pull_request = pull_request
963 reviewer.reasons = reviewers[uid]['reasons']
963 reviewer.reasons = reviewers[uid]['reasons']
964 # NOTE(marcink): mandatory shouldn't be changed now
964 # NOTE(marcink): mandatory shouldn't be changed now
965 # reviewer.mandatory = reviewers[uid]['reasons']
965 # reviewer.mandatory = reviewers[uid]['reasons']
966 Session().add(reviewer)
966 Session().add(reviewer)
967 self._log_audit_action(
967 self._log_audit_action(
968 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
968 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
969 user, pull_request)
969 user, pull_request)
970
970
971 for uid in ids_to_remove:
971 for uid in ids_to_remove:
972 changed = True
972 changed = True
973 reviewers = PullRequestReviewers.query()\
973 reviewers = PullRequestReviewers.query()\
974 .filter(PullRequestReviewers.user_id == uid,
974 .filter(PullRequestReviewers.user_id == uid,
975 PullRequestReviewers.pull_request == pull_request)\
975 PullRequestReviewers.pull_request == pull_request)\
976 .all()
976 .all()
977 # use .all() in case we accidentally added the same person twice
977 # use .all() in case we accidentally added the same person twice
978 # this CAN happen due to the lack of DB checks
978 # this CAN happen due to the lack of DB checks
979 for obj in reviewers:
979 for obj in reviewers:
980 old_data = obj.get_dict()
980 old_data = obj.get_dict()
981 Session().delete(obj)
981 Session().delete(obj)
982 self._log_audit_action(
982 self._log_audit_action(
983 'repo.pull_request.reviewer.delete',
983 'repo.pull_request.reviewer.delete',
984 {'old_data': old_data}, user, pull_request)
984 {'old_data': old_data}, user, pull_request)
985
985
986 if changed:
986 if changed:
987 pull_request.updated_on = datetime.datetime.now()
987 pull_request.updated_on = datetime.datetime.now()
988 Session().add(pull_request)
988 Session().add(pull_request)
989
989
990 self.notify_reviewers(pull_request, ids_to_add)
990 self.notify_reviewers(pull_request, ids_to_add)
991 return ids_to_add, ids_to_remove
991 return ids_to_add, ids_to_remove
992
992
993 def get_url(self, pull_request, request=None, permalink=False):
993 def get_url(self, pull_request, request=None, permalink=False):
994 if not request:
994 if not request:
995 request = get_current_request()
995 request = get_current_request()
996
996
997 if permalink:
997 if permalink:
998 return request.route_url(
998 return request.route_url(
999 'pull_requests_global',
999 'pull_requests_global',
1000 pull_request_id=pull_request.pull_request_id,)
1000 pull_request_id=pull_request.pull_request_id,)
1001 else:
1001 else:
1002 return request.route_url('pullrequest_show',
1002 return request.route_url('pullrequest_show',
1003 repo_name=safe_str(pull_request.target_repo.repo_name),
1003 repo_name=safe_str(pull_request.target_repo.repo_name),
1004 pull_request_id=pull_request.pull_request_id,)
1004 pull_request_id=pull_request.pull_request_id,)
1005
1005
1006 def get_shadow_clone_url(self, pull_request):
1006 def get_shadow_clone_url(self, pull_request):
1007 """
1007 """
1008 Returns qualified url pointing to the shadow repository. If this pull
1008 Returns qualified url pointing to the shadow repository. If this pull
1009 request is closed there is no shadow repository and ``None`` will be
1009 request is closed there is no shadow repository and ``None`` will be
1010 returned.
1010 returned.
1011 """
1011 """
1012 if pull_request.is_closed():
1012 if pull_request.is_closed():
1013 return None
1013 return None
1014 else:
1014 else:
1015 pr_url = urllib.unquote(self.get_url(pull_request))
1015 pr_url = urllib.unquote(self.get_url(pull_request))
1016 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1016 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1017
1017
1018 def notify_reviewers(self, pull_request, reviewers_ids):
1018 def notify_reviewers(self, pull_request, reviewers_ids):
1019 # notification to reviewers
1019 # notification to reviewers
1020 if not reviewers_ids:
1020 if not reviewers_ids:
1021 return
1021 return
1022
1022
1023 pull_request_obj = pull_request
1023 pull_request_obj = pull_request
1024 # get the current participants of this pull request
1024 # get the current participants of this pull request
1025 recipients = reviewers_ids
1025 recipients = reviewers_ids
1026 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1026 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1027
1027
1028 pr_source_repo = pull_request_obj.source_repo
1028 pr_source_repo = pull_request_obj.source_repo
1029 pr_target_repo = pull_request_obj.target_repo
1029 pr_target_repo = pull_request_obj.target_repo
1030
1030
1031 pr_url = h.route_url('pullrequest_show',
1031 pr_url = h.route_url('pullrequest_show',
1032 repo_name=pr_target_repo.repo_name,
1032 repo_name=pr_target_repo.repo_name,
1033 pull_request_id=pull_request_obj.pull_request_id,)
1033 pull_request_id=pull_request_obj.pull_request_id,)
1034
1034
1035 # set some variables for email notification
1035 # set some variables for email notification
1036 pr_target_repo_url = h.route_url(
1036 pr_target_repo_url = h.route_url(
1037 'repo_summary', repo_name=pr_target_repo.repo_name)
1037 'repo_summary', repo_name=pr_target_repo.repo_name)
1038
1038
1039 pr_source_repo_url = h.route_url(
1039 pr_source_repo_url = h.route_url(
1040 'repo_summary', repo_name=pr_source_repo.repo_name)
1040 'repo_summary', repo_name=pr_source_repo.repo_name)
1041
1041
1042 # pull request specifics
1042 # pull request specifics
1043 pull_request_commits = [
1043 pull_request_commits = [
1044 (x.raw_id, x.message)
1044 (x.raw_id, x.message)
1045 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1045 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1046
1046
1047 kwargs = {
1047 kwargs = {
1048 'user': pull_request.author,
1048 'user': pull_request.author,
1049 'pull_request': pull_request_obj,
1049 'pull_request': pull_request_obj,
1050 'pull_request_commits': pull_request_commits,
1050 'pull_request_commits': pull_request_commits,
1051
1051
1052 'pull_request_target_repo': pr_target_repo,
1052 'pull_request_target_repo': pr_target_repo,
1053 'pull_request_target_repo_url': pr_target_repo_url,
1053 'pull_request_target_repo_url': pr_target_repo_url,
1054
1054
1055 'pull_request_source_repo': pr_source_repo,
1055 'pull_request_source_repo': pr_source_repo,
1056 'pull_request_source_repo_url': pr_source_repo_url,
1056 'pull_request_source_repo_url': pr_source_repo_url,
1057
1057
1058 'pull_request_url': pr_url,
1058 'pull_request_url': pr_url,
1059 }
1059 }
1060
1060
1061 # pre-generate the subject for notification itself
1061 # pre-generate the subject for notification itself
1062 (subject,
1062 (subject,
1063 _h, _e, # we don't care about those
1063 _h, _e, # we don't care about those
1064 body_plaintext) = EmailNotificationModel().render_email(
1064 body_plaintext) = EmailNotificationModel().render_email(
1065 notification_type, **kwargs)
1065 notification_type, **kwargs)
1066
1066
1067 # create notification objects, and emails
1067 # create notification objects, and emails
1068 NotificationModel().create(
1068 NotificationModel().create(
1069 created_by=pull_request.author,
1069 created_by=pull_request.author,
1070 notification_subject=subject,
1070 notification_subject=subject,
1071 notification_body=body_plaintext,
1071 notification_body=body_plaintext,
1072 notification_type=notification_type,
1072 notification_type=notification_type,
1073 recipients=recipients,
1073 recipients=recipients,
1074 email_kwargs=kwargs,
1074 email_kwargs=kwargs,
1075 )
1075 )
1076
1076
1077 def delete(self, pull_request, user):
1077 def delete(self, pull_request, user):
1078 pull_request = self.__get_pull_request(pull_request)
1078 pull_request = self.__get_pull_request(pull_request)
1079 old_data = pull_request.get_api_data(with_merge_state=False)
1079 old_data = pull_request.get_api_data(with_merge_state=False)
1080 self._cleanup_merge_workspace(pull_request)
1080 self._cleanup_merge_workspace(pull_request)
1081 self._log_audit_action(
1081 self._log_audit_action(
1082 'repo.pull_request.delete', {'old_data': old_data},
1082 'repo.pull_request.delete', {'old_data': old_data},
1083 user, pull_request)
1083 user, pull_request)
1084 Session().delete(pull_request)
1084 Session().delete(pull_request)
1085
1085
1086 def close_pull_request(self, pull_request, user):
1086 def close_pull_request(self, pull_request, user):
1087 pull_request = self.__get_pull_request(pull_request)
1087 pull_request = self.__get_pull_request(pull_request)
1088 self._cleanup_merge_workspace(pull_request)
1088 self._cleanup_merge_workspace(pull_request)
1089 pull_request.status = PullRequest.STATUS_CLOSED
1089 pull_request.status = PullRequest.STATUS_CLOSED
1090 pull_request.updated_on = datetime.datetime.now()
1090 pull_request.updated_on = datetime.datetime.now()
1091 Session().add(pull_request)
1091 Session().add(pull_request)
1092 self._trigger_pull_request_hook(
1092 self._trigger_pull_request_hook(
1093 pull_request, pull_request.author, 'close')
1093 pull_request, pull_request.author, 'close')
1094
1095 pr_data = pull_request.get_api_data(with_merge_state=False)
1094 self._log_audit_action(
1096 self._log_audit_action(
1095 'repo.pull_request.close', {}, user, pull_request)
1097 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1096
1098
1097 def close_pull_request_with_comment(
1099 def close_pull_request_with_comment(
1098 self, pull_request, user, repo, message=None):
1100 self, pull_request, user, repo, message=None):
1099
1101
1100 pull_request_review_status = pull_request.calculated_review_status()
1102 pull_request_review_status = pull_request.calculated_review_status()
1101
1103
1102 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1104 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1103 # approved only if we have voting consent
1105 # approved only if we have voting consent
1104 status = ChangesetStatus.STATUS_APPROVED
1106 status = ChangesetStatus.STATUS_APPROVED
1105 else:
1107 else:
1106 status = ChangesetStatus.STATUS_REJECTED
1108 status = ChangesetStatus.STATUS_REJECTED
1107 status_lbl = ChangesetStatus.get_status_lbl(status)
1109 status_lbl = ChangesetStatus.get_status_lbl(status)
1108
1110
1109 default_message = (
1111 default_message = (
1110 _('Closing with status change {transition_icon} {status}.')
1112 _('Closing with status change {transition_icon} {status}.')
1111 ).format(transition_icon='>', status=status_lbl)
1113 ).format(transition_icon='>', status=status_lbl)
1112 text = message or default_message
1114 text = message or default_message
1113
1115
1114 # create a comment, and link it to new status
1116 # create a comment, and link it to new status
1115 comment = CommentsModel().create(
1117 comment = CommentsModel().create(
1116 text=text,
1118 text=text,
1117 repo=repo.repo_id,
1119 repo=repo.repo_id,
1118 user=user.user_id,
1120 user=user.user_id,
1119 pull_request=pull_request.pull_request_id,
1121 pull_request=pull_request.pull_request_id,
1120 status_change=status_lbl,
1122 status_change=status_lbl,
1121 status_change_type=status,
1123 status_change_type=status,
1122 closing_pr=True
1124 closing_pr=True
1123 )
1125 )
1124
1126
1125 # calculate old status before we change it
1127 # calculate old status before we change it
1126 old_calculated_status = pull_request.calculated_review_status()
1128 old_calculated_status = pull_request.calculated_review_status()
1127 ChangesetStatusModel().set_status(
1129 ChangesetStatusModel().set_status(
1128 repo.repo_id,
1130 repo.repo_id,
1129 status,
1131 status,
1130 user.user_id,
1132 user.user_id,
1131 comment=comment,
1133 comment=comment,
1132 pull_request=pull_request.pull_request_id
1134 pull_request=pull_request.pull_request_id
1133 )
1135 )
1134
1136
1135 Session().flush()
1137 Session().flush()
1136 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1138 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1137 # we now calculate the status of pull request again, and based on that
1139 # we now calculate the status of pull request again, and based on that
1138 # calculation trigger status change. This might happen in cases
1140 # calculation trigger status change. This might happen in cases
1139 # that non-reviewer admin closes a pr, which means his vote doesn't
1141 # that non-reviewer admin closes a pr, which means his vote doesn't
1140 # change the status, while if he's a reviewer this might change it.
1142 # change the status, while if he's a reviewer this might change it.
1141 calculated_status = pull_request.calculated_review_status()
1143 calculated_status = pull_request.calculated_review_status()
1142 if old_calculated_status != calculated_status:
1144 if old_calculated_status != calculated_status:
1143 self._trigger_pull_request_hook(
1145 self._trigger_pull_request_hook(
1144 pull_request, user, 'review_status_change')
1146 pull_request, user, 'review_status_change')
1145
1147
1146 # finally close the PR
1148 # finally close the PR
1147 PullRequestModel().close_pull_request(
1149 PullRequestModel().close_pull_request(
1148 pull_request.pull_request_id, user)
1150 pull_request.pull_request_id, user)
1149
1151
1150 return comment, status
1152 return comment, status
1151
1153
1152 def merge_status(self, pull_request):
1154 def merge_status(self, pull_request):
1153 if not self._is_merge_enabled(pull_request):
1155 if not self._is_merge_enabled(pull_request):
1154 return False, _('Server-side pull request merging is disabled.')
1156 return False, _('Server-side pull request merging is disabled.')
1155 if pull_request.is_closed():
1157 if pull_request.is_closed():
1156 return False, _('This pull request is closed.')
1158 return False, _('This pull request is closed.')
1157 merge_possible, msg = self._check_repo_requirements(
1159 merge_possible, msg = self._check_repo_requirements(
1158 target=pull_request.target_repo, source=pull_request.source_repo)
1160 target=pull_request.target_repo, source=pull_request.source_repo)
1159 if not merge_possible:
1161 if not merge_possible:
1160 return merge_possible, msg
1162 return merge_possible, msg
1161
1163
1162 try:
1164 try:
1163 resp = self._try_merge(pull_request)
1165 resp = self._try_merge(pull_request)
1164 log.debug("Merge response: %s", resp)
1166 log.debug("Merge response: %s", resp)
1165 status = resp.possible, self.merge_status_message(
1167 status = resp.possible, self.merge_status_message(
1166 resp.failure_reason)
1168 resp.failure_reason)
1167 except NotImplementedError:
1169 except NotImplementedError:
1168 status = False, _('Pull request merging is not supported.')
1170 status = False, _('Pull request merging is not supported.')
1169
1171
1170 return status
1172 return status
1171
1173
1172 def _check_repo_requirements(self, target, source):
1174 def _check_repo_requirements(self, target, source):
1173 """
1175 """
1174 Check if `target` and `source` have compatible requirements.
1176 Check if `target` and `source` have compatible requirements.
1175
1177
1176 Currently this is just checking for largefiles.
1178 Currently this is just checking for largefiles.
1177 """
1179 """
1178 target_has_largefiles = self._has_largefiles(target)
1180 target_has_largefiles = self._has_largefiles(target)
1179 source_has_largefiles = self._has_largefiles(source)
1181 source_has_largefiles = self._has_largefiles(source)
1180 merge_possible = True
1182 merge_possible = True
1181 message = u''
1183 message = u''
1182
1184
1183 if target_has_largefiles != source_has_largefiles:
1185 if target_has_largefiles != source_has_largefiles:
1184 merge_possible = False
1186 merge_possible = False
1185 if source_has_largefiles:
1187 if source_has_largefiles:
1186 message = _(
1188 message = _(
1187 'Target repository large files support is disabled.')
1189 'Target repository large files support is disabled.')
1188 else:
1190 else:
1189 message = _(
1191 message = _(
1190 'Source repository large files support is disabled.')
1192 'Source repository large files support is disabled.')
1191
1193
1192 return merge_possible, message
1194 return merge_possible, message
1193
1195
1194 def _has_largefiles(self, repo):
1196 def _has_largefiles(self, repo):
1195 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1197 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1196 'extensions', 'largefiles')
1198 'extensions', 'largefiles')
1197 return largefiles_ui and largefiles_ui[0].active
1199 return largefiles_ui and largefiles_ui[0].active
1198
1200
1199 def _try_merge(self, pull_request):
1201 def _try_merge(self, pull_request):
1200 """
1202 """
1201 Try to merge the pull request and return the merge status.
1203 Try to merge the pull request and return the merge status.
1202 """
1204 """
1203 log.debug(
1205 log.debug(
1204 "Trying out if the pull request %s can be merged.",
1206 "Trying out if the pull request %s can be merged.",
1205 pull_request.pull_request_id)
1207 pull_request.pull_request_id)
1206 target_vcs = pull_request.target_repo.scm_instance()
1208 target_vcs = pull_request.target_repo.scm_instance()
1207
1209
1208 # Refresh the target reference.
1210 # Refresh the target reference.
1209 try:
1211 try:
1210 target_ref = self._refresh_reference(
1212 target_ref = self._refresh_reference(
1211 pull_request.target_ref_parts, target_vcs)
1213 pull_request.target_ref_parts, target_vcs)
1212 except CommitDoesNotExistError:
1214 except CommitDoesNotExistError:
1213 merge_state = MergeResponse(
1215 merge_state = MergeResponse(
1214 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1216 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1215 return merge_state
1217 return merge_state
1216
1218
1217 target_locked = pull_request.target_repo.locked
1219 target_locked = pull_request.target_repo.locked
1218 if target_locked and target_locked[0]:
1220 if target_locked and target_locked[0]:
1219 log.debug("The target repository is locked.")
1221 log.debug("The target repository is locked.")
1220 merge_state = MergeResponse(
1222 merge_state = MergeResponse(
1221 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1223 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1222 elif self._needs_merge_state_refresh(pull_request, target_ref):
1224 elif self._needs_merge_state_refresh(pull_request, target_ref):
1223 log.debug("Refreshing the merge status of the repository.")
1225 log.debug("Refreshing the merge status of the repository.")
1224 merge_state = self._refresh_merge_state(
1226 merge_state = self._refresh_merge_state(
1225 pull_request, target_vcs, target_ref)
1227 pull_request, target_vcs, target_ref)
1226 else:
1228 else:
1227 possible = pull_request.\
1229 possible = pull_request.\
1228 last_merge_status == MergeFailureReason.NONE
1230 last_merge_status == MergeFailureReason.NONE
1229 merge_state = MergeResponse(
1231 merge_state = MergeResponse(
1230 possible, False, None, pull_request.last_merge_status)
1232 possible, False, None, pull_request.last_merge_status)
1231
1233
1232 return merge_state
1234 return merge_state
1233
1235
1234 def _refresh_reference(self, reference, vcs_repository):
1236 def _refresh_reference(self, reference, vcs_repository):
1235 if reference.type in ('branch', 'book'):
1237 if reference.type in ('branch', 'book'):
1236 name_or_id = reference.name
1238 name_or_id = reference.name
1237 else:
1239 else:
1238 name_or_id = reference.commit_id
1240 name_or_id = reference.commit_id
1239 refreshed_commit = vcs_repository.get_commit(name_or_id)
1241 refreshed_commit = vcs_repository.get_commit(name_or_id)
1240 refreshed_reference = Reference(
1242 refreshed_reference = Reference(
1241 reference.type, reference.name, refreshed_commit.raw_id)
1243 reference.type, reference.name, refreshed_commit.raw_id)
1242 return refreshed_reference
1244 return refreshed_reference
1243
1245
1244 def _needs_merge_state_refresh(self, pull_request, target_reference):
1246 def _needs_merge_state_refresh(self, pull_request, target_reference):
1245 return not(
1247 return not(
1246 pull_request.revisions and
1248 pull_request.revisions and
1247 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1249 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1248 target_reference.commit_id == pull_request._last_merge_target_rev)
1250 target_reference.commit_id == pull_request._last_merge_target_rev)
1249
1251
1250 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1252 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1251 workspace_id = self._workspace_id(pull_request)
1253 workspace_id = self._workspace_id(pull_request)
1252 source_vcs = pull_request.source_repo.scm_instance()
1254 source_vcs = pull_request.source_repo.scm_instance()
1253 use_rebase = self._use_rebase_for_merging(pull_request)
1255 use_rebase = self._use_rebase_for_merging(pull_request)
1254 close_branch = self._close_branch_before_merging(pull_request)
1256 close_branch = self._close_branch_before_merging(pull_request)
1255 merge_state = target_vcs.merge(
1257 merge_state = target_vcs.merge(
1256 target_reference, source_vcs, pull_request.source_ref_parts,
1258 target_reference, source_vcs, pull_request.source_ref_parts,
1257 workspace_id, dry_run=True, use_rebase=use_rebase,
1259 workspace_id, dry_run=True, use_rebase=use_rebase,
1258 close_branch=close_branch)
1260 close_branch=close_branch)
1259
1261
1260 # Do not store the response if there was an unknown error.
1262 # Do not store the response if there was an unknown error.
1261 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1263 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1262 pull_request._last_merge_source_rev = \
1264 pull_request._last_merge_source_rev = \
1263 pull_request.source_ref_parts.commit_id
1265 pull_request.source_ref_parts.commit_id
1264 pull_request._last_merge_target_rev = target_reference.commit_id
1266 pull_request._last_merge_target_rev = target_reference.commit_id
1265 pull_request.last_merge_status = merge_state.failure_reason
1267 pull_request.last_merge_status = merge_state.failure_reason
1266 pull_request.shadow_merge_ref = merge_state.merge_ref
1268 pull_request.shadow_merge_ref = merge_state.merge_ref
1267 Session().add(pull_request)
1269 Session().add(pull_request)
1268 Session().commit()
1270 Session().commit()
1269
1271
1270 return merge_state
1272 return merge_state
1271
1273
1272 def _workspace_id(self, pull_request):
1274 def _workspace_id(self, pull_request):
1273 workspace_id = 'pr-%s' % pull_request.pull_request_id
1275 workspace_id = 'pr-%s' % pull_request.pull_request_id
1274 return workspace_id
1276 return workspace_id
1275
1277
1276 def merge_status_message(self, status_code):
1278 def merge_status_message(self, status_code):
1277 """
1279 """
1278 Return a human friendly error message for the given merge status code.
1280 Return a human friendly error message for the given merge status code.
1279 """
1281 """
1280 return self.MERGE_STATUS_MESSAGES[status_code]
1282 return self.MERGE_STATUS_MESSAGES[status_code]
1281
1283
1282 def generate_repo_data(self, repo, commit_id=None, branch=None,
1284 def generate_repo_data(self, repo, commit_id=None, branch=None,
1283 bookmark=None):
1285 bookmark=None):
1284 all_refs, selected_ref = \
1286 all_refs, selected_ref = \
1285 self._get_repo_pullrequest_sources(
1287 self._get_repo_pullrequest_sources(
1286 repo.scm_instance(), commit_id=commit_id,
1288 repo.scm_instance(), commit_id=commit_id,
1287 branch=branch, bookmark=bookmark)
1289 branch=branch, bookmark=bookmark)
1288
1290
1289 refs_select2 = []
1291 refs_select2 = []
1290 for element in all_refs:
1292 for element in all_refs:
1291 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1293 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1292 refs_select2.append({'text': element[1], 'children': children})
1294 refs_select2.append({'text': element[1], 'children': children})
1293
1295
1294 return {
1296 return {
1295 'user': {
1297 'user': {
1296 'user_id': repo.user.user_id,
1298 'user_id': repo.user.user_id,
1297 'username': repo.user.username,
1299 'username': repo.user.username,
1298 'firstname': repo.user.first_name,
1300 'firstname': repo.user.first_name,
1299 'lastname': repo.user.last_name,
1301 'lastname': repo.user.last_name,
1300 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1302 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1301 },
1303 },
1302 'description': h.chop_at_smart(repo.description_safe, '\n'),
1304 'description': h.chop_at_smart(repo.description_safe, '\n'),
1303 'refs': {
1305 'refs': {
1304 'all_refs': all_refs,
1306 'all_refs': all_refs,
1305 'selected_ref': selected_ref,
1307 'selected_ref': selected_ref,
1306 'select2_refs': refs_select2
1308 'select2_refs': refs_select2
1307 }
1309 }
1308 }
1310 }
1309
1311
1310 def generate_pullrequest_title(self, source, source_ref, target):
1312 def generate_pullrequest_title(self, source, source_ref, target):
1311 return u'{source}#{at_ref} to {target}'.format(
1313 return u'{source}#{at_ref} to {target}'.format(
1312 source=source,
1314 source=source,
1313 at_ref=source_ref,
1315 at_ref=source_ref,
1314 target=target,
1316 target=target,
1315 )
1317 )
1316
1318
1317 def _cleanup_merge_workspace(self, pull_request):
1319 def _cleanup_merge_workspace(self, pull_request):
1318 # Merging related cleanup
1320 # Merging related cleanup
1319 target_scm = pull_request.target_repo.scm_instance()
1321 target_scm = pull_request.target_repo.scm_instance()
1320 workspace_id = 'pr-%s' % pull_request.pull_request_id
1322 workspace_id = 'pr-%s' % pull_request.pull_request_id
1321
1323
1322 try:
1324 try:
1323 target_scm.cleanup_merge_workspace(workspace_id)
1325 target_scm.cleanup_merge_workspace(workspace_id)
1324 except NotImplementedError:
1326 except NotImplementedError:
1325 pass
1327 pass
1326
1328
1327 def _get_repo_pullrequest_sources(
1329 def _get_repo_pullrequest_sources(
1328 self, repo, commit_id=None, branch=None, bookmark=None):
1330 self, repo, commit_id=None, branch=None, bookmark=None):
1329 """
1331 """
1330 Return a structure with repo's interesting commits, suitable for
1332 Return a structure with repo's interesting commits, suitable for
1331 the selectors in pullrequest controller
1333 the selectors in pullrequest controller
1332
1334
1333 :param commit_id: a commit that must be in the list somehow
1335 :param commit_id: a commit that must be in the list somehow
1334 and selected by default
1336 and selected by default
1335 :param branch: a branch that must be in the list and selected
1337 :param branch: a branch that must be in the list and selected
1336 by default - even if closed
1338 by default - even if closed
1337 :param bookmark: a bookmark that must be in the list and selected
1339 :param bookmark: a bookmark that must be in the list and selected
1338 """
1340 """
1339
1341
1340 commit_id = safe_str(commit_id) if commit_id else None
1342 commit_id = safe_str(commit_id) if commit_id else None
1341 branch = safe_str(branch) if branch else None
1343 branch = safe_str(branch) if branch else None
1342 bookmark = safe_str(bookmark) if bookmark else None
1344 bookmark = safe_str(bookmark) if bookmark else None
1343
1345
1344 selected = None
1346 selected = None
1345
1347
1346 # order matters: first source that has commit_id in it will be selected
1348 # order matters: first source that has commit_id in it will be selected
1347 sources = []
1349 sources = []
1348 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1350 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1349 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1351 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1350
1352
1351 if commit_id:
1353 if commit_id:
1352 ref_commit = (h.short_id(commit_id), commit_id)
1354 ref_commit = (h.short_id(commit_id), commit_id)
1353 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1355 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1354
1356
1355 sources.append(
1357 sources.append(
1356 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1358 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1357 )
1359 )
1358
1360
1359 groups = []
1361 groups = []
1360 for group_key, ref_list, group_name, match in sources:
1362 for group_key, ref_list, group_name, match in sources:
1361 group_refs = []
1363 group_refs = []
1362 for ref_name, ref_id in ref_list:
1364 for ref_name, ref_id in ref_list:
1363 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1365 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1364 group_refs.append((ref_key, ref_name))
1366 group_refs.append((ref_key, ref_name))
1365
1367
1366 if not selected:
1368 if not selected:
1367 if set([commit_id, match]) & set([ref_id, ref_name]):
1369 if set([commit_id, match]) & set([ref_id, ref_name]):
1368 selected = ref_key
1370 selected = ref_key
1369
1371
1370 if group_refs:
1372 if group_refs:
1371 groups.append((group_refs, group_name))
1373 groups.append((group_refs, group_name))
1372
1374
1373 if not selected:
1375 if not selected:
1374 ref = commit_id or branch or bookmark
1376 ref = commit_id or branch or bookmark
1375 if ref:
1377 if ref:
1376 raise CommitDoesNotExistError(
1378 raise CommitDoesNotExistError(
1377 'No commit refs could be found matching: %s' % ref)
1379 'No commit refs could be found matching: %s' % ref)
1378 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1380 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1379 selected = 'branch:%s:%s' % (
1381 selected = 'branch:%s:%s' % (
1380 repo.DEFAULT_BRANCH_NAME,
1382 repo.DEFAULT_BRANCH_NAME,
1381 repo.branches[repo.DEFAULT_BRANCH_NAME]
1383 repo.branches[repo.DEFAULT_BRANCH_NAME]
1382 )
1384 )
1383 elif repo.commit_ids:
1385 elif repo.commit_ids:
1384 rev = repo.commit_ids[0]
1386 rev = repo.commit_ids[0]
1385 selected = 'rev:%s:%s' % (rev, rev)
1387 selected = 'rev:%s:%s' % (rev, rev)
1386 else:
1388 else:
1387 raise EmptyRepositoryError()
1389 raise EmptyRepositoryError()
1388 return groups, selected
1390 return groups, selected
1389
1391
1390 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1392 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1391 return self._get_diff_from_pr_or_version(
1393 return self._get_diff_from_pr_or_version(
1392 source_repo, source_ref_id, target_ref_id, context=context)
1394 source_repo, source_ref_id, target_ref_id, context=context)
1393
1395
1394 def _get_diff_from_pr_or_version(
1396 def _get_diff_from_pr_or_version(
1395 self, source_repo, source_ref_id, target_ref_id, context):
1397 self, source_repo, source_ref_id, target_ref_id, context):
1396 target_commit = source_repo.get_commit(
1398 target_commit = source_repo.get_commit(
1397 commit_id=safe_str(target_ref_id))
1399 commit_id=safe_str(target_ref_id))
1398 source_commit = source_repo.get_commit(
1400 source_commit = source_repo.get_commit(
1399 commit_id=safe_str(source_ref_id))
1401 commit_id=safe_str(source_ref_id))
1400 if isinstance(source_repo, Repository):
1402 if isinstance(source_repo, Repository):
1401 vcs_repo = source_repo.scm_instance()
1403 vcs_repo = source_repo.scm_instance()
1402 else:
1404 else:
1403 vcs_repo = source_repo
1405 vcs_repo = source_repo
1404
1406
1405 # TODO: johbo: In the context of an update, we cannot reach
1407 # TODO: johbo: In the context of an update, we cannot reach
1406 # the old commit anymore with our normal mechanisms. It needs
1408 # the old commit anymore with our normal mechanisms. It needs
1407 # some sort of special support in the vcs layer to avoid this
1409 # some sort of special support in the vcs layer to avoid this
1408 # workaround.
1410 # workaround.
1409 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1411 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1410 vcs_repo.alias == 'git'):
1412 vcs_repo.alias == 'git'):
1411 source_commit.raw_id = safe_str(source_ref_id)
1413 source_commit.raw_id = safe_str(source_ref_id)
1412
1414
1413 log.debug('calculating diff between '
1415 log.debug('calculating diff between '
1414 'source_ref:%s and target_ref:%s for repo `%s`',
1416 'source_ref:%s and target_ref:%s for repo `%s`',
1415 target_ref_id, source_ref_id,
1417 target_ref_id, source_ref_id,
1416 safe_unicode(vcs_repo.path))
1418 safe_unicode(vcs_repo.path))
1417
1419
1418 vcs_diff = vcs_repo.get_diff(
1420 vcs_diff = vcs_repo.get_diff(
1419 commit1=target_commit, commit2=source_commit, context=context)
1421 commit1=target_commit, commit2=source_commit, context=context)
1420 return vcs_diff
1422 return vcs_diff
1421
1423
1422 def _is_merge_enabled(self, pull_request):
1424 def _is_merge_enabled(self, pull_request):
1423 return self._get_general_setting(
1425 return self._get_general_setting(
1424 pull_request, 'rhodecode_pr_merge_enabled')
1426 pull_request, 'rhodecode_pr_merge_enabled')
1425
1427
1426 def _use_rebase_for_merging(self, pull_request):
1428 def _use_rebase_for_merging(self, pull_request):
1427 repo_type = pull_request.target_repo.repo_type
1429 repo_type = pull_request.target_repo.repo_type
1428 if repo_type == 'hg':
1430 if repo_type == 'hg':
1429 return self._get_general_setting(
1431 return self._get_general_setting(
1430 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1432 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1431 elif repo_type == 'git':
1433 elif repo_type == 'git':
1432 return self._get_general_setting(
1434 return self._get_general_setting(
1433 pull_request, 'rhodecode_git_use_rebase_for_merging')
1435 pull_request, 'rhodecode_git_use_rebase_for_merging')
1434
1436
1435 return False
1437 return False
1436
1438
1437 def _close_branch_before_merging(self, pull_request):
1439 def _close_branch_before_merging(self, pull_request):
1438 repo_type = pull_request.target_repo.repo_type
1440 repo_type = pull_request.target_repo.repo_type
1439 if repo_type == 'hg':
1441 if repo_type == 'hg':
1440 return self._get_general_setting(
1442 return self._get_general_setting(
1441 pull_request, 'rhodecode_hg_close_branch_before_merging')
1443 pull_request, 'rhodecode_hg_close_branch_before_merging')
1442 elif repo_type == 'git':
1444 elif repo_type == 'git':
1443 return self._get_general_setting(
1445 return self._get_general_setting(
1444 pull_request, 'rhodecode_git_close_branch_before_merging')
1446 pull_request, 'rhodecode_git_close_branch_before_merging')
1445
1447
1446 return False
1448 return False
1447
1449
1448 def _get_general_setting(self, pull_request, settings_key, default=False):
1450 def _get_general_setting(self, pull_request, settings_key, default=False):
1449 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1451 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1450 settings = settings_model.get_general_settings()
1452 settings = settings_model.get_general_settings()
1451 return settings.get(settings_key, default)
1453 return settings.get(settings_key, default)
1452
1454
1453 def _log_audit_action(self, action, action_data, user, pull_request):
1455 def _log_audit_action(self, action, action_data, user, pull_request):
1454 audit_logger.store(
1456 audit_logger.store(
1455 action=action,
1457 action=action,
1456 action_data=action_data,
1458 action_data=action_data,
1457 user=user,
1459 user=user,
1458 repo=pull_request.target_repo)
1460 repo=pull_request.target_repo)
1459
1461
1460 def get_reviewer_functions(self):
1462 def get_reviewer_functions(self):
1461 """
1463 """
1462 Fetches functions for validation and fetching default reviewers.
1464 Fetches functions for validation and fetching default reviewers.
1463 If available we use the EE package, else we fallback to CE
1465 If available we use the EE package, else we fallback to CE
1464 package functions
1466 package functions
1465 """
1467 """
1466 try:
1468 try:
1467 from rc_reviewers.utils import get_default_reviewers_data
1469 from rc_reviewers.utils import get_default_reviewers_data
1468 from rc_reviewers.utils import validate_default_reviewers
1470 from rc_reviewers.utils import validate_default_reviewers
1469 except ImportError:
1471 except ImportError:
1470 from rhodecode.apps.repository.utils import \
1472 from rhodecode.apps.repository.utils import \
1471 get_default_reviewers_data
1473 get_default_reviewers_data
1472 from rhodecode.apps.repository.utils import \
1474 from rhodecode.apps.repository.utils import \
1473 validate_default_reviewers
1475 validate_default_reviewers
1474
1476
1475 return get_default_reviewers_data, validate_default_reviewers
1477 return get_default_reviewers_data, validate_default_reviewers
1476
1478
1477
1479
1478 class MergeCheck(object):
1480 class MergeCheck(object):
1479 """
1481 """
1480 Perform Merge Checks and returns a check object which stores information
1482 Perform Merge Checks and returns a check object which stores information
1481 about merge errors, and merge conditions
1483 about merge errors, and merge conditions
1482 """
1484 """
1483 TODO_CHECK = 'todo'
1485 TODO_CHECK = 'todo'
1484 PERM_CHECK = 'perm'
1486 PERM_CHECK = 'perm'
1485 REVIEW_CHECK = 'review'
1487 REVIEW_CHECK = 'review'
1486 MERGE_CHECK = 'merge'
1488 MERGE_CHECK = 'merge'
1487
1489
1488 def __init__(self):
1490 def __init__(self):
1489 self.review_status = None
1491 self.review_status = None
1490 self.merge_possible = None
1492 self.merge_possible = None
1491 self.merge_msg = ''
1493 self.merge_msg = ''
1492 self.failed = None
1494 self.failed = None
1493 self.errors = []
1495 self.errors = []
1494 self.error_details = OrderedDict()
1496 self.error_details = OrderedDict()
1495
1497
1496 def push_error(self, error_type, message, error_key, details):
1498 def push_error(self, error_type, message, error_key, details):
1497 self.failed = True
1499 self.failed = True
1498 self.errors.append([error_type, message])
1500 self.errors.append([error_type, message])
1499 self.error_details[error_key] = dict(
1501 self.error_details[error_key] = dict(
1500 details=details,
1502 details=details,
1501 error_type=error_type,
1503 error_type=error_type,
1502 message=message
1504 message=message
1503 )
1505 )
1504
1506
1505 @classmethod
1507 @classmethod
1506 def validate(cls, pull_request, user, fail_early=False, translator=None):
1508 def validate(cls, pull_request, user, fail_early=False, translator=None):
1507 # if migrated to pyramid...
1509 # if migrated to pyramid...
1508 # _ = lambda: translator or _ # use passed in translator if any
1510 # _ = lambda: translator or _ # use passed in translator if any
1509
1511
1510 merge_check = cls()
1512 merge_check = cls()
1511
1513
1512 # permissions to merge
1514 # permissions to merge
1513 user_allowed_to_merge = PullRequestModel().check_user_merge(
1515 user_allowed_to_merge = PullRequestModel().check_user_merge(
1514 pull_request, user)
1516 pull_request, user)
1515 if not user_allowed_to_merge:
1517 if not user_allowed_to_merge:
1516 log.debug("MergeCheck: cannot merge, approval is pending.")
1518 log.debug("MergeCheck: cannot merge, approval is pending.")
1517
1519
1518 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1520 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1519 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1521 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1520 if fail_early:
1522 if fail_early:
1521 return merge_check
1523 return merge_check
1522
1524
1523 # review status, must be always present
1525 # review status, must be always present
1524 review_status = pull_request.calculated_review_status()
1526 review_status = pull_request.calculated_review_status()
1525 merge_check.review_status = review_status
1527 merge_check.review_status = review_status
1526
1528
1527 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1529 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1528 if not status_approved:
1530 if not status_approved:
1529 log.debug("MergeCheck: cannot merge, approval is pending.")
1531 log.debug("MergeCheck: cannot merge, approval is pending.")
1530
1532
1531 msg = _('Pull request reviewer approval is pending.')
1533 msg = _('Pull request reviewer approval is pending.')
1532
1534
1533 merge_check.push_error(
1535 merge_check.push_error(
1534 'warning', msg, cls.REVIEW_CHECK, review_status)
1536 'warning', msg, cls.REVIEW_CHECK, review_status)
1535
1537
1536 if fail_early:
1538 if fail_early:
1537 return merge_check
1539 return merge_check
1538
1540
1539 # left over TODOs
1541 # left over TODOs
1540 todos = CommentsModel().get_unresolved_todos(pull_request)
1542 todos = CommentsModel().get_unresolved_todos(pull_request)
1541 if todos:
1543 if todos:
1542 log.debug("MergeCheck: cannot merge, {} "
1544 log.debug("MergeCheck: cannot merge, {} "
1543 "unresolved todos left.".format(len(todos)))
1545 "unresolved todos left.".format(len(todos)))
1544
1546
1545 if len(todos) == 1:
1547 if len(todos) == 1:
1546 msg = _('Cannot merge, {} TODO still not resolved.').format(
1548 msg = _('Cannot merge, {} TODO still not resolved.').format(
1547 len(todos))
1549 len(todos))
1548 else:
1550 else:
1549 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1551 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1550 len(todos))
1552 len(todos))
1551
1553
1552 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1554 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1553
1555
1554 if fail_early:
1556 if fail_early:
1555 return merge_check
1557 return merge_check
1556
1558
1557 # merge possible
1559 # merge possible
1558 merge_status, msg = PullRequestModel().merge_status(pull_request)
1560 merge_status, msg = PullRequestModel().merge_status(pull_request)
1559 merge_check.merge_possible = merge_status
1561 merge_check.merge_possible = merge_status
1560 merge_check.merge_msg = msg
1562 merge_check.merge_msg = msg
1561 if not merge_status:
1563 if not merge_status:
1562 log.debug(
1564 log.debug(
1563 "MergeCheck: cannot merge, pull request merge not possible.")
1565 "MergeCheck: cannot merge, pull request merge not possible.")
1564 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1566 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1565
1567
1566 if fail_early:
1568 if fail_early:
1567 return merge_check
1569 return merge_check
1568
1570
1569 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1571 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1570 return merge_check
1572 return merge_check
1571
1573
1572 @classmethod
1574 @classmethod
1573 def get_merge_conditions(cls, pull_request):
1575 def get_merge_conditions(cls, pull_request):
1574 merge_details = {}
1576 merge_details = {}
1575
1577
1576 model = PullRequestModel()
1578 model = PullRequestModel()
1577 use_rebase = model._use_rebase_for_merging(pull_request)
1579 use_rebase = model._use_rebase_for_merging(pull_request)
1578
1580
1579 if use_rebase:
1581 if use_rebase:
1580 merge_details['merge_strategy'] = dict(
1582 merge_details['merge_strategy'] = dict(
1581 details={},
1583 details={},
1582 message=_('Merge strategy: rebase')
1584 message=_('Merge strategy: rebase')
1583 )
1585 )
1584 else:
1586 else:
1585 merge_details['merge_strategy'] = dict(
1587 merge_details['merge_strategy'] = dict(
1586 details={},
1588 details={},
1587 message=_('Merge strategy: explicit merge commit')
1589 message=_('Merge strategy: explicit merge commit')
1588 )
1590 )
1589
1591
1590 close_branch = model._close_branch_before_merging(pull_request)
1592 close_branch = model._close_branch_before_merging(pull_request)
1591 if close_branch:
1593 if close_branch:
1592 repo_type = pull_request.target_repo.repo_type
1594 repo_type = pull_request.target_repo.repo_type
1593 if repo_type == 'hg':
1595 if repo_type == 'hg':
1594 close_msg = _('Source branch will be closed after merge.')
1596 close_msg = _('Source branch will be closed after merge.')
1595 elif repo_type == 'git':
1597 elif repo_type == 'git':
1596 close_msg = _('Source branch will be deleted after merge.')
1598 close_msg = _('Source branch will be deleted after merge.')
1597
1599
1598 merge_details['close_branch'] = dict(
1600 merge_details['close_branch'] = dict(
1599 details={},
1601 details={},
1600 message=close_msg
1602 message=close_msg
1601 )
1603 )
1602
1604
1603 return merge_details
1605 return merge_details
1604
1606
1605 ChangeTuple = namedtuple('ChangeTuple',
1607 ChangeTuple = namedtuple('ChangeTuple',
1606 ['added', 'common', 'removed', 'total'])
1608 ['added', 'common', 'removed', 'total'])
1607
1609
1608 FileChangeTuple = namedtuple('FileChangeTuple',
1610 FileChangeTuple = namedtuple('FileChangeTuple',
1609 ['added', 'modified', 'removed'])
1611 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now