##// END OF EJS Templates
default-reviewers: fixed voting rule calculation on user-group. The previous...
marcink -
r2960:545905e2 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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