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