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