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