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