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