##// END OF EJS Templates
api: fix creation of PR with default reviewer rules.
marcink -
r2855:33ba3269 stable
parent child Browse files
Show More
@@ -1,1688 +1,1688 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 = list(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 Session().flush()
514
514
515 # Set approval status to "Under Review" for all commits which are
515 # Set approval status to "Under Review" for all commits which are
516 # part of this pull request.
516 # part of this pull request.
517 ChangesetStatusModel().set_status(
517 ChangesetStatusModel().set_status(
518 repo=target_repo,
518 repo=target_repo,
519 status=ChangesetStatus.STATUS_UNDER_REVIEW,
519 status=ChangesetStatus.STATUS_UNDER_REVIEW,
520 user=created_by_user,
520 user=created_by_user,
521 pull_request=pull_request
521 pull_request=pull_request
522 )
522 )
523 # we commit early at this point. This has to do with a fact
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
524 # that before queries do some row-locking. And because of that
525 # we need to commit and finish transation before below validate call
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
526 # that for large repos could be long resulting in long row locks
527 Session().commit()
527 Session().commit()
528
528
529 # prepare workspace, and run initial merge simulation
529 # prepare workspace, and run initial merge simulation
530 MergeCheck.validate(
530 MergeCheck.validate(
531 pull_request, user=created_by_user, translator=translator)
531 pull_request, user=created_by_user, translator=translator)
532
532
533 self.notify_reviewers(pull_request, reviewer_ids)
533 self.notify_reviewers(pull_request, reviewer_ids)
534 self._trigger_pull_request_hook(
534 self._trigger_pull_request_hook(
535 pull_request, created_by_user, 'create')
535 pull_request, created_by_user, 'create')
536
536
537 creation_data = pull_request.get_api_data(with_merge_state=False)
537 creation_data = pull_request.get_api_data(with_merge_state=False)
538 self._log_audit_action(
538 self._log_audit_action(
539 'repo.pull_request.create', {'data': creation_data},
539 'repo.pull_request.create', {'data': creation_data},
540 created_by_user, pull_request)
540 created_by_user, pull_request)
541
541
542 return pull_request
542 return pull_request
543
543
544 def _trigger_pull_request_hook(self, pull_request, user, action):
544 def _trigger_pull_request_hook(self, pull_request, user, action):
545 pull_request = self.__get_pull_request(pull_request)
545 pull_request = self.__get_pull_request(pull_request)
546 target_scm = pull_request.target_repo.scm_instance()
546 target_scm = pull_request.target_repo.scm_instance()
547 if action == 'create':
547 if action == 'create':
548 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
548 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
549 elif action == 'merge':
549 elif action == 'merge':
550 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
550 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
551 elif action == 'close':
551 elif action == 'close':
552 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
552 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
553 elif action == 'review_status_change':
553 elif action == 'review_status_change':
554 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
554 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
555 elif action == 'update':
555 elif action == 'update':
556 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
556 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
557 else:
557 else:
558 return
558 return
559
559
560 trigger_hook(
560 trigger_hook(
561 username=user.username,
561 username=user.username,
562 repo_name=pull_request.target_repo.repo_name,
562 repo_name=pull_request.target_repo.repo_name,
563 repo_alias=target_scm.alias,
563 repo_alias=target_scm.alias,
564 pull_request=pull_request)
564 pull_request=pull_request)
565
565
566 def _get_commit_ids(self, pull_request):
566 def _get_commit_ids(self, pull_request):
567 """
567 """
568 Return the commit ids of the merged pull request.
568 Return the commit ids of the merged pull request.
569
569
570 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
571 nor with the implicit target updates.
571 nor with the implicit target updates.
572 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
573 will be reported anyways.
573 will be reported anyways.
574 """
574 """
575 merge_rev = pull_request.merge_rev
575 merge_rev = pull_request.merge_rev
576 if merge_rev is None:
576 if merge_rev is None:
577 raise ValueError('This pull request was not merged yet')
577 raise ValueError('This pull request was not merged yet')
578
578
579 commit_ids = list(pull_request.revisions)
579 commit_ids = list(pull_request.revisions)
580 if merge_rev not in commit_ids:
580 if merge_rev not in commit_ids:
581 commit_ids.append(merge_rev)
581 commit_ids.append(merge_rev)
582
582
583 return commit_ids
583 return commit_ids
584
584
585 def merge(self, pull_request, user, extras):
585 def merge(self, pull_request, user, extras):
586 log.debug("Merging pull request %s", pull_request.pull_request_id)
586 log.debug("Merging pull request %s", pull_request.pull_request_id)
587 merge_state = self._merge_pull_request(pull_request, user, extras)
587 merge_state = self._merge_pull_request(pull_request, user, extras)
588 if merge_state.executed:
588 if merge_state.executed:
589 log.debug(
589 log.debug(
590 "Merge was successful, updating the pull request comments.")
590 "Merge was successful, updating the pull request comments.")
591 self._comment_and_close_pr(pull_request, user, merge_state)
591 self._comment_and_close_pr(pull_request, user, merge_state)
592
592
593 self._log_audit_action(
593 self._log_audit_action(
594 'repo.pull_request.merge',
594 'repo.pull_request.merge',
595 {'merge_state': merge_state.__dict__},
595 {'merge_state': merge_state.__dict__},
596 user, pull_request)
596 user, pull_request)
597
597
598 else:
598 else:
599 log.warn("Merge failed, not updating the pull request.")
599 log.warn("Merge failed, not updating the pull request.")
600 return merge_state
600 return merge_state
601
601
602 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):
603 target_vcs = pull_request.target_repo.scm_instance()
603 target_vcs = pull_request.target_repo.scm_instance()
604 source_vcs = pull_request.source_repo.scm_instance()
604 source_vcs = pull_request.source_repo.scm_instance()
605 target_ref = self._refresh_reference(
605 target_ref = self._refresh_reference(
606 pull_request.target_ref_parts, target_vcs)
606 pull_request.target_ref_parts, target_vcs)
607
607
608 message = merge_msg or (
608 message = merge_msg or (
609 'Merge pull request #%(pr_id)s from '
609 'Merge pull request #%(pr_id)s from '
610 '%(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') % {
611 'pr_id': pull_request.pull_request_id,
611 'pr_id': pull_request.pull_request_id,
612 'source_repo': source_vcs.name,
612 'source_repo': source_vcs.name,
613 'source_ref_name': pull_request.source_ref_parts.name,
613 'source_ref_name': pull_request.source_ref_parts.name,
614 'pr_title': pull_request.title
614 'pr_title': pull_request.title
615 }
615 }
616
616
617 workspace_id = self._workspace_id(pull_request)
617 workspace_id = self._workspace_id(pull_request)
618 use_rebase = self._use_rebase_for_merging(pull_request)
618 use_rebase = self._use_rebase_for_merging(pull_request)
619 close_branch = self._close_branch_before_merging(pull_request)
619 close_branch = self._close_branch_before_merging(pull_request)
620
620
621 callback_daemon, extras = prepare_callback_daemon(
621 callback_daemon, extras = prepare_callback_daemon(
622 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
622 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
623 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
623 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
624
624
625 with callback_daemon:
625 with callback_daemon:
626 # TODO: johbo: Implement a clean way to run a config_override
626 # TODO: johbo: Implement a clean way to run a config_override
627 # for a single call.
627 # for a single call.
628 target_vcs.config.set(
628 target_vcs.config.set(
629 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
629 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
630 merge_state = target_vcs.merge(
630 merge_state = target_vcs.merge(
631 target_ref, source_vcs, pull_request.source_ref_parts,
631 target_ref, source_vcs, pull_request.source_ref_parts,
632 workspace_id, user_name=user.username,
632 workspace_id, user_name=user.username,
633 user_email=user.email, message=message, use_rebase=use_rebase,
633 user_email=user.email, message=message, use_rebase=use_rebase,
634 close_branch=close_branch)
634 close_branch=close_branch)
635 return merge_state
635 return merge_state
636
636
637 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):
638 pull_request.merge_rev = merge_state.merge_ref.commit_id
638 pull_request.merge_rev = merge_state.merge_ref.commit_id
639 pull_request.updated_on = datetime.datetime.now()
639 pull_request.updated_on = datetime.datetime.now()
640 close_msg = close_msg or 'Pull request merged and closed'
640 close_msg = close_msg or 'Pull request merged and closed'
641
641
642 CommentsModel().create(
642 CommentsModel().create(
643 text=safe_unicode(close_msg),
643 text=safe_unicode(close_msg),
644 repo=pull_request.target_repo.repo_id,
644 repo=pull_request.target_repo.repo_id,
645 user=user.user_id,
645 user=user.user_id,
646 pull_request=pull_request.pull_request_id,
646 pull_request=pull_request.pull_request_id,
647 f_path=None,
647 f_path=None,
648 line_no=None,
648 line_no=None,
649 closing_pr=True
649 closing_pr=True
650 )
650 )
651
651
652 Session().add(pull_request)
652 Session().add(pull_request)
653 Session().flush()
653 Session().flush()
654 # TODO: paris: replace invalidation with less radical solution
654 # TODO: paris: replace invalidation with less radical solution
655 ScmModel().mark_for_invalidation(
655 ScmModel().mark_for_invalidation(
656 pull_request.target_repo.repo_name)
656 pull_request.target_repo.repo_name)
657 self._trigger_pull_request_hook(pull_request, user, 'merge')
657 self._trigger_pull_request_hook(pull_request, user, 'merge')
658
658
659 def has_valid_update_type(self, pull_request):
659 def has_valid_update_type(self, pull_request):
660 source_ref_type = pull_request.source_ref_parts.type
660 source_ref_type = pull_request.source_ref_parts.type
661 return source_ref_type in ['book', 'branch', 'tag']
661 return source_ref_type in ['book', 'branch', 'tag']
662
662
663 def update_commits(self, pull_request):
663 def update_commits(self, pull_request):
664 """
664 """
665 Get the updated list of commits for the pull request
665 Get the updated list of commits for the pull request
666 and return the new pull request version and the list
666 and return the new pull request version and the list
667 of commits processed by this update action
667 of commits processed by this update action
668 """
668 """
669 pull_request = self.__get_pull_request(pull_request)
669 pull_request = self.__get_pull_request(pull_request)
670 source_ref_type = pull_request.source_ref_parts.type
670 source_ref_type = pull_request.source_ref_parts.type
671 source_ref_name = pull_request.source_ref_parts.name
671 source_ref_name = pull_request.source_ref_parts.name
672 source_ref_id = pull_request.source_ref_parts.commit_id
672 source_ref_id = pull_request.source_ref_parts.commit_id
673
673
674 target_ref_type = pull_request.target_ref_parts.type
674 target_ref_type = pull_request.target_ref_parts.type
675 target_ref_name = pull_request.target_ref_parts.name
675 target_ref_name = pull_request.target_ref_parts.name
676 target_ref_id = pull_request.target_ref_parts.commit_id
676 target_ref_id = pull_request.target_ref_parts.commit_id
677
677
678 if not self.has_valid_update_type(pull_request):
678 if not self.has_valid_update_type(pull_request):
679 log.debug(
679 log.debug(
680 "Skipping update of pull request %s due to ref type: %s",
680 "Skipping update of pull request %s due to ref type: %s",
681 pull_request, source_ref_type)
681 pull_request, source_ref_type)
682 return UpdateResponse(
682 return UpdateResponse(
683 executed=False,
683 executed=False,
684 reason=UpdateFailureReason.WRONG_REF_TYPE,
684 reason=UpdateFailureReason.WRONG_REF_TYPE,
685 old=pull_request, new=None, changes=None,
685 old=pull_request, new=None, changes=None,
686 source_changed=False, target_changed=False)
686 source_changed=False, target_changed=False)
687
687
688 # source repo
688 # source repo
689 source_repo = pull_request.source_repo.scm_instance()
689 source_repo = pull_request.source_repo.scm_instance()
690 try:
690 try:
691 source_commit = source_repo.get_commit(commit_id=source_ref_name)
691 source_commit = source_repo.get_commit(commit_id=source_ref_name)
692 except CommitDoesNotExistError:
692 except CommitDoesNotExistError:
693 return UpdateResponse(
693 return UpdateResponse(
694 executed=False,
694 executed=False,
695 reason=UpdateFailureReason.MISSING_SOURCE_REF,
695 reason=UpdateFailureReason.MISSING_SOURCE_REF,
696 old=pull_request, new=None, changes=None,
696 old=pull_request, new=None, changes=None,
697 source_changed=False, target_changed=False)
697 source_changed=False, target_changed=False)
698
698
699 source_changed = source_ref_id != source_commit.raw_id
699 source_changed = source_ref_id != source_commit.raw_id
700
700
701 # target repo
701 # target repo
702 target_repo = pull_request.target_repo.scm_instance()
702 target_repo = pull_request.target_repo.scm_instance()
703 try:
703 try:
704 target_commit = target_repo.get_commit(commit_id=target_ref_name)
704 target_commit = target_repo.get_commit(commit_id=target_ref_name)
705 except CommitDoesNotExistError:
705 except CommitDoesNotExistError:
706 return UpdateResponse(
706 return UpdateResponse(
707 executed=False,
707 executed=False,
708 reason=UpdateFailureReason.MISSING_TARGET_REF,
708 reason=UpdateFailureReason.MISSING_TARGET_REF,
709 old=pull_request, new=None, changes=None,
709 old=pull_request, new=None, changes=None,
710 source_changed=False, target_changed=False)
710 source_changed=False, target_changed=False)
711 target_changed = target_ref_id != target_commit.raw_id
711 target_changed = target_ref_id != target_commit.raw_id
712
712
713 if not (source_changed or target_changed):
713 if not (source_changed or target_changed):
714 log.debug("Nothing changed in pull request %s", pull_request)
714 log.debug("Nothing changed in pull request %s", pull_request)
715 return UpdateResponse(
715 return UpdateResponse(
716 executed=False,
716 executed=False,
717 reason=UpdateFailureReason.NO_CHANGE,
717 reason=UpdateFailureReason.NO_CHANGE,
718 old=pull_request, new=None, changes=None,
718 old=pull_request, new=None, changes=None,
719 source_changed=target_changed, target_changed=source_changed)
719 source_changed=target_changed, target_changed=source_changed)
720
720
721 change_in_found = 'target repo' if target_changed else 'source repo'
721 change_in_found = 'target repo' if target_changed else 'source repo'
722 log.debug('Updating pull request because of change in %s detected',
722 log.debug('Updating pull request because of change in %s detected',
723 change_in_found)
723 change_in_found)
724
724
725 # 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
726 # we create a new version, else just an update
726 # we create a new version, else just an update
727 if source_changed:
727 if source_changed:
728 pull_request_version = self._create_version_from_snapshot(pull_request)
728 pull_request_version = self._create_version_from_snapshot(pull_request)
729 self._link_comments_to_version(pull_request_version)
729 self._link_comments_to_version(pull_request_version)
730 else:
730 else:
731 try:
731 try:
732 ver = pull_request.versions[-1]
732 ver = pull_request.versions[-1]
733 except IndexError:
733 except IndexError:
734 ver = None
734 ver = None
735
735
736 pull_request.pull_request_version_id = \
736 pull_request.pull_request_version_id = \
737 ver.pull_request_version_id if ver else None
737 ver.pull_request_version_id if ver else None
738 pull_request_version = pull_request
738 pull_request_version = pull_request
739
739
740 try:
740 try:
741 if target_ref_type in ('tag', 'branch', 'book'):
741 if target_ref_type in ('tag', 'branch', 'book'):
742 target_commit = target_repo.get_commit(target_ref_name)
742 target_commit = target_repo.get_commit(target_ref_name)
743 else:
743 else:
744 target_commit = target_repo.get_commit(target_ref_id)
744 target_commit = target_repo.get_commit(target_ref_id)
745 except CommitDoesNotExistError:
745 except CommitDoesNotExistError:
746 return UpdateResponse(
746 return UpdateResponse(
747 executed=False,
747 executed=False,
748 reason=UpdateFailureReason.MISSING_TARGET_REF,
748 reason=UpdateFailureReason.MISSING_TARGET_REF,
749 old=pull_request, new=None, changes=None,
749 old=pull_request, new=None, changes=None,
750 source_changed=source_changed, target_changed=target_changed)
750 source_changed=source_changed, target_changed=target_changed)
751
751
752 # re-compute commit ids
752 # re-compute commit ids
753 old_commit_ids = pull_request.revisions
753 old_commit_ids = pull_request.revisions
754 pre_load = ["author", "branch", "date", "message"]
754 pre_load = ["author", "branch", "date", "message"]
755 commit_ranges = target_repo.compare(
755 commit_ranges = target_repo.compare(
756 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,
757 pre_load=pre_load)
757 pre_load=pre_load)
758
758
759 ancestor = target_repo.get_common_ancestor(
759 ancestor = target_repo.get_common_ancestor(
760 target_commit.raw_id, source_commit.raw_id, source_repo)
760 target_commit.raw_id, source_commit.raw_id, source_repo)
761
761
762 pull_request.source_ref = '%s:%s:%s' % (
762 pull_request.source_ref = '%s:%s:%s' % (
763 source_ref_type, source_ref_name, source_commit.raw_id)
763 source_ref_type, source_ref_name, source_commit.raw_id)
764 pull_request.target_ref = '%s:%s:%s' % (
764 pull_request.target_ref = '%s:%s:%s' % (
765 target_ref_type, target_ref_name, ancestor)
765 target_ref_type, target_ref_name, ancestor)
766
766
767 pull_request.revisions = [
767 pull_request.revisions = [
768 commit.raw_id for commit in reversed(commit_ranges)]
768 commit.raw_id for commit in reversed(commit_ranges)]
769 pull_request.updated_on = datetime.datetime.now()
769 pull_request.updated_on = datetime.datetime.now()
770 Session().add(pull_request)
770 Session().add(pull_request)
771 new_commit_ids = pull_request.revisions
771 new_commit_ids = pull_request.revisions
772
772
773 old_diff_data, new_diff_data = self._generate_update_diffs(
773 old_diff_data, new_diff_data = self._generate_update_diffs(
774 pull_request, pull_request_version)
774 pull_request, pull_request_version)
775
775
776 # calculate commit and file changes
776 # calculate commit and file changes
777 changes = self._calculate_commit_id_changes(
777 changes = self._calculate_commit_id_changes(
778 old_commit_ids, new_commit_ids)
778 old_commit_ids, new_commit_ids)
779 file_changes = self._calculate_file_changes(
779 file_changes = self._calculate_file_changes(
780 old_diff_data, new_diff_data)
780 old_diff_data, new_diff_data)
781
781
782 # set comments as outdated if DIFFS changed
782 # set comments as outdated if DIFFS changed
783 CommentsModel().outdate_comments(
783 CommentsModel().outdate_comments(
784 pull_request, old_diff_data=old_diff_data,
784 pull_request, old_diff_data=old_diff_data,
785 new_diff_data=new_diff_data)
785 new_diff_data=new_diff_data)
786
786
787 commit_changes = (changes.added or changes.removed)
787 commit_changes = (changes.added or changes.removed)
788 file_node_changes = (
788 file_node_changes = (
789 file_changes.added or file_changes.modified or file_changes.removed)
789 file_changes.added or file_changes.modified or file_changes.removed)
790 pr_has_changes = commit_changes or file_node_changes
790 pr_has_changes = commit_changes or file_node_changes
791
791
792 # Add an automatic comment to the pull request, in case
792 # Add an automatic comment to the pull request, in case
793 # anything has changed
793 # anything has changed
794 if pr_has_changes:
794 if pr_has_changes:
795 update_comment = CommentsModel().create(
795 update_comment = CommentsModel().create(
796 text=self._render_update_message(changes, file_changes),
796 text=self._render_update_message(changes, file_changes),
797 repo=pull_request.target_repo,
797 repo=pull_request.target_repo,
798 user=pull_request.author,
798 user=pull_request.author,
799 pull_request=pull_request,
799 pull_request=pull_request,
800 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
800 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
801
801
802 # Update status to "Under Review" for added commits
802 # Update status to "Under Review" for added commits
803 for commit_id in changes.added:
803 for commit_id in changes.added:
804 ChangesetStatusModel().set_status(
804 ChangesetStatusModel().set_status(
805 repo=pull_request.source_repo,
805 repo=pull_request.source_repo,
806 status=ChangesetStatus.STATUS_UNDER_REVIEW,
806 status=ChangesetStatus.STATUS_UNDER_REVIEW,
807 comment=update_comment,
807 comment=update_comment,
808 user=pull_request.author,
808 user=pull_request.author,
809 pull_request=pull_request,
809 pull_request=pull_request,
810 revision=commit_id)
810 revision=commit_id)
811
811
812 log.debug(
812 log.debug(
813 'Updated pull request %s, added_ids: %s, common_ids: %s, '
813 'Updated pull request %s, added_ids: %s, common_ids: %s, '
814 'removed_ids: %s', pull_request.pull_request_id,
814 'removed_ids: %s', pull_request.pull_request_id,
815 changes.added, changes.common, changes.removed)
815 changes.added, changes.common, changes.removed)
816 log.debug(
816 log.debug(
817 'Updated pull request with the following file changes: %s',
817 'Updated pull request with the following file changes: %s',
818 file_changes)
818 file_changes)
819
819
820 log.info(
820 log.info(
821 "Updated pull request %s from commit %s to commit %s, "
821 "Updated pull request %s from commit %s to commit %s, "
822 "stored new version %s of this pull request.",
822 "stored new version %s of this pull request.",
823 pull_request.pull_request_id, source_ref_id,
823 pull_request.pull_request_id, source_ref_id,
824 pull_request.source_ref_parts.commit_id,
824 pull_request.source_ref_parts.commit_id,
825 pull_request_version.pull_request_version_id)
825 pull_request_version.pull_request_version_id)
826 Session().commit()
826 Session().commit()
827 self._trigger_pull_request_hook(
827 self._trigger_pull_request_hook(
828 pull_request, pull_request.author, 'update')
828 pull_request, pull_request.author, 'update')
829
829
830 return UpdateResponse(
830 return UpdateResponse(
831 executed=True, reason=UpdateFailureReason.NONE,
831 executed=True, reason=UpdateFailureReason.NONE,
832 old=pull_request, new=pull_request_version, changes=changes,
832 old=pull_request, new=pull_request_version, changes=changes,
833 source_changed=source_changed, target_changed=target_changed)
833 source_changed=source_changed, target_changed=target_changed)
834
834
835 def _create_version_from_snapshot(self, pull_request):
835 def _create_version_from_snapshot(self, pull_request):
836 version = PullRequestVersion()
836 version = PullRequestVersion()
837 version.title = pull_request.title
837 version.title = pull_request.title
838 version.description = pull_request.description
838 version.description = pull_request.description
839 version.status = pull_request.status
839 version.status = pull_request.status
840 version.created_on = datetime.datetime.now()
840 version.created_on = datetime.datetime.now()
841 version.updated_on = pull_request.updated_on
841 version.updated_on = pull_request.updated_on
842 version.user_id = pull_request.user_id
842 version.user_id = pull_request.user_id
843 version.source_repo = pull_request.source_repo
843 version.source_repo = pull_request.source_repo
844 version.source_ref = pull_request.source_ref
844 version.source_ref = pull_request.source_ref
845 version.target_repo = pull_request.target_repo
845 version.target_repo = pull_request.target_repo
846 version.target_ref = pull_request.target_ref
846 version.target_ref = pull_request.target_ref
847
847
848 version._last_merge_source_rev = pull_request._last_merge_source_rev
848 version._last_merge_source_rev = pull_request._last_merge_source_rev
849 version._last_merge_target_rev = pull_request._last_merge_target_rev
849 version._last_merge_target_rev = pull_request._last_merge_target_rev
850 version.last_merge_status = pull_request.last_merge_status
850 version.last_merge_status = pull_request.last_merge_status
851 version.shadow_merge_ref = pull_request.shadow_merge_ref
851 version.shadow_merge_ref = pull_request.shadow_merge_ref
852 version.merge_rev = pull_request.merge_rev
852 version.merge_rev = pull_request.merge_rev
853 version.reviewer_data = pull_request.reviewer_data
853 version.reviewer_data = pull_request.reviewer_data
854
854
855 version.revisions = pull_request.revisions
855 version.revisions = pull_request.revisions
856 version.pull_request = pull_request
856 version.pull_request = pull_request
857 Session().add(version)
857 Session().add(version)
858 Session().flush()
858 Session().flush()
859
859
860 return version
860 return version
861
861
862 def _generate_update_diffs(self, pull_request, pull_request_version):
862 def _generate_update_diffs(self, pull_request, pull_request_version):
863
863
864 diff_context = (
864 diff_context = (
865 self.DIFF_CONTEXT +
865 self.DIFF_CONTEXT +
866 CommentsModel.needed_extra_diff_context())
866 CommentsModel.needed_extra_diff_context())
867
867
868 source_repo = pull_request_version.source_repo
868 source_repo = pull_request_version.source_repo
869 source_ref_id = pull_request_version.source_ref_parts.commit_id
869 source_ref_id = pull_request_version.source_ref_parts.commit_id
870 target_ref_id = pull_request_version.target_ref_parts.commit_id
870 target_ref_id = pull_request_version.target_ref_parts.commit_id
871 old_diff = self._get_diff_from_pr_or_version(
871 old_diff = self._get_diff_from_pr_or_version(
872 source_repo, source_ref_id, target_ref_id, context=diff_context)
872 source_repo, source_ref_id, target_ref_id, context=diff_context)
873
873
874 source_repo = pull_request.source_repo
874 source_repo = pull_request.source_repo
875 source_ref_id = pull_request.source_ref_parts.commit_id
875 source_ref_id = pull_request.source_ref_parts.commit_id
876 target_ref_id = pull_request.target_ref_parts.commit_id
876 target_ref_id = pull_request.target_ref_parts.commit_id
877
877
878 new_diff = self._get_diff_from_pr_or_version(
878 new_diff = self._get_diff_from_pr_or_version(
879 source_repo, source_ref_id, target_ref_id, context=diff_context)
879 source_repo, source_ref_id, target_ref_id, context=diff_context)
880
880
881 old_diff_data = diffs.DiffProcessor(old_diff)
881 old_diff_data = diffs.DiffProcessor(old_diff)
882 old_diff_data.prepare()
882 old_diff_data.prepare()
883 new_diff_data = diffs.DiffProcessor(new_diff)
883 new_diff_data = diffs.DiffProcessor(new_diff)
884 new_diff_data.prepare()
884 new_diff_data.prepare()
885
885
886 return old_diff_data, new_diff_data
886 return old_diff_data, new_diff_data
887
887
888 def _link_comments_to_version(self, pull_request_version):
888 def _link_comments_to_version(self, pull_request_version):
889 """
889 """
890 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.
891
891
892 :param pull_request_version: The `PullRequestVersion` to which
892 :param pull_request_version: The `PullRequestVersion` to which
893 the comments shall be linked.
893 the comments shall be linked.
894
894
895 """
895 """
896 pull_request = pull_request_version.pull_request
896 pull_request = pull_request_version.pull_request
897 comments = ChangesetComment.query()\
897 comments = ChangesetComment.query()\
898 .filter(
898 .filter(
899 # TODO: johbo: Should we query for the repo at all here?
899 # TODO: johbo: Should we query for the repo at all here?
900 # Pending decision on how comments of PRs are to be related
900 # Pending decision on how comments of PRs are to be related
901 # 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.
902 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
902 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
903 ChangesetComment.pull_request == pull_request,
903 ChangesetComment.pull_request == pull_request,
904 ChangesetComment.pull_request_version == None)\
904 ChangesetComment.pull_request_version == None)\
905 .order_by(ChangesetComment.comment_id.asc())
905 .order_by(ChangesetComment.comment_id.asc())
906
906
907 # 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
908 # operation.
908 # operation.
909 for comment in comments:
909 for comment in comments:
910 comment.pull_request_version_id = (
910 comment.pull_request_version_id = (
911 pull_request_version.pull_request_version_id)
911 pull_request_version.pull_request_version_id)
912 Session().add(comment)
912 Session().add(comment)
913
913
914 def _calculate_commit_id_changes(self, old_ids, new_ids):
914 def _calculate_commit_id_changes(self, old_ids, new_ids):
915 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]
916 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]
917 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]
918 total = new_ids
918 total = new_ids
919 return ChangeTuple(added, common, removed, total)
919 return ChangeTuple(added, common, removed, total)
920
920
921 def _calculate_file_changes(self, old_diff_data, new_diff_data):
921 def _calculate_file_changes(self, old_diff_data, new_diff_data):
922
922
923 old_files = OrderedDict()
923 old_files = OrderedDict()
924 for diff_data in old_diff_data.parsed_diff:
924 for diff_data in old_diff_data.parsed_diff:
925 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
925 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
926
926
927 added_files = []
927 added_files = []
928 modified_files = []
928 modified_files = []
929 removed_files = []
929 removed_files = []
930 for diff_data in new_diff_data.parsed_diff:
930 for diff_data in new_diff_data.parsed_diff:
931 new_filename = diff_data['filename']
931 new_filename = diff_data['filename']
932 new_hash = md5_safe(diff_data['raw_diff'])
932 new_hash = md5_safe(diff_data['raw_diff'])
933
933
934 old_hash = old_files.get(new_filename)
934 old_hash = old_files.get(new_filename)
935 if not old_hash:
935 if not old_hash:
936 # file is not present in old diff, means it's added
936 # file is not present in old diff, means it's added
937 added_files.append(new_filename)
937 added_files.append(new_filename)
938 else:
938 else:
939 if new_hash != old_hash:
939 if new_hash != old_hash:
940 modified_files.append(new_filename)
940 modified_files.append(new_filename)
941 # 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
942 del old_files[new_filename]
942 del old_files[new_filename]
943
943
944 # 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,
945 # 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
946 # if any should be the removed files
946 # if any should be the removed files
947 removed_files.extend(old_files.keys())
947 removed_files.extend(old_files.keys())
948
948
949 return FileChangeTuple(added_files, modified_files, removed_files)
949 return FileChangeTuple(added_files, modified_files, removed_files)
950
950
951 def _render_update_message(self, changes, file_changes):
951 def _render_update_message(self, changes, file_changes):
952 """
952 """
953 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
953 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
954 so it's always looking the same disregarding on which default
954 so it's always looking the same disregarding on which default
955 renderer system is using.
955 renderer system is using.
956
956
957 :param changes: changes named tuple
957 :param changes: changes named tuple
958 :param file_changes: file changes named tuple
958 :param file_changes: file changes named tuple
959
959
960 """
960 """
961 new_status = ChangesetStatus.get_status_lbl(
961 new_status = ChangesetStatus.get_status_lbl(
962 ChangesetStatus.STATUS_UNDER_REVIEW)
962 ChangesetStatus.STATUS_UNDER_REVIEW)
963
963
964 changed_files = (
964 changed_files = (
965 file_changes.added + file_changes.modified + file_changes.removed)
965 file_changes.added + file_changes.modified + file_changes.removed)
966
966
967 params = {
967 params = {
968 'under_review_label': new_status,
968 'under_review_label': new_status,
969 'added_commits': changes.added,
969 'added_commits': changes.added,
970 'removed_commits': changes.removed,
970 'removed_commits': changes.removed,
971 'changed_files': changed_files,
971 'changed_files': changed_files,
972 'added_files': file_changes.added,
972 'added_files': file_changes.added,
973 'modified_files': file_changes.modified,
973 'modified_files': file_changes.modified,
974 'removed_files': file_changes.removed,
974 'removed_files': file_changes.removed,
975 }
975 }
976 renderer = RstTemplateRenderer()
976 renderer = RstTemplateRenderer()
977 return renderer.render('pull_request_update.mako', **params)
977 return renderer.render('pull_request_update.mako', **params)
978
978
979 def edit(self, pull_request, title, description, user):
979 def edit(self, pull_request, title, description, user):
980 pull_request = self.__get_pull_request(pull_request)
980 pull_request = self.__get_pull_request(pull_request)
981 old_data = pull_request.get_api_data(with_merge_state=False)
981 old_data = pull_request.get_api_data(with_merge_state=False)
982 if pull_request.is_closed():
982 if pull_request.is_closed():
983 raise ValueError('This pull request is closed')
983 raise ValueError('This pull request is closed')
984 if title:
984 if title:
985 pull_request.title = title
985 pull_request.title = title
986 pull_request.description = description
986 pull_request.description = description
987 pull_request.updated_on = datetime.datetime.now()
987 pull_request.updated_on = datetime.datetime.now()
988 Session().add(pull_request)
988 Session().add(pull_request)
989 self._log_audit_action(
989 self._log_audit_action(
990 'repo.pull_request.edit', {'old_data': old_data},
990 'repo.pull_request.edit', {'old_data': old_data},
991 user, pull_request)
991 user, pull_request)
992
992
993 def update_reviewers(self, pull_request, reviewer_data, user):
993 def update_reviewers(self, pull_request, reviewer_data, user):
994 """
994 """
995 Update the reviewers in the pull request
995 Update the reviewers in the pull request
996
996
997 :param pull_request: the pr to update
997 :param pull_request: the pr to update
998 :param reviewer_data: list of tuples
998 :param reviewer_data: list of tuples
999 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
999 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1000 """
1000 """
1001 pull_request = self.__get_pull_request(pull_request)
1001 pull_request = self.__get_pull_request(pull_request)
1002 if pull_request.is_closed():
1002 if pull_request.is_closed():
1003 raise ValueError('This pull request is closed')
1003 raise ValueError('This pull request is closed')
1004
1004
1005 reviewers = {}
1005 reviewers = {}
1006 for user_id, reasons, mandatory, rules in reviewer_data:
1006 for user_id, reasons, mandatory, rules in reviewer_data:
1007 if isinstance(user_id, (int, basestring)):
1007 if isinstance(user_id, (int, basestring)):
1008 user_id = self._get_user(user_id).user_id
1008 user_id = self._get_user(user_id).user_id
1009 reviewers[user_id] = {
1009 reviewers[user_id] = {
1010 'reasons': reasons, 'mandatory': mandatory}
1010 'reasons': reasons, 'mandatory': mandatory}
1011
1011
1012 reviewers_ids = set(reviewers.keys())
1012 reviewers_ids = set(reviewers.keys())
1013 current_reviewers = PullRequestReviewers.query()\
1013 current_reviewers = PullRequestReviewers.query()\
1014 .filter(PullRequestReviewers.pull_request ==
1014 .filter(PullRequestReviewers.pull_request ==
1015 pull_request).all()
1015 pull_request).all()
1016 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])
1017
1017
1018 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1018 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1019 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1019 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1020
1020
1021 log.debug("Adding %s reviewers", ids_to_add)
1021 log.debug("Adding %s reviewers", ids_to_add)
1022 log.debug("Removing %s reviewers", ids_to_remove)
1022 log.debug("Removing %s reviewers", ids_to_remove)
1023 changed = False
1023 changed = False
1024 for uid in ids_to_add:
1024 for uid in ids_to_add:
1025 changed = True
1025 changed = True
1026 _usr = self._get_user(uid)
1026 _usr = self._get_user(uid)
1027 reviewer = PullRequestReviewers()
1027 reviewer = PullRequestReviewers()
1028 reviewer.user = _usr
1028 reviewer.user = _usr
1029 reviewer.pull_request = pull_request
1029 reviewer.pull_request = pull_request
1030 reviewer.reasons = reviewers[uid]['reasons']
1030 reviewer.reasons = reviewers[uid]['reasons']
1031 # NOTE(marcink): mandatory shouldn't be changed now
1031 # NOTE(marcink): mandatory shouldn't be changed now
1032 # reviewer.mandatory = reviewers[uid]['reasons']
1032 # reviewer.mandatory = reviewers[uid]['reasons']
1033 Session().add(reviewer)
1033 Session().add(reviewer)
1034 self._log_audit_action(
1034 self._log_audit_action(
1035 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1035 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1036 user, pull_request)
1036 user, pull_request)
1037
1037
1038 for uid in ids_to_remove:
1038 for uid in ids_to_remove:
1039 changed = True
1039 changed = True
1040 reviewers = PullRequestReviewers.query()\
1040 reviewers = PullRequestReviewers.query()\
1041 .filter(PullRequestReviewers.user_id == uid,
1041 .filter(PullRequestReviewers.user_id == uid,
1042 PullRequestReviewers.pull_request == pull_request)\
1042 PullRequestReviewers.pull_request == pull_request)\
1043 .all()
1043 .all()
1044 # use .all() in case we accidentally added the same person twice
1044 # use .all() in case we accidentally added the same person twice
1045 # this CAN happen due to the lack of DB checks
1045 # this CAN happen due to the lack of DB checks
1046 for obj in reviewers:
1046 for obj in reviewers:
1047 old_data = obj.get_dict()
1047 old_data = obj.get_dict()
1048 Session().delete(obj)
1048 Session().delete(obj)
1049 self._log_audit_action(
1049 self._log_audit_action(
1050 'repo.pull_request.reviewer.delete',
1050 'repo.pull_request.reviewer.delete',
1051 {'old_data': old_data}, user, pull_request)
1051 {'old_data': old_data}, user, pull_request)
1052
1052
1053 if changed:
1053 if changed:
1054 pull_request.updated_on = datetime.datetime.now()
1054 pull_request.updated_on = datetime.datetime.now()
1055 Session().add(pull_request)
1055 Session().add(pull_request)
1056
1056
1057 self.notify_reviewers(pull_request, ids_to_add)
1057 self.notify_reviewers(pull_request, ids_to_add)
1058 return ids_to_add, ids_to_remove
1058 return ids_to_add, ids_to_remove
1059
1059
1060 def get_url(self, pull_request, request=None, permalink=False):
1060 def get_url(self, pull_request, request=None, permalink=False):
1061 if not request:
1061 if not request:
1062 request = get_current_request()
1062 request = get_current_request()
1063
1063
1064 if permalink:
1064 if permalink:
1065 return request.route_url(
1065 return request.route_url(
1066 'pull_requests_global',
1066 'pull_requests_global',
1067 pull_request_id=pull_request.pull_request_id,)
1067 pull_request_id=pull_request.pull_request_id,)
1068 else:
1068 else:
1069 return request.route_url('pullrequest_show',
1069 return request.route_url('pullrequest_show',
1070 repo_name=safe_str(pull_request.target_repo.repo_name),
1070 repo_name=safe_str(pull_request.target_repo.repo_name),
1071 pull_request_id=pull_request.pull_request_id,)
1071 pull_request_id=pull_request.pull_request_id,)
1072
1072
1073 def get_shadow_clone_url(self, pull_request, request=None):
1073 def get_shadow_clone_url(self, pull_request, request=None):
1074 """
1074 """
1075 Returns qualified url pointing to the shadow repository. If this pull
1075 Returns qualified url pointing to the shadow repository. If this pull
1076 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
1077 returned.
1077 returned.
1078 """
1078 """
1079 if pull_request.is_closed():
1079 if pull_request.is_closed():
1080 return None
1080 return None
1081 else:
1081 else:
1082 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1082 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1083 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1083 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1084
1084
1085 def notify_reviewers(self, pull_request, reviewers_ids):
1085 def notify_reviewers(self, pull_request, reviewers_ids):
1086 # notification to reviewers
1086 # notification to reviewers
1087 if not reviewers_ids:
1087 if not reviewers_ids:
1088 return
1088 return
1089
1089
1090 pull_request_obj = pull_request
1090 pull_request_obj = pull_request
1091 # get the current participants of this pull request
1091 # get the current participants of this pull request
1092 recipients = reviewers_ids
1092 recipients = reviewers_ids
1093 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1093 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1094
1094
1095 pr_source_repo = pull_request_obj.source_repo
1095 pr_source_repo = pull_request_obj.source_repo
1096 pr_target_repo = pull_request_obj.target_repo
1096 pr_target_repo = pull_request_obj.target_repo
1097
1097
1098 pr_url = h.route_url('pullrequest_show',
1098 pr_url = h.route_url('pullrequest_show',
1099 repo_name=pr_target_repo.repo_name,
1099 repo_name=pr_target_repo.repo_name,
1100 pull_request_id=pull_request_obj.pull_request_id,)
1100 pull_request_id=pull_request_obj.pull_request_id,)
1101
1101
1102 # set some variables for email notification
1102 # set some variables for email notification
1103 pr_target_repo_url = h.route_url(
1103 pr_target_repo_url = h.route_url(
1104 'repo_summary', repo_name=pr_target_repo.repo_name)
1104 'repo_summary', repo_name=pr_target_repo.repo_name)
1105
1105
1106 pr_source_repo_url = h.route_url(
1106 pr_source_repo_url = h.route_url(
1107 'repo_summary', repo_name=pr_source_repo.repo_name)
1107 'repo_summary', repo_name=pr_source_repo.repo_name)
1108
1108
1109 # pull request specifics
1109 # pull request specifics
1110 pull_request_commits = [
1110 pull_request_commits = [
1111 (x.raw_id, x.message)
1111 (x.raw_id, x.message)
1112 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)]
1113
1113
1114 kwargs = {
1114 kwargs = {
1115 'user': pull_request.author,
1115 'user': pull_request.author,
1116 'pull_request': pull_request_obj,
1116 'pull_request': pull_request_obj,
1117 'pull_request_commits': pull_request_commits,
1117 'pull_request_commits': pull_request_commits,
1118
1118
1119 'pull_request_target_repo': pr_target_repo,
1119 'pull_request_target_repo': pr_target_repo,
1120 'pull_request_target_repo_url': pr_target_repo_url,
1120 'pull_request_target_repo_url': pr_target_repo_url,
1121
1121
1122 'pull_request_source_repo': pr_source_repo,
1122 'pull_request_source_repo': pr_source_repo,
1123 'pull_request_source_repo_url': pr_source_repo_url,
1123 'pull_request_source_repo_url': pr_source_repo_url,
1124
1124
1125 'pull_request_url': pr_url,
1125 'pull_request_url': pr_url,
1126 }
1126 }
1127
1127
1128 # pre-generate the subject for notification itself
1128 # pre-generate the subject for notification itself
1129 (subject,
1129 (subject,
1130 _h, _e, # we don't care about those
1130 _h, _e, # we don't care about those
1131 body_plaintext) = EmailNotificationModel().render_email(
1131 body_plaintext) = EmailNotificationModel().render_email(
1132 notification_type, **kwargs)
1132 notification_type, **kwargs)
1133
1133
1134 # create notification objects, and emails
1134 # create notification objects, and emails
1135 NotificationModel().create(
1135 NotificationModel().create(
1136 created_by=pull_request.author,
1136 created_by=pull_request.author,
1137 notification_subject=subject,
1137 notification_subject=subject,
1138 notification_body=body_plaintext,
1138 notification_body=body_plaintext,
1139 notification_type=notification_type,
1139 notification_type=notification_type,
1140 recipients=recipients,
1140 recipients=recipients,
1141 email_kwargs=kwargs,
1141 email_kwargs=kwargs,
1142 )
1142 )
1143
1143
1144 def delete(self, pull_request, user):
1144 def delete(self, pull_request, user):
1145 pull_request = self.__get_pull_request(pull_request)
1145 pull_request = self.__get_pull_request(pull_request)
1146 old_data = pull_request.get_api_data(with_merge_state=False)
1146 old_data = pull_request.get_api_data(with_merge_state=False)
1147 self._cleanup_merge_workspace(pull_request)
1147 self._cleanup_merge_workspace(pull_request)
1148 self._log_audit_action(
1148 self._log_audit_action(
1149 'repo.pull_request.delete', {'old_data': old_data},
1149 'repo.pull_request.delete', {'old_data': old_data},
1150 user, pull_request)
1150 user, pull_request)
1151 Session().delete(pull_request)
1151 Session().delete(pull_request)
1152
1152
1153 def close_pull_request(self, pull_request, user):
1153 def close_pull_request(self, pull_request, user):
1154 pull_request = self.__get_pull_request(pull_request)
1154 pull_request = self.__get_pull_request(pull_request)
1155 self._cleanup_merge_workspace(pull_request)
1155 self._cleanup_merge_workspace(pull_request)
1156 pull_request.status = PullRequest.STATUS_CLOSED
1156 pull_request.status = PullRequest.STATUS_CLOSED
1157 pull_request.updated_on = datetime.datetime.now()
1157 pull_request.updated_on = datetime.datetime.now()
1158 Session().add(pull_request)
1158 Session().add(pull_request)
1159 self._trigger_pull_request_hook(
1159 self._trigger_pull_request_hook(
1160 pull_request, pull_request.author, 'close')
1160 pull_request, pull_request.author, 'close')
1161
1161
1162 pr_data = pull_request.get_api_data(with_merge_state=False)
1162 pr_data = pull_request.get_api_data(with_merge_state=False)
1163 self._log_audit_action(
1163 self._log_audit_action(
1164 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1164 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1165
1165
1166 def close_pull_request_with_comment(
1166 def close_pull_request_with_comment(
1167 self, pull_request, user, repo, message=None):
1167 self, pull_request, user, repo, message=None):
1168
1168
1169 pull_request_review_status = pull_request.calculated_review_status()
1169 pull_request_review_status = pull_request.calculated_review_status()
1170
1170
1171 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1171 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1172 # approved only if we have voting consent
1172 # approved only if we have voting consent
1173 status = ChangesetStatus.STATUS_APPROVED
1173 status = ChangesetStatus.STATUS_APPROVED
1174 else:
1174 else:
1175 status = ChangesetStatus.STATUS_REJECTED
1175 status = ChangesetStatus.STATUS_REJECTED
1176 status_lbl = ChangesetStatus.get_status_lbl(status)
1176 status_lbl = ChangesetStatus.get_status_lbl(status)
1177
1177
1178 default_message = (
1178 default_message = (
1179 'Closing with status change {transition_icon} {status}.'
1179 'Closing with status change {transition_icon} {status}.'
1180 ).format(transition_icon='>', status=status_lbl)
1180 ).format(transition_icon='>', status=status_lbl)
1181 text = message or default_message
1181 text = message or default_message
1182
1182
1183 # create a comment, and link it to new status
1183 # create a comment, and link it to new status
1184 comment = CommentsModel().create(
1184 comment = CommentsModel().create(
1185 text=text,
1185 text=text,
1186 repo=repo.repo_id,
1186 repo=repo.repo_id,
1187 user=user.user_id,
1187 user=user.user_id,
1188 pull_request=pull_request.pull_request_id,
1188 pull_request=pull_request.pull_request_id,
1189 status_change=status_lbl,
1189 status_change=status_lbl,
1190 status_change_type=status,
1190 status_change_type=status,
1191 closing_pr=True
1191 closing_pr=True
1192 )
1192 )
1193
1193
1194 # calculate old status before we change it
1194 # calculate old status before we change it
1195 old_calculated_status = pull_request.calculated_review_status()
1195 old_calculated_status = pull_request.calculated_review_status()
1196 ChangesetStatusModel().set_status(
1196 ChangesetStatusModel().set_status(
1197 repo.repo_id,
1197 repo.repo_id,
1198 status,
1198 status,
1199 user.user_id,
1199 user.user_id,
1200 comment=comment,
1200 comment=comment,
1201 pull_request=pull_request.pull_request_id
1201 pull_request=pull_request.pull_request_id
1202 )
1202 )
1203
1203
1204 Session().flush()
1204 Session().flush()
1205 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1205 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1206 # 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
1207 # calculation trigger status change. This might happen in cases
1207 # calculation trigger status change. This might happen in cases
1208 # 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
1209 # 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.
1210 calculated_status = pull_request.calculated_review_status()
1210 calculated_status = pull_request.calculated_review_status()
1211 if old_calculated_status != calculated_status:
1211 if old_calculated_status != calculated_status:
1212 self._trigger_pull_request_hook(
1212 self._trigger_pull_request_hook(
1213 pull_request, user, 'review_status_change')
1213 pull_request, user, 'review_status_change')
1214
1214
1215 # finally close the PR
1215 # finally close the PR
1216 PullRequestModel().close_pull_request(
1216 PullRequestModel().close_pull_request(
1217 pull_request.pull_request_id, user)
1217 pull_request.pull_request_id, user)
1218
1218
1219 return comment, status
1219 return comment, status
1220
1220
1221 def merge_status(self, pull_request, translator=None):
1221 def merge_status(self, pull_request, translator=None):
1222 _ = translator or get_current_request().translate
1222 _ = translator or get_current_request().translate
1223
1223
1224 if not self._is_merge_enabled(pull_request):
1224 if not self._is_merge_enabled(pull_request):
1225 return False, _('Server-side pull request merging is disabled.')
1225 return False, _('Server-side pull request merging is disabled.')
1226 if pull_request.is_closed():
1226 if pull_request.is_closed():
1227 return False, _('This pull request is closed.')
1227 return False, _('This pull request is closed.')
1228 merge_possible, msg = self._check_repo_requirements(
1228 merge_possible, msg = self._check_repo_requirements(
1229 target=pull_request.target_repo, source=pull_request.source_repo,
1229 target=pull_request.target_repo, source=pull_request.source_repo,
1230 translator=_)
1230 translator=_)
1231 if not merge_possible:
1231 if not merge_possible:
1232 return merge_possible, msg
1232 return merge_possible, msg
1233
1233
1234 try:
1234 try:
1235 resp = self._try_merge(pull_request)
1235 resp = self._try_merge(pull_request)
1236 log.debug("Merge response: %s", resp)
1236 log.debug("Merge response: %s", resp)
1237 status = resp.possible, self.merge_status_message(
1237 status = resp.possible, self.merge_status_message(
1238 resp.failure_reason)
1238 resp.failure_reason)
1239 except NotImplementedError:
1239 except NotImplementedError:
1240 status = False, _('Pull request merging is not supported.')
1240 status = False, _('Pull request merging is not supported.')
1241
1241
1242 return status
1242 return status
1243
1243
1244 def _check_repo_requirements(self, target, source, translator):
1244 def _check_repo_requirements(self, target, source, translator):
1245 """
1245 """
1246 Check if `target` and `source` have compatible requirements.
1246 Check if `target` and `source` have compatible requirements.
1247
1247
1248 Currently this is just checking for largefiles.
1248 Currently this is just checking for largefiles.
1249 """
1249 """
1250 _ = translator
1250 _ = translator
1251 target_has_largefiles = self._has_largefiles(target)
1251 target_has_largefiles = self._has_largefiles(target)
1252 source_has_largefiles = self._has_largefiles(source)
1252 source_has_largefiles = self._has_largefiles(source)
1253 merge_possible = True
1253 merge_possible = True
1254 message = u''
1254 message = u''
1255
1255
1256 if target_has_largefiles != source_has_largefiles:
1256 if target_has_largefiles != source_has_largefiles:
1257 merge_possible = False
1257 merge_possible = False
1258 if source_has_largefiles:
1258 if source_has_largefiles:
1259 message = _(
1259 message = _(
1260 'Target repository large files support is disabled.')
1260 'Target repository large files support is disabled.')
1261 else:
1261 else:
1262 message = _(
1262 message = _(
1263 'Source repository large files support is disabled.')
1263 'Source repository large files support is disabled.')
1264
1264
1265 return merge_possible, message
1265 return merge_possible, message
1266
1266
1267 def _has_largefiles(self, repo):
1267 def _has_largefiles(self, repo):
1268 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1268 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1269 'extensions', 'largefiles')
1269 'extensions', 'largefiles')
1270 return largefiles_ui and largefiles_ui[0].active
1270 return largefiles_ui and largefiles_ui[0].active
1271
1271
1272 def _try_merge(self, pull_request):
1272 def _try_merge(self, pull_request):
1273 """
1273 """
1274 Try to merge the pull request and return the merge status.
1274 Try to merge the pull request and return the merge status.
1275 """
1275 """
1276 log.debug(
1276 log.debug(
1277 "Trying out if the pull request %s can be merged.",
1277 "Trying out if the pull request %s can be merged.",
1278 pull_request.pull_request_id)
1278 pull_request.pull_request_id)
1279 target_vcs = pull_request.target_repo.scm_instance()
1279 target_vcs = pull_request.target_repo.scm_instance()
1280
1280
1281 # Refresh the target reference.
1281 # Refresh the target reference.
1282 try:
1282 try:
1283 target_ref = self._refresh_reference(
1283 target_ref = self._refresh_reference(
1284 pull_request.target_ref_parts, target_vcs)
1284 pull_request.target_ref_parts, target_vcs)
1285 except CommitDoesNotExistError:
1285 except CommitDoesNotExistError:
1286 merge_state = MergeResponse(
1286 merge_state = MergeResponse(
1287 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1287 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1288 return merge_state
1288 return merge_state
1289
1289
1290 target_locked = pull_request.target_repo.locked
1290 target_locked = pull_request.target_repo.locked
1291 if target_locked and target_locked[0]:
1291 if target_locked and target_locked[0]:
1292 log.debug("The target repository is locked.")
1292 log.debug("The target repository is locked.")
1293 merge_state = MergeResponse(
1293 merge_state = MergeResponse(
1294 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1294 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1295 elif self._needs_merge_state_refresh(pull_request, target_ref):
1295 elif self._needs_merge_state_refresh(pull_request, target_ref):
1296 log.debug("Refreshing the merge status of the repository.")
1296 log.debug("Refreshing the merge status of the repository.")
1297 merge_state = self._refresh_merge_state(
1297 merge_state = self._refresh_merge_state(
1298 pull_request, target_vcs, target_ref)
1298 pull_request, target_vcs, target_ref)
1299 else:
1299 else:
1300 possible = pull_request.\
1300 possible = pull_request.\
1301 last_merge_status == MergeFailureReason.NONE
1301 last_merge_status == MergeFailureReason.NONE
1302 merge_state = MergeResponse(
1302 merge_state = MergeResponse(
1303 possible, False, None, pull_request.last_merge_status)
1303 possible, False, None, pull_request.last_merge_status)
1304
1304
1305 return merge_state
1305 return merge_state
1306
1306
1307 def _refresh_reference(self, reference, vcs_repository):
1307 def _refresh_reference(self, reference, vcs_repository):
1308 if reference.type in ('branch', 'book'):
1308 if reference.type in ('branch', 'book'):
1309 name_or_id = reference.name
1309 name_or_id = reference.name
1310 else:
1310 else:
1311 name_or_id = reference.commit_id
1311 name_or_id = reference.commit_id
1312 refreshed_commit = vcs_repository.get_commit(name_or_id)
1312 refreshed_commit = vcs_repository.get_commit(name_or_id)
1313 refreshed_reference = Reference(
1313 refreshed_reference = Reference(
1314 reference.type, reference.name, refreshed_commit.raw_id)
1314 reference.type, reference.name, refreshed_commit.raw_id)
1315 return refreshed_reference
1315 return refreshed_reference
1316
1316
1317 def _needs_merge_state_refresh(self, pull_request, target_reference):
1317 def _needs_merge_state_refresh(self, pull_request, target_reference):
1318 return not(
1318 return not(
1319 pull_request.revisions and
1319 pull_request.revisions and
1320 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1320 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1321 target_reference.commit_id == pull_request._last_merge_target_rev)
1321 target_reference.commit_id == pull_request._last_merge_target_rev)
1322
1322
1323 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1323 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1324 workspace_id = self._workspace_id(pull_request)
1324 workspace_id = self._workspace_id(pull_request)
1325 source_vcs = pull_request.source_repo.scm_instance()
1325 source_vcs = pull_request.source_repo.scm_instance()
1326 use_rebase = self._use_rebase_for_merging(pull_request)
1326 use_rebase = self._use_rebase_for_merging(pull_request)
1327 close_branch = self._close_branch_before_merging(pull_request)
1327 close_branch = self._close_branch_before_merging(pull_request)
1328 merge_state = target_vcs.merge(
1328 merge_state = target_vcs.merge(
1329 target_reference, source_vcs, pull_request.source_ref_parts,
1329 target_reference, source_vcs, pull_request.source_ref_parts,
1330 workspace_id, dry_run=True, use_rebase=use_rebase,
1330 workspace_id, dry_run=True, use_rebase=use_rebase,
1331 close_branch=close_branch)
1331 close_branch=close_branch)
1332
1332
1333 # Do not store the response if there was an unknown error.
1333 # Do not store the response if there was an unknown error.
1334 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1334 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1335 pull_request._last_merge_source_rev = \
1335 pull_request._last_merge_source_rev = \
1336 pull_request.source_ref_parts.commit_id
1336 pull_request.source_ref_parts.commit_id
1337 pull_request._last_merge_target_rev = target_reference.commit_id
1337 pull_request._last_merge_target_rev = target_reference.commit_id
1338 pull_request.last_merge_status = merge_state.failure_reason
1338 pull_request.last_merge_status = merge_state.failure_reason
1339 pull_request.shadow_merge_ref = merge_state.merge_ref
1339 pull_request.shadow_merge_ref = merge_state.merge_ref
1340 Session().add(pull_request)
1340 Session().add(pull_request)
1341 Session().commit()
1341 Session().commit()
1342
1342
1343 return merge_state
1343 return merge_state
1344
1344
1345 def _workspace_id(self, pull_request):
1345 def _workspace_id(self, pull_request):
1346 workspace_id = 'pr-%s' % pull_request.pull_request_id
1346 workspace_id = 'pr-%s' % pull_request.pull_request_id
1347 return workspace_id
1347 return workspace_id
1348
1348
1349 def merge_status_message(self, status_code):
1349 def merge_status_message(self, status_code):
1350 """
1350 """
1351 Return a human friendly error message for the given merge status code.
1351 Return a human friendly error message for the given merge status code.
1352 """
1352 """
1353 return self.MERGE_STATUS_MESSAGES[status_code]
1353 return self.MERGE_STATUS_MESSAGES[status_code]
1354
1354
1355 def generate_repo_data(self, repo, commit_id=None, branch=None,
1355 def generate_repo_data(self, repo, commit_id=None, branch=None,
1356 bookmark=None, translator=None):
1356 bookmark=None, translator=None):
1357 from rhodecode.model.repo import RepoModel
1357 from rhodecode.model.repo import RepoModel
1358
1358
1359 all_refs, selected_ref = \
1359 all_refs, selected_ref = \
1360 self._get_repo_pullrequest_sources(
1360 self._get_repo_pullrequest_sources(
1361 repo.scm_instance(), commit_id=commit_id,
1361 repo.scm_instance(), commit_id=commit_id,
1362 branch=branch, bookmark=bookmark, translator=translator)
1362 branch=branch, bookmark=bookmark, translator=translator)
1363
1363
1364 refs_select2 = []
1364 refs_select2 = []
1365 for element in all_refs:
1365 for element in all_refs:
1366 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1366 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1367 refs_select2.append({'text': element[1], 'children': children})
1367 refs_select2.append({'text': element[1], 'children': children})
1368
1368
1369 return {
1369 return {
1370 'user': {
1370 'user': {
1371 'user_id': repo.user.user_id,
1371 'user_id': repo.user.user_id,
1372 'username': repo.user.username,
1372 'username': repo.user.username,
1373 'firstname': repo.user.first_name,
1373 'firstname': repo.user.first_name,
1374 'lastname': repo.user.last_name,
1374 'lastname': repo.user.last_name,
1375 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1375 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1376 },
1376 },
1377 'name': repo.repo_name,
1377 'name': repo.repo_name,
1378 'link': RepoModel().get_url(repo),
1378 'link': RepoModel().get_url(repo),
1379 'description': h.chop_at_smart(repo.description_safe, '\n'),
1379 'description': h.chop_at_smart(repo.description_safe, '\n'),
1380 'refs': {
1380 'refs': {
1381 'all_refs': all_refs,
1381 'all_refs': all_refs,
1382 'selected_ref': selected_ref,
1382 'selected_ref': selected_ref,
1383 'select2_refs': refs_select2
1383 'select2_refs': refs_select2
1384 }
1384 }
1385 }
1385 }
1386
1386
1387 def generate_pullrequest_title(self, source, source_ref, target):
1387 def generate_pullrequest_title(self, source, source_ref, target):
1388 return u'{source}#{at_ref} to {target}'.format(
1388 return u'{source}#{at_ref} to {target}'.format(
1389 source=source,
1389 source=source,
1390 at_ref=source_ref,
1390 at_ref=source_ref,
1391 target=target,
1391 target=target,
1392 )
1392 )
1393
1393
1394 def _cleanup_merge_workspace(self, pull_request):
1394 def _cleanup_merge_workspace(self, pull_request):
1395 # Merging related cleanup
1395 # Merging related cleanup
1396 target_scm = pull_request.target_repo.scm_instance()
1396 target_scm = pull_request.target_repo.scm_instance()
1397 workspace_id = 'pr-%s' % pull_request.pull_request_id
1397 workspace_id = 'pr-%s' % pull_request.pull_request_id
1398
1398
1399 try:
1399 try:
1400 target_scm.cleanup_merge_workspace(workspace_id)
1400 target_scm.cleanup_merge_workspace(workspace_id)
1401 except NotImplementedError:
1401 except NotImplementedError:
1402 pass
1402 pass
1403
1403
1404 def _get_repo_pullrequest_sources(
1404 def _get_repo_pullrequest_sources(
1405 self, repo, commit_id=None, branch=None, bookmark=None,
1405 self, repo, commit_id=None, branch=None, bookmark=None,
1406 translator=None):
1406 translator=None):
1407 """
1407 """
1408 Return a structure with repo's interesting commits, suitable for
1408 Return a structure with repo's interesting commits, suitable for
1409 the selectors in pullrequest controller
1409 the selectors in pullrequest controller
1410
1410
1411 :param commit_id: a commit that must be in the list somehow
1411 :param commit_id: a commit that must be in the list somehow
1412 and selected by default
1412 and selected by default
1413 :param branch: a branch that must be in the list and selected
1413 :param branch: a branch that must be in the list and selected
1414 by default - even if closed
1414 by default - even if closed
1415 :param bookmark: a bookmark that must be in the list and selected
1415 :param bookmark: a bookmark that must be in the list and selected
1416 """
1416 """
1417 _ = translator or get_current_request().translate
1417 _ = translator or get_current_request().translate
1418
1418
1419 commit_id = safe_str(commit_id) if commit_id else None
1419 commit_id = safe_str(commit_id) if commit_id else None
1420 branch = safe_str(branch) if branch else None
1420 branch = safe_str(branch) if branch else None
1421 bookmark = safe_str(bookmark) if bookmark else None
1421 bookmark = safe_str(bookmark) if bookmark else None
1422
1422
1423 selected = None
1423 selected = None
1424
1424
1425 # order matters: first source that has commit_id in it will be selected
1425 # order matters: first source that has commit_id in it will be selected
1426 sources = []
1426 sources = []
1427 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1427 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1428 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1428 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1429
1429
1430 if commit_id:
1430 if commit_id:
1431 ref_commit = (h.short_id(commit_id), commit_id)
1431 ref_commit = (h.short_id(commit_id), commit_id)
1432 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1432 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1433
1433
1434 sources.append(
1434 sources.append(
1435 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1435 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1436 )
1436 )
1437
1437
1438 groups = []
1438 groups = []
1439 for group_key, ref_list, group_name, match in sources:
1439 for group_key, ref_list, group_name, match in sources:
1440 group_refs = []
1440 group_refs = []
1441 for ref_name, ref_id in ref_list:
1441 for ref_name, ref_id in ref_list:
1442 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1442 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1443 group_refs.append((ref_key, ref_name))
1443 group_refs.append((ref_key, ref_name))
1444
1444
1445 if not selected:
1445 if not selected:
1446 if set([commit_id, match]) & set([ref_id, ref_name]):
1446 if set([commit_id, match]) & set([ref_id, ref_name]):
1447 selected = ref_key
1447 selected = ref_key
1448
1448
1449 if group_refs:
1449 if group_refs:
1450 groups.append((group_refs, group_name))
1450 groups.append((group_refs, group_name))
1451
1451
1452 if not selected:
1452 if not selected:
1453 ref = commit_id or branch or bookmark
1453 ref = commit_id or branch or bookmark
1454 if ref:
1454 if ref:
1455 raise CommitDoesNotExistError(
1455 raise CommitDoesNotExistError(
1456 'No commit refs could be found matching: %s' % ref)
1456 'No commit refs could be found matching: %s' % ref)
1457 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1457 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1458 selected = 'branch:%s:%s' % (
1458 selected = 'branch:%s:%s' % (
1459 repo.DEFAULT_BRANCH_NAME,
1459 repo.DEFAULT_BRANCH_NAME,
1460 repo.branches[repo.DEFAULT_BRANCH_NAME]
1460 repo.branches[repo.DEFAULT_BRANCH_NAME]
1461 )
1461 )
1462 elif repo.commit_ids:
1462 elif repo.commit_ids:
1463 # make the user select in this case
1463 # make the user select in this case
1464 selected = None
1464 selected = None
1465 else:
1465 else:
1466 raise EmptyRepositoryError()
1466 raise EmptyRepositoryError()
1467 return groups, selected
1467 return groups, selected
1468
1468
1469 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1469 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1470 return self._get_diff_from_pr_or_version(
1470 return self._get_diff_from_pr_or_version(
1471 source_repo, source_ref_id, target_ref_id, context=context)
1471 source_repo, source_ref_id, target_ref_id, context=context)
1472
1472
1473 def _get_diff_from_pr_or_version(
1473 def _get_diff_from_pr_or_version(
1474 self, source_repo, source_ref_id, target_ref_id, context):
1474 self, source_repo, source_ref_id, target_ref_id, context):
1475 target_commit = source_repo.get_commit(
1475 target_commit = source_repo.get_commit(
1476 commit_id=safe_str(target_ref_id))
1476 commit_id=safe_str(target_ref_id))
1477 source_commit = source_repo.get_commit(
1477 source_commit = source_repo.get_commit(
1478 commit_id=safe_str(source_ref_id))
1478 commit_id=safe_str(source_ref_id))
1479 if isinstance(source_repo, Repository):
1479 if isinstance(source_repo, Repository):
1480 vcs_repo = source_repo.scm_instance()
1480 vcs_repo = source_repo.scm_instance()
1481 else:
1481 else:
1482 vcs_repo = source_repo
1482 vcs_repo = source_repo
1483
1483
1484 # TODO: johbo: In the context of an update, we cannot reach
1484 # TODO: johbo: In the context of an update, we cannot reach
1485 # the old commit anymore with our normal mechanisms. It needs
1485 # the old commit anymore with our normal mechanisms. It needs
1486 # some sort of special support in the vcs layer to avoid this
1486 # some sort of special support in the vcs layer to avoid this
1487 # workaround.
1487 # workaround.
1488 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1488 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1489 vcs_repo.alias == 'git'):
1489 vcs_repo.alias == 'git'):
1490 source_commit.raw_id = safe_str(source_ref_id)
1490 source_commit.raw_id = safe_str(source_ref_id)
1491
1491
1492 log.debug('calculating diff between '
1492 log.debug('calculating diff between '
1493 'source_ref:%s and target_ref:%s for repo `%s`',
1493 'source_ref:%s and target_ref:%s for repo `%s`',
1494 target_ref_id, source_ref_id,
1494 target_ref_id, source_ref_id,
1495 safe_unicode(vcs_repo.path))
1495 safe_unicode(vcs_repo.path))
1496
1496
1497 vcs_diff = vcs_repo.get_diff(
1497 vcs_diff = vcs_repo.get_diff(
1498 commit1=target_commit, commit2=source_commit, context=context)
1498 commit1=target_commit, commit2=source_commit, context=context)
1499 return vcs_diff
1499 return vcs_diff
1500
1500
1501 def _is_merge_enabled(self, pull_request):
1501 def _is_merge_enabled(self, pull_request):
1502 return self._get_general_setting(
1502 return self._get_general_setting(
1503 pull_request, 'rhodecode_pr_merge_enabled')
1503 pull_request, 'rhodecode_pr_merge_enabled')
1504
1504
1505 def _use_rebase_for_merging(self, pull_request):
1505 def _use_rebase_for_merging(self, pull_request):
1506 repo_type = pull_request.target_repo.repo_type
1506 repo_type = pull_request.target_repo.repo_type
1507 if repo_type == 'hg':
1507 if repo_type == 'hg':
1508 return self._get_general_setting(
1508 return self._get_general_setting(
1509 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1509 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1510 elif repo_type == 'git':
1510 elif repo_type == 'git':
1511 return self._get_general_setting(
1511 return self._get_general_setting(
1512 pull_request, 'rhodecode_git_use_rebase_for_merging')
1512 pull_request, 'rhodecode_git_use_rebase_for_merging')
1513
1513
1514 return False
1514 return False
1515
1515
1516 def _close_branch_before_merging(self, pull_request):
1516 def _close_branch_before_merging(self, pull_request):
1517 repo_type = pull_request.target_repo.repo_type
1517 repo_type = pull_request.target_repo.repo_type
1518 if repo_type == 'hg':
1518 if repo_type == 'hg':
1519 return self._get_general_setting(
1519 return self._get_general_setting(
1520 pull_request, 'rhodecode_hg_close_branch_before_merging')
1520 pull_request, 'rhodecode_hg_close_branch_before_merging')
1521 elif repo_type == 'git':
1521 elif repo_type == 'git':
1522 return self._get_general_setting(
1522 return self._get_general_setting(
1523 pull_request, 'rhodecode_git_close_branch_before_merging')
1523 pull_request, 'rhodecode_git_close_branch_before_merging')
1524
1524
1525 return False
1525 return False
1526
1526
1527 def _get_general_setting(self, pull_request, settings_key, default=False):
1527 def _get_general_setting(self, pull_request, settings_key, default=False):
1528 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1528 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1529 settings = settings_model.get_general_settings()
1529 settings = settings_model.get_general_settings()
1530 return settings.get(settings_key, default)
1530 return settings.get(settings_key, default)
1531
1531
1532 def _log_audit_action(self, action, action_data, user, pull_request):
1532 def _log_audit_action(self, action, action_data, user, pull_request):
1533 audit_logger.store(
1533 audit_logger.store(
1534 action=action,
1534 action=action,
1535 action_data=action_data,
1535 action_data=action_data,
1536 user=user,
1536 user=user,
1537 repo=pull_request.target_repo)
1537 repo=pull_request.target_repo)
1538
1538
1539 def get_reviewer_functions(self):
1539 def get_reviewer_functions(self):
1540 """
1540 """
1541 Fetches functions for validation and fetching default reviewers.
1541 Fetches functions for validation and fetching default reviewers.
1542 If available we use the EE package, else we fallback to CE
1542 If available we use the EE package, else we fallback to CE
1543 package functions
1543 package functions
1544 """
1544 """
1545 try:
1545 try:
1546 from rc_reviewers.utils import get_default_reviewers_data
1546 from rc_reviewers.utils import get_default_reviewers_data
1547 from rc_reviewers.utils import validate_default_reviewers
1547 from rc_reviewers.utils import validate_default_reviewers
1548 except ImportError:
1548 except ImportError:
1549 from rhodecode.apps.repository.utils import \
1549 from rhodecode.apps.repository.utils import \
1550 get_default_reviewers_data
1550 get_default_reviewers_data
1551 from rhodecode.apps.repository.utils import \
1551 from rhodecode.apps.repository.utils import \
1552 validate_default_reviewers
1552 validate_default_reviewers
1553
1553
1554 return get_default_reviewers_data, validate_default_reviewers
1554 return get_default_reviewers_data, validate_default_reviewers
1555
1555
1556
1556
1557 class MergeCheck(object):
1557 class MergeCheck(object):
1558 """
1558 """
1559 Perform Merge Checks and returns a check object which stores information
1559 Perform Merge Checks and returns a check object which stores information
1560 about merge errors, and merge conditions
1560 about merge errors, and merge conditions
1561 """
1561 """
1562 TODO_CHECK = 'todo'
1562 TODO_CHECK = 'todo'
1563 PERM_CHECK = 'perm'
1563 PERM_CHECK = 'perm'
1564 REVIEW_CHECK = 'review'
1564 REVIEW_CHECK = 'review'
1565 MERGE_CHECK = 'merge'
1565 MERGE_CHECK = 'merge'
1566
1566
1567 def __init__(self):
1567 def __init__(self):
1568 self.review_status = None
1568 self.review_status = None
1569 self.merge_possible = None
1569 self.merge_possible = None
1570 self.merge_msg = ''
1570 self.merge_msg = ''
1571 self.failed = None
1571 self.failed = None
1572 self.errors = []
1572 self.errors = []
1573 self.error_details = OrderedDict()
1573 self.error_details = OrderedDict()
1574
1574
1575 def push_error(self, error_type, message, error_key, details):
1575 def push_error(self, error_type, message, error_key, details):
1576 self.failed = True
1576 self.failed = True
1577 self.errors.append([error_type, message])
1577 self.errors.append([error_type, message])
1578 self.error_details[error_key] = dict(
1578 self.error_details[error_key] = dict(
1579 details=details,
1579 details=details,
1580 error_type=error_type,
1580 error_type=error_type,
1581 message=message
1581 message=message
1582 )
1582 )
1583
1583
1584 @classmethod
1584 @classmethod
1585 def validate(cls, pull_request, user, translator, fail_early=False):
1585 def validate(cls, pull_request, user, translator, fail_early=False):
1586 _ = translator
1586 _ = translator
1587 merge_check = cls()
1587 merge_check = cls()
1588
1588
1589 # permissions to merge
1589 # permissions to merge
1590 user_allowed_to_merge = PullRequestModel().check_user_merge(
1590 user_allowed_to_merge = PullRequestModel().check_user_merge(
1591 pull_request, user)
1591 pull_request, user)
1592 if not user_allowed_to_merge:
1592 if not user_allowed_to_merge:
1593 log.debug("MergeCheck: cannot merge, approval is pending.")
1593 log.debug("MergeCheck: cannot merge, approval is pending.")
1594
1594
1595 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1595 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1596 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1596 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1597 if fail_early:
1597 if fail_early:
1598 return merge_check
1598 return merge_check
1599
1599
1600 # review status, must be always present
1600 # review status, must be always present
1601 review_status = pull_request.calculated_review_status()
1601 review_status = pull_request.calculated_review_status()
1602 merge_check.review_status = review_status
1602 merge_check.review_status = review_status
1603
1603
1604 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1604 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1605 if not status_approved:
1605 if not status_approved:
1606 log.debug("MergeCheck: cannot merge, approval is pending.")
1606 log.debug("MergeCheck: cannot merge, approval is pending.")
1607
1607
1608 msg = _('Pull request reviewer approval is pending.')
1608 msg = _('Pull request reviewer approval is pending.')
1609
1609
1610 merge_check.push_error(
1610 merge_check.push_error(
1611 'warning', msg, cls.REVIEW_CHECK, review_status)
1611 'warning', msg, cls.REVIEW_CHECK, review_status)
1612
1612
1613 if fail_early:
1613 if fail_early:
1614 return merge_check
1614 return merge_check
1615
1615
1616 # left over TODOs
1616 # left over TODOs
1617 todos = CommentsModel().get_unresolved_todos(pull_request)
1617 todos = CommentsModel().get_unresolved_todos(pull_request)
1618 if todos:
1618 if todos:
1619 log.debug("MergeCheck: cannot merge, {} "
1619 log.debug("MergeCheck: cannot merge, {} "
1620 "unresolved todos left.".format(len(todos)))
1620 "unresolved todos left.".format(len(todos)))
1621
1621
1622 if len(todos) == 1:
1622 if len(todos) == 1:
1623 msg = _('Cannot merge, {} TODO still not resolved.').format(
1623 msg = _('Cannot merge, {} TODO still not resolved.').format(
1624 len(todos))
1624 len(todos))
1625 else:
1625 else:
1626 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1626 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1627 len(todos))
1627 len(todos))
1628
1628
1629 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1629 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1630
1630
1631 if fail_early:
1631 if fail_early:
1632 return merge_check
1632 return merge_check
1633
1633
1634 # merge possible, here is the filesystem simulation + shadow repo
1634 # merge possible, here is the filesystem simulation + shadow repo
1635 merge_status, msg = PullRequestModel().merge_status(
1635 merge_status, msg = PullRequestModel().merge_status(
1636 pull_request, translator=translator)
1636 pull_request, translator=translator)
1637 merge_check.merge_possible = merge_status
1637 merge_check.merge_possible = merge_status
1638 merge_check.merge_msg = msg
1638 merge_check.merge_msg = msg
1639 if not merge_status:
1639 if not merge_status:
1640 log.debug(
1640 log.debug(
1641 "MergeCheck: cannot merge, pull request merge not possible.")
1641 "MergeCheck: cannot merge, pull request merge not possible.")
1642 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1642 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1643
1643
1644 if fail_early:
1644 if fail_early:
1645 return merge_check
1645 return merge_check
1646
1646
1647 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1647 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1648 return merge_check
1648 return merge_check
1649
1649
1650 @classmethod
1650 @classmethod
1651 def get_merge_conditions(cls, pull_request, translator):
1651 def get_merge_conditions(cls, pull_request, translator):
1652 _ = translator
1652 _ = translator
1653 merge_details = {}
1653 merge_details = {}
1654
1654
1655 model = PullRequestModel()
1655 model = PullRequestModel()
1656 use_rebase = model._use_rebase_for_merging(pull_request)
1656 use_rebase = model._use_rebase_for_merging(pull_request)
1657
1657
1658 if use_rebase:
1658 if use_rebase:
1659 merge_details['merge_strategy'] = dict(
1659 merge_details['merge_strategy'] = dict(
1660 details={},
1660 details={},
1661 message=_('Merge strategy: rebase')
1661 message=_('Merge strategy: rebase')
1662 )
1662 )
1663 else:
1663 else:
1664 merge_details['merge_strategy'] = dict(
1664 merge_details['merge_strategy'] = dict(
1665 details={},
1665 details={},
1666 message=_('Merge strategy: explicit merge commit')
1666 message=_('Merge strategy: explicit merge commit')
1667 )
1667 )
1668
1668
1669 close_branch = model._close_branch_before_merging(pull_request)
1669 close_branch = model._close_branch_before_merging(pull_request)
1670 if close_branch:
1670 if close_branch:
1671 repo_type = pull_request.target_repo.repo_type
1671 repo_type = pull_request.target_repo.repo_type
1672 if repo_type == 'hg':
1672 if repo_type == 'hg':
1673 close_msg = _('Source branch will be closed after merge.')
1673 close_msg = _('Source branch will be closed after merge.')
1674 elif repo_type == 'git':
1674 elif repo_type == 'git':
1675 close_msg = _('Source branch will be deleted after merge.')
1675 close_msg = _('Source branch will be deleted after merge.')
1676
1676
1677 merge_details['close_branch'] = dict(
1677 merge_details['close_branch'] = dict(
1678 details={},
1678 details={},
1679 message=close_msg
1679 message=close_msg
1680 )
1680 )
1681
1681
1682 return merge_details
1682 return merge_details
1683
1683
1684 ChangeTuple = collections.namedtuple(
1684 ChangeTuple = collections.namedtuple(
1685 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1685 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1686
1686
1687 FileChangeTuple = collections.namedtuple(
1687 FileChangeTuple = collections.namedtuple(
1688 'FileChangeTuple', ['added', 'modified', 'removed'])
1688 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now