##// END OF EJS Templates
pull-request: force update pull-request in case of the target repo reference changes....
marcink -
r1595:c00c09dd default
parent child Browse files
Show More
@@ -1,1425 +1,1450 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(
66 UpdateResponse = namedtuple(
67 'UpdateResponse', 'executed, reason, new, old, changes')
67 'UpdateResponse', 'executed, reason, new, old, changes')
68
68
69
69
70 class PullRequestModel(BaseModel):
70 class PullRequestModel(BaseModel):
71
71
72 cls = PullRequest
72 cls = PullRequest
73
73
74 DIFF_CONTEXT = 3
74 DIFF_CONTEXT = 3
75
75
76 MERGE_STATUS_MESSAGES = {
76 MERGE_STATUS_MESSAGES = {
77 MergeFailureReason.NONE: lazy_ugettext(
77 MergeFailureReason.NONE: lazy_ugettext(
78 'This pull request can be automatically merged.'),
78 'This pull request can be automatically merged.'),
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 'This pull request cannot be merged because of an unhandled'
80 'This pull request cannot be merged because of an unhandled'
81 ' exception.'),
81 ' exception.'),
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 'This pull request cannot be merged because of merge conflicts.'),
83 'This pull request cannot be merged because of merge conflicts.'),
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 'This pull request could not be merged because push to target'
85 'This pull request could not be merged because push to target'
86 ' failed.'),
86 ' failed.'),
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 'This pull request cannot be merged because the target is not a'
88 'This pull request cannot be merged because the target is not a'
89 ' head.'),
89 ' head.'),
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 'This pull request cannot be merged because the source contains'
91 'This pull request cannot be merged because the source contains'
92 ' more branches than the target.'),
92 ' more branches than the target.'),
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 'This pull request cannot be merged because the target has'
94 'This pull request cannot be merged because the target has'
95 ' multiple heads.'),
95 ' multiple heads.'),
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 'This pull request cannot be merged because the target repository'
97 'This pull request cannot be merged because the target repository'
98 ' is locked.'),
98 ' is locked.'),
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 'This pull request cannot be merged because the target or the '
100 'This pull request cannot be merged because the target or the '
101 'source reference is missing.'),
101 'source reference is missing.'),
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 'This pull request cannot be merged because the target '
103 'This pull request cannot be merged because the target '
104 'reference is missing.'),
104 'reference is missing.'),
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 'This pull request cannot be merged because the source '
106 'This pull request cannot be merged because the source '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 'This pull request cannot be merged because of conflicts related '
109 'This pull request cannot be merged because of conflicts related '
110 'to sub repositories.'),
110 'to sub repositories.'),
111 }
111 }
112
112
113 UPDATE_STATUS_MESSAGES = {
113 UPDATE_STATUS_MESSAGES = {
114 UpdateFailureReason.NONE: lazy_ugettext(
114 UpdateFailureReason.NONE: lazy_ugettext(
115 'Pull request update successful.'),
115 'Pull request update successful.'),
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 'Pull request update failed because of an unknown error.'),
117 'Pull request update failed because of an unknown error.'),
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 'No update needed because the source reference is already '
119 'No update needed because the source reference is already '
120 'up to date.'),
120 'up to date.'),
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TPYE: 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.'),
123 'not supported for an update.'),
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
588 target_ref_name = pull_request.target_ref_parts.name
589 target_ref_id = pull_request.target_ref_parts.commit_id
590
587 if not self.has_valid_update_type(pull_request):
591 if not self.has_valid_update_type(pull_request):
588 log.debug(
592 log.debug(
589 "Skipping update of pull request %s due to ref type: %s",
593 "Skipping update of pull request %s due to ref type: %s",
590 pull_request, source_ref_type)
594 pull_request, source_ref_type)
591 return UpdateResponse(
595 return UpdateResponse(
592 executed=False,
596 executed=False,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
597 reason=UpdateFailureReason.WRONG_REF_TPYE,
594 old=pull_request, new=None, changes=None)
598 old=pull_request, new=None, changes=None)
595
599
600 # source repo
596 source_repo = pull_request.source_repo.scm_instance()
601 source_repo = pull_request.source_repo.scm_instance()
597 try:
602 try:
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
603 source_commit = source_repo.get_commit(commit_id=source_ref_name)
599 except CommitDoesNotExistError:
604 except CommitDoesNotExistError:
600 return UpdateResponse(
605 return UpdateResponse(
601 executed=False,
606 executed=False,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
607 reason=UpdateFailureReason.MISSING_SOURCE_REF,
603 old=pull_request, new=None, changes=None)
608 old=pull_request, new=None, changes=None)
604
609
605 if source_ref_id == source_commit.raw_id:
610 source_changed = source_ref_id != source_commit.raw_id
611
612 # target repo
613 target_repo = pull_request.target_repo.scm_instance()
614 try:
615 target_commit = target_repo.get_commit(commit_id=target_ref_name)
616 except CommitDoesNotExistError:
617 return UpdateResponse(
618 executed=False,
619 reason=UpdateFailureReason.MISSING_TARGET_REF,
620 old=pull_request, new=None, changes=None)
621 target_changed = target_ref_id != target_commit.raw_id
622
623 if not (source_changed or target_changed):
606 log.debug("Nothing changed in pull request %s", pull_request)
624 log.debug("Nothing changed in pull request %s", pull_request)
607 return UpdateResponse(
625 return UpdateResponse(
608 executed=False,
626 executed=False,
609 reason=UpdateFailureReason.NO_CHANGE,
627 reason=UpdateFailureReason.NO_CHANGE,
610 old=pull_request, new=None, changes=None)
628 old=pull_request, new=None, changes=None)
611
629
612 # Finally there is a need for an update
630 change_in_found = 'target repo' if target_changed else 'source repo'
613 pull_request_version = self._create_version_from_snapshot(pull_request)
631 log.debug('Updating pull request because of change in %s detected',
614 self._link_comments_to_version(pull_request_version)
632 change_in_found)
615
633
616 target_ref_type = pull_request.target_ref_parts.type
634 # Finally there is a need for an update, in case of source change
617 target_ref_name = pull_request.target_ref_parts.name
635 # we create a new version, else just an update
618 target_ref_id = pull_request.target_ref_parts.commit_id
636 if source_changed:
619 target_repo = pull_request.target_repo.scm_instance()
637 pull_request_version = self._create_version_from_snapshot(pull_request)
638 self._link_comments_to_version(pull_request_version)
639 else:
640 ver = pull_request.versions[-1]
641 pull_request.pull_request_version_id = \
642 ver.pull_request_version_id if ver else None
643 pull_request_version = pull_request
620
644
621 try:
645 try:
622 if target_ref_type in ('tag', 'branch', 'book'):
646 if target_ref_type in ('tag', 'branch', 'book'):
623 target_commit = target_repo.get_commit(target_ref_name)
647 target_commit = target_repo.get_commit(target_ref_name)
624 else:
648 else:
625 target_commit = target_repo.get_commit(target_ref_id)
649 target_commit = target_repo.get_commit(target_ref_id)
626 except CommitDoesNotExistError:
650 except CommitDoesNotExistError:
627 return UpdateResponse(
651 return UpdateResponse(
628 executed=False,
652 executed=False,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
653 reason=UpdateFailureReason.MISSING_TARGET_REF,
630 old=pull_request, new=None, changes=None)
654 old=pull_request, new=None, changes=None)
631
655
632 # re-compute commit ids
656 # re-compute commit ids
633 old_commit_ids = pull_request.revisions
657 old_commit_ids = pull_request.revisions
634 pre_load = ["author", "branch", "date", "message"]
658 pre_load = ["author", "branch", "date", "message"]
635 commit_ranges = target_repo.compare(
659 commit_ranges = target_repo.compare(
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
660 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
637 pre_load=pre_load)
661 pre_load=pre_load)
638
662
639 ancestor = target_repo.get_common_ancestor(
663 ancestor = target_repo.get_common_ancestor(
640 target_commit.raw_id, source_commit.raw_id, source_repo)
664 target_commit.raw_id, source_commit.raw_id, source_repo)
641
665
642 pull_request.source_ref = '%s:%s:%s' % (
666 pull_request.source_ref = '%s:%s:%s' % (
643 source_ref_type, source_ref_name, source_commit.raw_id)
667 source_ref_type, source_ref_name, source_commit.raw_id)
644 pull_request.target_ref = '%s:%s:%s' % (
668 pull_request.target_ref = '%s:%s:%s' % (
645 target_ref_type, target_ref_name, ancestor)
669 target_ref_type, target_ref_name, ancestor)
670
646 pull_request.revisions = [
671 pull_request.revisions = [
647 commit.raw_id for commit in reversed(commit_ranges)]
672 commit.raw_id for commit in reversed(commit_ranges)]
648 pull_request.updated_on = datetime.datetime.now()
673 pull_request.updated_on = datetime.datetime.now()
649 Session().add(pull_request)
674 Session().add(pull_request)
650 new_commit_ids = pull_request.revisions
675 new_commit_ids = pull_request.revisions
651
676
652 changes = self._calculate_commit_id_changes(
677 changes = self._calculate_commit_id_changes(
653 old_commit_ids, new_commit_ids)
678 old_commit_ids, new_commit_ids)
654
679
655 old_diff_data, new_diff_data = self._generate_update_diffs(
680 old_diff_data, new_diff_data = self._generate_update_diffs(
656 pull_request, pull_request_version)
681 pull_request, pull_request_version)
657
682
658 CommentsModel().outdate_comments(
683 CommentsModel().outdate_comments(
659 pull_request, old_diff_data=old_diff_data,
684 pull_request, old_diff_data=old_diff_data,
660 new_diff_data=new_diff_data)
685 new_diff_data=new_diff_data)
661
686
662 file_changes = self._calculate_file_changes(
687 file_changes = self._calculate_file_changes(
663 old_diff_data, new_diff_data)
688 old_diff_data, new_diff_data)
664
689
665 # Add an automatic comment to the pull request
690 # Add an automatic comment to the pull request
666 update_comment = CommentsModel().create(
691 update_comment = CommentsModel().create(
667 text=self._render_update_message(changes, file_changes),
692 text=self._render_update_message(changes, file_changes),
668 repo=pull_request.target_repo,
693 repo=pull_request.target_repo,
669 user=pull_request.author,
694 user=pull_request.author,
670 pull_request=pull_request,
695 pull_request=pull_request,
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
696 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
672
697
673 # Update status to "Under Review" for added commits
698 # Update status to "Under Review" for added commits
674 for commit_id in changes.added:
699 for commit_id in changes.added:
675 ChangesetStatusModel().set_status(
700 ChangesetStatusModel().set_status(
676 repo=pull_request.source_repo,
701 repo=pull_request.source_repo,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
702 status=ChangesetStatus.STATUS_UNDER_REVIEW,
678 comment=update_comment,
703 comment=update_comment,
679 user=pull_request.author,
704 user=pull_request.author,
680 pull_request=pull_request,
705 pull_request=pull_request,
681 revision=commit_id)
706 revision=commit_id)
682
707
683 log.debug(
708 log.debug(
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
709 'Updated pull request %s, added_ids: %s, common_ids: %s, '
685 'removed_ids: %s', pull_request.pull_request_id,
710 'removed_ids: %s', pull_request.pull_request_id,
686 changes.added, changes.common, changes.removed)
711 changes.added, changes.common, changes.removed)
687 log.debug('Updated pull request with the following file changes: %s',
712 log.debug('Updated pull request with the following file changes: %s',
688 file_changes)
713 file_changes)
689
714
690 log.info(
715 log.info(
691 "Updated pull request %s from commit %s to commit %s, "
716 "Updated pull request %s from commit %s to commit %s, "
692 "stored new version %s of this pull request.",
717 "stored new version %s of this pull request.",
693 pull_request.pull_request_id, source_ref_id,
718 pull_request.pull_request_id, source_ref_id,
694 pull_request.source_ref_parts.commit_id,
719 pull_request.source_ref_parts.commit_id,
695 pull_request_version.pull_request_version_id)
720 pull_request_version.pull_request_version_id)
696 Session().commit()
721 Session().commit()
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
722 self._trigger_pull_request_hook(pull_request, pull_request.author,
698 'update')
723 'update')
699
724
700 return UpdateResponse(
725 return UpdateResponse(
701 executed=True, reason=UpdateFailureReason.NONE,
726 executed=True, reason=UpdateFailureReason.NONE,
702 old=pull_request, new=pull_request_version, changes=changes)
727 old=pull_request, new=pull_request_version, changes=changes)
703
728
704 def _create_version_from_snapshot(self, pull_request):
729 def _create_version_from_snapshot(self, pull_request):
705 version = PullRequestVersion()
730 version = PullRequestVersion()
706 version.title = pull_request.title
731 version.title = pull_request.title
707 version.description = pull_request.description
732 version.description = pull_request.description
708 version.status = pull_request.status
733 version.status = pull_request.status
709 version.created_on = datetime.datetime.now()
734 version.created_on = datetime.datetime.now()
710 version.updated_on = pull_request.updated_on
735 version.updated_on = pull_request.updated_on
711 version.user_id = pull_request.user_id
736 version.user_id = pull_request.user_id
712 version.source_repo = pull_request.source_repo
737 version.source_repo = pull_request.source_repo
713 version.source_ref = pull_request.source_ref
738 version.source_ref = pull_request.source_ref
714 version.target_repo = pull_request.target_repo
739 version.target_repo = pull_request.target_repo
715 version.target_ref = pull_request.target_ref
740 version.target_ref = pull_request.target_ref
716
741
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
742 version._last_merge_source_rev = pull_request._last_merge_source_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
743 version._last_merge_target_rev = pull_request._last_merge_target_rev
719 version._last_merge_status = pull_request._last_merge_status
744 version._last_merge_status = pull_request._last_merge_status
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
745 version.shadow_merge_ref = pull_request.shadow_merge_ref
721 version.merge_rev = pull_request.merge_rev
746 version.merge_rev = pull_request.merge_rev
722
747
723 version.revisions = pull_request.revisions
748 version.revisions = pull_request.revisions
724 version.pull_request = pull_request
749 version.pull_request = pull_request
725 Session().add(version)
750 Session().add(version)
726 Session().flush()
751 Session().flush()
727
752
728 return version
753 return version
729
754
730 def _generate_update_diffs(self, pull_request, pull_request_version):
755 def _generate_update_diffs(self, pull_request, pull_request_version):
731
756
732 diff_context = (
757 diff_context = (
733 self.DIFF_CONTEXT +
758 self.DIFF_CONTEXT +
734 CommentsModel.needed_extra_diff_context())
759 CommentsModel.needed_extra_diff_context())
735
760
736 source_repo = pull_request_version.source_repo
761 source_repo = pull_request_version.source_repo
737 source_ref_id = pull_request_version.source_ref_parts.commit_id
762 source_ref_id = pull_request_version.source_ref_parts.commit_id
738 target_ref_id = pull_request_version.target_ref_parts.commit_id
763 target_ref_id = pull_request_version.target_ref_parts.commit_id
739 old_diff = self._get_diff_from_pr_or_version(
764 old_diff = self._get_diff_from_pr_or_version(
740 source_repo, source_ref_id, target_ref_id, context=diff_context)
765 source_repo, source_ref_id, target_ref_id, context=diff_context)
741
766
742 source_repo = pull_request.source_repo
767 source_repo = pull_request.source_repo
743 source_ref_id = pull_request.source_ref_parts.commit_id
768 source_ref_id = pull_request.source_ref_parts.commit_id
744 target_ref_id = pull_request.target_ref_parts.commit_id
769 target_ref_id = pull_request.target_ref_parts.commit_id
745
770
746 new_diff = self._get_diff_from_pr_or_version(
771 new_diff = self._get_diff_from_pr_or_version(
747 source_repo, source_ref_id, target_ref_id, context=diff_context)
772 source_repo, source_ref_id, target_ref_id, context=diff_context)
748
773
749 old_diff_data = diffs.DiffProcessor(old_diff)
774 old_diff_data = diffs.DiffProcessor(old_diff)
750 old_diff_data.prepare()
775 old_diff_data.prepare()
751 new_diff_data = diffs.DiffProcessor(new_diff)
776 new_diff_data = diffs.DiffProcessor(new_diff)
752 new_diff_data.prepare()
777 new_diff_data.prepare()
753
778
754 return old_diff_data, new_diff_data
779 return old_diff_data, new_diff_data
755
780
756 def _link_comments_to_version(self, pull_request_version):
781 def _link_comments_to_version(self, pull_request_version):
757 """
782 """
758 Link all unlinked comments of this pull request to the given version.
783 Link all unlinked comments of this pull request to the given version.
759
784
760 :param pull_request_version: The `PullRequestVersion` to which
785 :param pull_request_version: The `PullRequestVersion` to which
761 the comments shall be linked.
786 the comments shall be linked.
762
787
763 """
788 """
764 pull_request = pull_request_version.pull_request
789 pull_request = pull_request_version.pull_request
765 comments = ChangesetComment.query().filter(
790 comments = ChangesetComment.query().filter(
766 # TODO: johbo: Should we query for the repo at all here?
791 # TODO: johbo: Should we query for the repo at all here?
767 # Pending decision on how comments of PRs are to be related
792 # Pending decision on how comments of PRs are to be related
768 # to either the source repo, the target repo or no repo at all.
793 # to either the source repo, the target repo or no repo at all.
769 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
794 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
770 ChangesetComment.pull_request == pull_request,
795 ChangesetComment.pull_request == pull_request,
771 ChangesetComment.pull_request_version == None)
796 ChangesetComment.pull_request_version == None)
772
797
773 # TODO: johbo: Find out why this breaks if it is done in a bulk
798 # TODO: johbo: Find out why this breaks if it is done in a bulk
774 # operation.
799 # operation.
775 for comment in comments:
800 for comment in comments:
776 comment.pull_request_version_id = (
801 comment.pull_request_version_id = (
777 pull_request_version.pull_request_version_id)
802 pull_request_version.pull_request_version_id)
778 Session().add(comment)
803 Session().add(comment)
779
804
780 def _calculate_commit_id_changes(self, old_ids, new_ids):
805 def _calculate_commit_id_changes(self, old_ids, new_ids):
781 added = [x for x in new_ids if x not in old_ids]
806 added = [x for x in new_ids if x not in old_ids]
782 common = [x for x in new_ids if x in old_ids]
807 common = [x for x in new_ids if x in old_ids]
783 removed = [x for x in old_ids if x not in new_ids]
808 removed = [x for x in old_ids if x not in new_ids]
784 total = new_ids
809 total = new_ids
785 return ChangeTuple(added, common, removed, total)
810 return ChangeTuple(added, common, removed, total)
786
811
787 def _calculate_file_changes(self, old_diff_data, new_diff_data):
812 def _calculate_file_changes(self, old_diff_data, new_diff_data):
788
813
789 old_files = OrderedDict()
814 old_files = OrderedDict()
790 for diff_data in old_diff_data.parsed_diff:
815 for diff_data in old_diff_data.parsed_diff:
791 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
816 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
792
817
793 added_files = []
818 added_files = []
794 modified_files = []
819 modified_files = []
795 removed_files = []
820 removed_files = []
796 for diff_data in new_diff_data.parsed_diff:
821 for diff_data in new_diff_data.parsed_diff:
797 new_filename = diff_data['filename']
822 new_filename = diff_data['filename']
798 new_hash = md5_safe(diff_data['raw_diff'])
823 new_hash = md5_safe(diff_data['raw_diff'])
799
824
800 old_hash = old_files.get(new_filename)
825 old_hash = old_files.get(new_filename)
801 if not old_hash:
826 if not old_hash:
802 # file is not present in old diff, means it's added
827 # file is not present in old diff, means it's added
803 added_files.append(new_filename)
828 added_files.append(new_filename)
804 else:
829 else:
805 if new_hash != old_hash:
830 if new_hash != old_hash:
806 modified_files.append(new_filename)
831 modified_files.append(new_filename)
807 # now remove a file from old, since we have seen it already
832 # now remove a file from old, since we have seen it already
808 del old_files[new_filename]
833 del old_files[new_filename]
809
834
810 # removed files is when there are present in old, but not in NEW,
835 # removed files is when there are present in old, but not in NEW,
811 # since we remove old files that are present in new diff, left-overs
836 # since we remove old files that are present in new diff, left-overs
812 # if any should be the removed files
837 # if any should be the removed files
813 removed_files.extend(old_files.keys())
838 removed_files.extend(old_files.keys())
814
839
815 return FileChangeTuple(added_files, modified_files, removed_files)
840 return FileChangeTuple(added_files, modified_files, removed_files)
816
841
817 def _render_update_message(self, changes, file_changes):
842 def _render_update_message(self, changes, file_changes):
818 """
843 """
819 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
844 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
820 so it's always looking the same disregarding on which default
845 so it's always looking the same disregarding on which default
821 renderer system is using.
846 renderer system is using.
822
847
823 :param changes: changes named tuple
848 :param changes: changes named tuple
824 :param file_changes: file changes named tuple
849 :param file_changes: file changes named tuple
825
850
826 """
851 """
827 new_status = ChangesetStatus.get_status_lbl(
852 new_status = ChangesetStatus.get_status_lbl(
828 ChangesetStatus.STATUS_UNDER_REVIEW)
853 ChangesetStatus.STATUS_UNDER_REVIEW)
829
854
830 changed_files = (
855 changed_files = (
831 file_changes.added + file_changes.modified + file_changes.removed)
856 file_changes.added + file_changes.modified + file_changes.removed)
832
857
833 params = {
858 params = {
834 'under_review_label': new_status,
859 'under_review_label': new_status,
835 'added_commits': changes.added,
860 'added_commits': changes.added,
836 'removed_commits': changes.removed,
861 'removed_commits': changes.removed,
837 'changed_files': changed_files,
862 'changed_files': changed_files,
838 'added_files': file_changes.added,
863 'added_files': file_changes.added,
839 'modified_files': file_changes.modified,
864 'modified_files': file_changes.modified,
840 'removed_files': file_changes.removed,
865 'removed_files': file_changes.removed,
841 }
866 }
842 renderer = RstTemplateRenderer()
867 renderer = RstTemplateRenderer()
843 return renderer.render('pull_request_update.mako', **params)
868 return renderer.render('pull_request_update.mako', **params)
844
869
845 def edit(self, pull_request, title, description):
870 def edit(self, pull_request, title, description):
846 pull_request = self.__get_pull_request(pull_request)
871 pull_request = self.__get_pull_request(pull_request)
847 if pull_request.is_closed():
872 if pull_request.is_closed():
848 raise ValueError('This pull request is closed')
873 raise ValueError('This pull request is closed')
849 if title:
874 if title:
850 pull_request.title = title
875 pull_request.title = title
851 pull_request.description = description
876 pull_request.description = description
852 pull_request.updated_on = datetime.datetime.now()
877 pull_request.updated_on = datetime.datetime.now()
853 Session().add(pull_request)
878 Session().add(pull_request)
854
879
855 def update_reviewers(self, pull_request, reviewer_data):
880 def update_reviewers(self, pull_request, reviewer_data):
856 """
881 """
857 Update the reviewers in the pull request
882 Update the reviewers in the pull request
858
883
859 :param pull_request: the pr to update
884 :param pull_request: the pr to update
860 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
885 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
861 """
886 """
862
887
863 reviewers_reasons = {}
888 reviewers_reasons = {}
864 for user_id, reasons in reviewer_data:
889 for user_id, reasons in reviewer_data:
865 if isinstance(user_id, (int, basestring)):
890 if isinstance(user_id, (int, basestring)):
866 user_id = self._get_user(user_id).user_id
891 user_id = self._get_user(user_id).user_id
867 reviewers_reasons[user_id] = reasons
892 reviewers_reasons[user_id] = reasons
868
893
869 reviewers_ids = set(reviewers_reasons.keys())
894 reviewers_ids = set(reviewers_reasons.keys())
870 pull_request = self.__get_pull_request(pull_request)
895 pull_request = self.__get_pull_request(pull_request)
871 current_reviewers = PullRequestReviewers.query()\
896 current_reviewers = PullRequestReviewers.query()\
872 .filter(PullRequestReviewers.pull_request ==
897 .filter(PullRequestReviewers.pull_request ==
873 pull_request).all()
898 pull_request).all()
874 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
899 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
875
900
876 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
901 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
877 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
902 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
878
903
879 log.debug("Adding %s reviewers", ids_to_add)
904 log.debug("Adding %s reviewers", ids_to_add)
880 log.debug("Removing %s reviewers", ids_to_remove)
905 log.debug("Removing %s reviewers", ids_to_remove)
881 changed = False
906 changed = False
882 for uid in ids_to_add:
907 for uid in ids_to_add:
883 changed = True
908 changed = True
884 _usr = self._get_user(uid)
909 _usr = self._get_user(uid)
885 reasons = reviewers_reasons[uid]
910 reasons = reviewers_reasons[uid]
886 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
911 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
887 Session().add(reviewer)
912 Session().add(reviewer)
888
913
889 for uid in ids_to_remove:
914 for uid in ids_to_remove:
890 changed = True
915 changed = True
891 reviewers = PullRequestReviewers.query()\
916 reviewers = PullRequestReviewers.query()\
892 .filter(PullRequestReviewers.user_id == uid,
917 .filter(PullRequestReviewers.user_id == uid,
893 PullRequestReviewers.pull_request == pull_request)\
918 PullRequestReviewers.pull_request == pull_request)\
894 .all()
919 .all()
895 # use .all() in case we accidentally added the same person twice
920 # use .all() in case we accidentally added the same person twice
896 # this CAN happen due to the lack of DB checks
921 # this CAN happen due to the lack of DB checks
897 for obj in reviewers:
922 for obj in reviewers:
898 Session().delete(obj)
923 Session().delete(obj)
899
924
900 if changed:
925 if changed:
901 pull_request.updated_on = datetime.datetime.now()
926 pull_request.updated_on = datetime.datetime.now()
902 Session().add(pull_request)
927 Session().add(pull_request)
903
928
904 self.notify_reviewers(pull_request, ids_to_add)
929 self.notify_reviewers(pull_request, ids_to_add)
905 return ids_to_add, ids_to_remove
930 return ids_to_add, ids_to_remove
906
931
907 def get_url(self, pull_request):
932 def get_url(self, pull_request):
908 return h.url('pullrequest_show',
933 return h.url('pullrequest_show',
909 repo_name=safe_str(pull_request.target_repo.repo_name),
934 repo_name=safe_str(pull_request.target_repo.repo_name),
910 pull_request_id=pull_request.pull_request_id,
935 pull_request_id=pull_request.pull_request_id,
911 qualified=True)
936 qualified=True)
912
937
913 def get_shadow_clone_url(self, pull_request):
938 def get_shadow_clone_url(self, pull_request):
914 """
939 """
915 Returns qualified url pointing to the shadow repository. If this pull
940 Returns qualified url pointing to the shadow repository. If this pull
916 request is closed there is no shadow repository and ``None`` will be
941 request is closed there is no shadow repository and ``None`` will be
917 returned.
942 returned.
918 """
943 """
919 if pull_request.is_closed():
944 if pull_request.is_closed():
920 return None
945 return None
921 else:
946 else:
922 pr_url = urllib.unquote(self.get_url(pull_request))
947 pr_url = urllib.unquote(self.get_url(pull_request))
923 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
948 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
924
949
925 def notify_reviewers(self, pull_request, reviewers_ids):
950 def notify_reviewers(self, pull_request, reviewers_ids):
926 # notification to reviewers
951 # notification to reviewers
927 if not reviewers_ids:
952 if not reviewers_ids:
928 return
953 return
929
954
930 pull_request_obj = pull_request
955 pull_request_obj = pull_request
931 # get the current participants of this pull request
956 # get the current participants of this pull request
932 recipients = reviewers_ids
957 recipients = reviewers_ids
933 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
958 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
934
959
935 pr_source_repo = pull_request_obj.source_repo
960 pr_source_repo = pull_request_obj.source_repo
936 pr_target_repo = pull_request_obj.target_repo
961 pr_target_repo = pull_request_obj.target_repo
937
962
938 pr_url = h.url(
963 pr_url = h.url(
939 'pullrequest_show',
964 'pullrequest_show',
940 repo_name=pr_target_repo.repo_name,
965 repo_name=pr_target_repo.repo_name,
941 pull_request_id=pull_request_obj.pull_request_id,
966 pull_request_id=pull_request_obj.pull_request_id,
942 qualified=True,)
967 qualified=True,)
943
968
944 # set some variables for email notification
969 # set some variables for email notification
945 pr_target_repo_url = h.url(
970 pr_target_repo_url = h.url(
946 'summary_home',
971 'summary_home',
947 repo_name=pr_target_repo.repo_name,
972 repo_name=pr_target_repo.repo_name,
948 qualified=True)
973 qualified=True)
949
974
950 pr_source_repo_url = h.url(
975 pr_source_repo_url = h.url(
951 'summary_home',
976 'summary_home',
952 repo_name=pr_source_repo.repo_name,
977 repo_name=pr_source_repo.repo_name,
953 qualified=True)
978 qualified=True)
954
979
955 # pull request specifics
980 # pull request specifics
956 pull_request_commits = [
981 pull_request_commits = [
957 (x.raw_id, x.message)
982 (x.raw_id, x.message)
958 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
983 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
959
984
960 kwargs = {
985 kwargs = {
961 'user': pull_request.author,
986 'user': pull_request.author,
962 'pull_request': pull_request_obj,
987 'pull_request': pull_request_obj,
963 'pull_request_commits': pull_request_commits,
988 'pull_request_commits': pull_request_commits,
964
989
965 'pull_request_target_repo': pr_target_repo,
990 'pull_request_target_repo': pr_target_repo,
966 'pull_request_target_repo_url': pr_target_repo_url,
991 'pull_request_target_repo_url': pr_target_repo_url,
967
992
968 'pull_request_source_repo': pr_source_repo,
993 'pull_request_source_repo': pr_source_repo,
969 'pull_request_source_repo_url': pr_source_repo_url,
994 'pull_request_source_repo_url': pr_source_repo_url,
970
995
971 'pull_request_url': pr_url,
996 'pull_request_url': pr_url,
972 }
997 }
973
998
974 # pre-generate the subject for notification itself
999 # pre-generate the subject for notification itself
975 (subject,
1000 (subject,
976 _h, _e, # we don't care about those
1001 _h, _e, # we don't care about those
977 body_plaintext) = EmailNotificationModel().render_email(
1002 body_plaintext) = EmailNotificationModel().render_email(
978 notification_type, **kwargs)
1003 notification_type, **kwargs)
979
1004
980 # create notification objects, and emails
1005 # create notification objects, and emails
981 NotificationModel().create(
1006 NotificationModel().create(
982 created_by=pull_request.author,
1007 created_by=pull_request.author,
983 notification_subject=subject,
1008 notification_subject=subject,
984 notification_body=body_plaintext,
1009 notification_body=body_plaintext,
985 notification_type=notification_type,
1010 notification_type=notification_type,
986 recipients=recipients,
1011 recipients=recipients,
987 email_kwargs=kwargs,
1012 email_kwargs=kwargs,
988 )
1013 )
989
1014
990 def delete(self, pull_request):
1015 def delete(self, pull_request):
991 pull_request = self.__get_pull_request(pull_request)
1016 pull_request = self.__get_pull_request(pull_request)
992 self._cleanup_merge_workspace(pull_request)
1017 self._cleanup_merge_workspace(pull_request)
993 Session().delete(pull_request)
1018 Session().delete(pull_request)
994
1019
995 def close_pull_request(self, pull_request, user):
1020 def close_pull_request(self, pull_request, user):
996 pull_request = self.__get_pull_request(pull_request)
1021 pull_request = self.__get_pull_request(pull_request)
997 self._cleanup_merge_workspace(pull_request)
1022 self._cleanup_merge_workspace(pull_request)
998 pull_request.status = PullRequest.STATUS_CLOSED
1023 pull_request.status = PullRequest.STATUS_CLOSED
999 pull_request.updated_on = datetime.datetime.now()
1024 pull_request.updated_on = datetime.datetime.now()
1000 Session().add(pull_request)
1025 Session().add(pull_request)
1001 self._trigger_pull_request_hook(
1026 self._trigger_pull_request_hook(
1002 pull_request, pull_request.author, 'close')
1027 pull_request, pull_request.author, 'close')
1003 self._log_action('user_closed_pull_request', user, pull_request)
1028 self._log_action('user_closed_pull_request', user, pull_request)
1004
1029
1005 def close_pull_request_with_comment(self, pull_request, user, repo,
1030 def close_pull_request_with_comment(self, pull_request, user, repo,
1006 message=None):
1031 message=None):
1007 status = ChangesetStatus.STATUS_REJECTED
1032 status = ChangesetStatus.STATUS_REJECTED
1008
1033
1009 if not message:
1034 if not message:
1010 message = (
1035 message = (
1011 _('Status change %(transition_icon)s %(status)s') % {
1036 _('Status change %(transition_icon)s %(status)s') % {
1012 'transition_icon': '>',
1037 'transition_icon': '>',
1013 'status': ChangesetStatus.get_status_lbl(status)})
1038 'status': ChangesetStatus.get_status_lbl(status)})
1014
1039
1015 internal_message = _('Closing with') + ' ' + message
1040 internal_message = _('Closing with') + ' ' + message
1016
1041
1017 comm = CommentsModel().create(
1042 comm = CommentsModel().create(
1018 text=internal_message,
1043 text=internal_message,
1019 repo=repo.repo_id,
1044 repo=repo.repo_id,
1020 user=user.user_id,
1045 user=user.user_id,
1021 pull_request=pull_request.pull_request_id,
1046 pull_request=pull_request.pull_request_id,
1022 f_path=None,
1047 f_path=None,
1023 line_no=None,
1048 line_no=None,
1024 status_change=ChangesetStatus.get_status_lbl(status),
1049 status_change=ChangesetStatus.get_status_lbl(status),
1025 status_change_type=status,
1050 status_change_type=status,
1026 closing_pr=True
1051 closing_pr=True
1027 )
1052 )
1028
1053
1029 ChangesetStatusModel().set_status(
1054 ChangesetStatusModel().set_status(
1030 repo.repo_id,
1055 repo.repo_id,
1031 status,
1056 status,
1032 user.user_id,
1057 user.user_id,
1033 comm,
1058 comm,
1034 pull_request=pull_request.pull_request_id
1059 pull_request=pull_request.pull_request_id
1035 )
1060 )
1036 Session().flush()
1061 Session().flush()
1037
1062
1038 PullRequestModel().close_pull_request(
1063 PullRequestModel().close_pull_request(
1039 pull_request.pull_request_id, user)
1064 pull_request.pull_request_id, user)
1040
1065
1041 def merge_status(self, pull_request):
1066 def merge_status(self, pull_request):
1042 if not self._is_merge_enabled(pull_request):
1067 if not self._is_merge_enabled(pull_request):
1043 return False, _('Server-side pull request merging is disabled.')
1068 return False, _('Server-side pull request merging is disabled.')
1044 if pull_request.is_closed():
1069 if pull_request.is_closed():
1045 return False, _('This pull request is closed.')
1070 return False, _('This pull request is closed.')
1046 merge_possible, msg = self._check_repo_requirements(
1071 merge_possible, msg = self._check_repo_requirements(
1047 target=pull_request.target_repo, source=pull_request.source_repo)
1072 target=pull_request.target_repo, source=pull_request.source_repo)
1048 if not merge_possible:
1073 if not merge_possible:
1049 return merge_possible, msg
1074 return merge_possible, msg
1050
1075
1051 try:
1076 try:
1052 resp = self._try_merge(pull_request)
1077 resp = self._try_merge(pull_request)
1053 log.debug("Merge response: %s", resp)
1078 log.debug("Merge response: %s", resp)
1054 status = resp.possible, self.merge_status_message(
1079 status = resp.possible, self.merge_status_message(
1055 resp.failure_reason)
1080 resp.failure_reason)
1056 except NotImplementedError:
1081 except NotImplementedError:
1057 status = False, _('Pull request merging is not supported.')
1082 status = False, _('Pull request merging is not supported.')
1058
1083
1059 return status
1084 return status
1060
1085
1061 def _check_repo_requirements(self, target, source):
1086 def _check_repo_requirements(self, target, source):
1062 """
1087 """
1063 Check if `target` and `source` have compatible requirements.
1088 Check if `target` and `source` have compatible requirements.
1064
1089
1065 Currently this is just checking for largefiles.
1090 Currently this is just checking for largefiles.
1066 """
1091 """
1067 target_has_largefiles = self._has_largefiles(target)
1092 target_has_largefiles = self._has_largefiles(target)
1068 source_has_largefiles = self._has_largefiles(source)
1093 source_has_largefiles = self._has_largefiles(source)
1069 merge_possible = True
1094 merge_possible = True
1070 message = u''
1095 message = u''
1071
1096
1072 if target_has_largefiles != source_has_largefiles:
1097 if target_has_largefiles != source_has_largefiles:
1073 merge_possible = False
1098 merge_possible = False
1074 if source_has_largefiles:
1099 if source_has_largefiles:
1075 message = _(
1100 message = _(
1076 'Target repository large files support is disabled.')
1101 'Target repository large files support is disabled.')
1077 else:
1102 else:
1078 message = _(
1103 message = _(
1079 'Source repository large files support is disabled.')
1104 'Source repository large files support is disabled.')
1080
1105
1081 return merge_possible, message
1106 return merge_possible, message
1082
1107
1083 def _has_largefiles(self, repo):
1108 def _has_largefiles(self, repo):
1084 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1109 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1085 'extensions', 'largefiles')
1110 'extensions', 'largefiles')
1086 return largefiles_ui and largefiles_ui[0].active
1111 return largefiles_ui and largefiles_ui[0].active
1087
1112
1088 def _try_merge(self, pull_request):
1113 def _try_merge(self, pull_request):
1089 """
1114 """
1090 Try to merge the pull request and return the merge status.
1115 Try to merge the pull request and return the merge status.
1091 """
1116 """
1092 log.debug(
1117 log.debug(
1093 "Trying out if the pull request %s can be merged.",
1118 "Trying out if the pull request %s can be merged.",
1094 pull_request.pull_request_id)
1119 pull_request.pull_request_id)
1095 target_vcs = pull_request.target_repo.scm_instance()
1120 target_vcs = pull_request.target_repo.scm_instance()
1096
1121
1097 # Refresh the target reference.
1122 # Refresh the target reference.
1098 try:
1123 try:
1099 target_ref = self._refresh_reference(
1124 target_ref = self._refresh_reference(
1100 pull_request.target_ref_parts, target_vcs)
1125 pull_request.target_ref_parts, target_vcs)
1101 except CommitDoesNotExistError:
1126 except CommitDoesNotExistError:
1102 merge_state = MergeResponse(
1127 merge_state = MergeResponse(
1103 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1128 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1104 return merge_state
1129 return merge_state
1105
1130
1106 target_locked = pull_request.target_repo.locked
1131 target_locked = pull_request.target_repo.locked
1107 if target_locked and target_locked[0]:
1132 if target_locked and target_locked[0]:
1108 log.debug("The target repository is locked.")
1133 log.debug("The target repository is locked.")
1109 merge_state = MergeResponse(
1134 merge_state = MergeResponse(
1110 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1135 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1111 elif self._needs_merge_state_refresh(pull_request, target_ref):
1136 elif self._needs_merge_state_refresh(pull_request, target_ref):
1112 log.debug("Refreshing the merge status of the repository.")
1137 log.debug("Refreshing the merge status of the repository.")
1113 merge_state = self._refresh_merge_state(
1138 merge_state = self._refresh_merge_state(
1114 pull_request, target_vcs, target_ref)
1139 pull_request, target_vcs, target_ref)
1115 else:
1140 else:
1116 possible = pull_request.\
1141 possible = pull_request.\
1117 _last_merge_status == MergeFailureReason.NONE
1142 _last_merge_status == MergeFailureReason.NONE
1118 merge_state = MergeResponse(
1143 merge_state = MergeResponse(
1119 possible, False, None, pull_request._last_merge_status)
1144 possible, False, None, pull_request._last_merge_status)
1120
1145
1121 return merge_state
1146 return merge_state
1122
1147
1123 def _refresh_reference(self, reference, vcs_repository):
1148 def _refresh_reference(self, reference, vcs_repository):
1124 if reference.type in ('branch', 'book'):
1149 if reference.type in ('branch', 'book'):
1125 name_or_id = reference.name
1150 name_or_id = reference.name
1126 else:
1151 else:
1127 name_or_id = reference.commit_id
1152 name_or_id = reference.commit_id
1128 refreshed_commit = vcs_repository.get_commit(name_or_id)
1153 refreshed_commit = vcs_repository.get_commit(name_or_id)
1129 refreshed_reference = Reference(
1154 refreshed_reference = Reference(
1130 reference.type, reference.name, refreshed_commit.raw_id)
1155 reference.type, reference.name, refreshed_commit.raw_id)
1131 return refreshed_reference
1156 return refreshed_reference
1132
1157
1133 def _needs_merge_state_refresh(self, pull_request, target_reference):
1158 def _needs_merge_state_refresh(self, pull_request, target_reference):
1134 return not(
1159 return not(
1135 pull_request.revisions and
1160 pull_request.revisions and
1136 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1161 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1137 target_reference.commit_id == pull_request._last_merge_target_rev)
1162 target_reference.commit_id == pull_request._last_merge_target_rev)
1138
1163
1139 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1164 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1140 workspace_id = self._workspace_id(pull_request)
1165 workspace_id = self._workspace_id(pull_request)
1141 source_vcs = pull_request.source_repo.scm_instance()
1166 source_vcs = pull_request.source_repo.scm_instance()
1142 use_rebase = self._use_rebase_for_merging(pull_request)
1167 use_rebase = self._use_rebase_for_merging(pull_request)
1143 merge_state = target_vcs.merge(
1168 merge_state = target_vcs.merge(
1144 target_reference, source_vcs, pull_request.source_ref_parts,
1169 target_reference, source_vcs, pull_request.source_ref_parts,
1145 workspace_id, dry_run=True, use_rebase=use_rebase)
1170 workspace_id, dry_run=True, use_rebase=use_rebase)
1146
1171
1147 # Do not store the response if there was an unknown error.
1172 # Do not store the response if there was an unknown error.
1148 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1173 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1149 pull_request._last_merge_source_rev = \
1174 pull_request._last_merge_source_rev = \
1150 pull_request.source_ref_parts.commit_id
1175 pull_request.source_ref_parts.commit_id
1151 pull_request._last_merge_target_rev = target_reference.commit_id
1176 pull_request._last_merge_target_rev = target_reference.commit_id
1152 pull_request._last_merge_status = merge_state.failure_reason
1177 pull_request._last_merge_status = merge_state.failure_reason
1153 pull_request.shadow_merge_ref = merge_state.merge_ref
1178 pull_request.shadow_merge_ref = merge_state.merge_ref
1154 Session().add(pull_request)
1179 Session().add(pull_request)
1155 Session().commit()
1180 Session().commit()
1156
1181
1157 return merge_state
1182 return merge_state
1158
1183
1159 def _workspace_id(self, pull_request):
1184 def _workspace_id(self, pull_request):
1160 workspace_id = 'pr-%s' % pull_request.pull_request_id
1185 workspace_id = 'pr-%s' % pull_request.pull_request_id
1161 return workspace_id
1186 return workspace_id
1162
1187
1163 def merge_status_message(self, status_code):
1188 def merge_status_message(self, status_code):
1164 """
1189 """
1165 Return a human friendly error message for the given merge status code.
1190 Return a human friendly error message for the given merge status code.
1166 """
1191 """
1167 return self.MERGE_STATUS_MESSAGES[status_code]
1192 return self.MERGE_STATUS_MESSAGES[status_code]
1168
1193
1169 def generate_repo_data(self, repo, commit_id=None, branch=None,
1194 def generate_repo_data(self, repo, commit_id=None, branch=None,
1170 bookmark=None):
1195 bookmark=None):
1171 all_refs, selected_ref = \
1196 all_refs, selected_ref = \
1172 self._get_repo_pullrequest_sources(
1197 self._get_repo_pullrequest_sources(
1173 repo.scm_instance(), commit_id=commit_id,
1198 repo.scm_instance(), commit_id=commit_id,
1174 branch=branch, bookmark=bookmark)
1199 branch=branch, bookmark=bookmark)
1175
1200
1176 refs_select2 = []
1201 refs_select2 = []
1177 for element in all_refs:
1202 for element in all_refs:
1178 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1203 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1179 refs_select2.append({'text': element[1], 'children': children})
1204 refs_select2.append({'text': element[1], 'children': children})
1180
1205
1181 return {
1206 return {
1182 'user': {
1207 'user': {
1183 'user_id': repo.user.user_id,
1208 'user_id': repo.user.user_id,
1184 'username': repo.user.username,
1209 'username': repo.user.username,
1185 'firstname': repo.user.firstname,
1210 'firstname': repo.user.firstname,
1186 'lastname': repo.user.lastname,
1211 'lastname': repo.user.lastname,
1187 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1212 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1188 },
1213 },
1189 'description': h.chop_at_smart(repo.description, '\n'),
1214 'description': h.chop_at_smart(repo.description, '\n'),
1190 'refs': {
1215 'refs': {
1191 'all_refs': all_refs,
1216 'all_refs': all_refs,
1192 'selected_ref': selected_ref,
1217 'selected_ref': selected_ref,
1193 'select2_refs': refs_select2
1218 'select2_refs': refs_select2
1194 }
1219 }
1195 }
1220 }
1196
1221
1197 def generate_pullrequest_title(self, source, source_ref, target):
1222 def generate_pullrequest_title(self, source, source_ref, target):
1198 return u'{source}#{at_ref} to {target}'.format(
1223 return u'{source}#{at_ref} to {target}'.format(
1199 source=source,
1224 source=source,
1200 at_ref=source_ref,
1225 at_ref=source_ref,
1201 target=target,
1226 target=target,
1202 )
1227 )
1203
1228
1204 def _cleanup_merge_workspace(self, pull_request):
1229 def _cleanup_merge_workspace(self, pull_request):
1205 # Merging related cleanup
1230 # Merging related cleanup
1206 target_scm = pull_request.target_repo.scm_instance()
1231 target_scm = pull_request.target_repo.scm_instance()
1207 workspace_id = 'pr-%s' % pull_request.pull_request_id
1232 workspace_id = 'pr-%s' % pull_request.pull_request_id
1208
1233
1209 try:
1234 try:
1210 target_scm.cleanup_merge_workspace(workspace_id)
1235 target_scm.cleanup_merge_workspace(workspace_id)
1211 except NotImplementedError:
1236 except NotImplementedError:
1212 pass
1237 pass
1213
1238
1214 def _get_repo_pullrequest_sources(
1239 def _get_repo_pullrequest_sources(
1215 self, repo, commit_id=None, branch=None, bookmark=None):
1240 self, repo, commit_id=None, branch=None, bookmark=None):
1216 """
1241 """
1217 Return a structure with repo's interesting commits, suitable for
1242 Return a structure with repo's interesting commits, suitable for
1218 the selectors in pullrequest controller
1243 the selectors in pullrequest controller
1219
1244
1220 :param commit_id: a commit that must be in the list somehow
1245 :param commit_id: a commit that must be in the list somehow
1221 and selected by default
1246 and selected by default
1222 :param branch: a branch that must be in the list and selected
1247 :param branch: a branch that must be in the list and selected
1223 by default - even if closed
1248 by default - even if closed
1224 :param bookmark: a bookmark that must be in the list and selected
1249 :param bookmark: a bookmark that must be in the list and selected
1225 """
1250 """
1226
1251
1227 commit_id = safe_str(commit_id) if commit_id else None
1252 commit_id = safe_str(commit_id) if commit_id else None
1228 branch = safe_str(branch) if branch else None
1253 branch = safe_str(branch) if branch else None
1229 bookmark = safe_str(bookmark) if bookmark else None
1254 bookmark = safe_str(bookmark) if bookmark else None
1230
1255
1231 selected = None
1256 selected = None
1232
1257
1233 # order matters: first source that has commit_id in it will be selected
1258 # order matters: first source that has commit_id in it will be selected
1234 sources = []
1259 sources = []
1235 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1260 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1236 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1261 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1237
1262
1238 if commit_id:
1263 if commit_id:
1239 ref_commit = (h.short_id(commit_id), commit_id)
1264 ref_commit = (h.short_id(commit_id), commit_id)
1240 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1265 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1241
1266
1242 sources.append(
1267 sources.append(
1243 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1268 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1244 )
1269 )
1245
1270
1246 groups = []
1271 groups = []
1247 for group_key, ref_list, group_name, match in sources:
1272 for group_key, ref_list, group_name, match in sources:
1248 group_refs = []
1273 group_refs = []
1249 for ref_name, ref_id in ref_list:
1274 for ref_name, ref_id in ref_list:
1250 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1275 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1251 group_refs.append((ref_key, ref_name))
1276 group_refs.append((ref_key, ref_name))
1252
1277
1253 if not selected:
1278 if not selected:
1254 if set([commit_id, match]) & set([ref_id, ref_name]):
1279 if set([commit_id, match]) & set([ref_id, ref_name]):
1255 selected = ref_key
1280 selected = ref_key
1256
1281
1257 if group_refs:
1282 if group_refs:
1258 groups.append((group_refs, group_name))
1283 groups.append((group_refs, group_name))
1259
1284
1260 if not selected:
1285 if not selected:
1261 ref = commit_id or branch or bookmark
1286 ref = commit_id or branch or bookmark
1262 if ref:
1287 if ref:
1263 raise CommitDoesNotExistError(
1288 raise CommitDoesNotExistError(
1264 'No commit refs could be found matching: %s' % ref)
1289 'No commit refs could be found matching: %s' % ref)
1265 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1290 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1266 selected = 'branch:%s:%s' % (
1291 selected = 'branch:%s:%s' % (
1267 repo.DEFAULT_BRANCH_NAME,
1292 repo.DEFAULT_BRANCH_NAME,
1268 repo.branches[repo.DEFAULT_BRANCH_NAME]
1293 repo.branches[repo.DEFAULT_BRANCH_NAME]
1269 )
1294 )
1270 elif repo.commit_ids:
1295 elif repo.commit_ids:
1271 rev = repo.commit_ids[0]
1296 rev = repo.commit_ids[0]
1272 selected = 'rev:%s:%s' % (rev, rev)
1297 selected = 'rev:%s:%s' % (rev, rev)
1273 else:
1298 else:
1274 raise EmptyRepositoryError()
1299 raise EmptyRepositoryError()
1275 return groups, selected
1300 return groups, selected
1276
1301
1277 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1302 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1278 return self._get_diff_from_pr_or_version(
1303 return self._get_diff_from_pr_or_version(
1279 source_repo, source_ref_id, target_ref_id, context=context)
1304 source_repo, source_ref_id, target_ref_id, context=context)
1280
1305
1281 def _get_diff_from_pr_or_version(
1306 def _get_diff_from_pr_or_version(
1282 self, source_repo, source_ref_id, target_ref_id, context):
1307 self, source_repo, source_ref_id, target_ref_id, context):
1283 target_commit = source_repo.get_commit(
1308 target_commit = source_repo.get_commit(
1284 commit_id=safe_str(target_ref_id))
1309 commit_id=safe_str(target_ref_id))
1285 source_commit = source_repo.get_commit(
1310 source_commit = source_repo.get_commit(
1286 commit_id=safe_str(source_ref_id))
1311 commit_id=safe_str(source_ref_id))
1287 if isinstance(source_repo, Repository):
1312 if isinstance(source_repo, Repository):
1288 vcs_repo = source_repo.scm_instance()
1313 vcs_repo = source_repo.scm_instance()
1289 else:
1314 else:
1290 vcs_repo = source_repo
1315 vcs_repo = source_repo
1291
1316
1292 # TODO: johbo: In the context of an update, we cannot reach
1317 # TODO: johbo: In the context of an update, we cannot reach
1293 # the old commit anymore with our normal mechanisms. It needs
1318 # the old commit anymore with our normal mechanisms. It needs
1294 # some sort of special support in the vcs layer to avoid this
1319 # some sort of special support in the vcs layer to avoid this
1295 # workaround.
1320 # workaround.
1296 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1321 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1297 vcs_repo.alias == 'git'):
1322 vcs_repo.alias == 'git'):
1298 source_commit.raw_id = safe_str(source_ref_id)
1323 source_commit.raw_id = safe_str(source_ref_id)
1299
1324
1300 log.debug('calculating diff between '
1325 log.debug('calculating diff between '
1301 'source_ref:%s and target_ref:%s for repo `%s`',
1326 'source_ref:%s and target_ref:%s for repo `%s`',
1302 target_ref_id, source_ref_id,
1327 target_ref_id, source_ref_id,
1303 safe_unicode(vcs_repo.path))
1328 safe_unicode(vcs_repo.path))
1304
1329
1305 vcs_diff = vcs_repo.get_diff(
1330 vcs_diff = vcs_repo.get_diff(
1306 commit1=target_commit, commit2=source_commit, context=context)
1331 commit1=target_commit, commit2=source_commit, context=context)
1307 return vcs_diff
1332 return vcs_diff
1308
1333
1309 def _is_merge_enabled(self, pull_request):
1334 def _is_merge_enabled(self, pull_request):
1310 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1335 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1311 settings = settings_model.get_general_settings()
1336 settings = settings_model.get_general_settings()
1312 return settings.get('rhodecode_pr_merge_enabled', False)
1337 return settings.get('rhodecode_pr_merge_enabled', False)
1313
1338
1314 def _use_rebase_for_merging(self, pull_request):
1339 def _use_rebase_for_merging(self, pull_request):
1315 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1340 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1316 settings = settings_model.get_general_settings()
1341 settings = settings_model.get_general_settings()
1317 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1342 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1318
1343
1319 def _log_action(self, action, user, pull_request):
1344 def _log_action(self, action, user, pull_request):
1320 action_logger(
1345 action_logger(
1321 user,
1346 user,
1322 '{action}:{pr_id}'.format(
1347 '{action}:{pr_id}'.format(
1323 action=action, pr_id=pull_request.pull_request_id),
1348 action=action, pr_id=pull_request.pull_request_id),
1324 pull_request.target_repo)
1349 pull_request.target_repo)
1325
1350
1326
1351
1327 class MergeCheck(object):
1352 class MergeCheck(object):
1328 """
1353 """
1329 Perform Merge Checks and returns a check object which stores information
1354 Perform Merge Checks and returns a check object which stores information
1330 about merge errors, and merge conditions
1355 about merge errors, and merge conditions
1331 """
1356 """
1332 TODO_CHECK = 'todo'
1357 TODO_CHECK = 'todo'
1333 PERM_CHECK = 'perm'
1358 PERM_CHECK = 'perm'
1334 REVIEW_CHECK = 'review'
1359 REVIEW_CHECK = 'review'
1335 MERGE_CHECK = 'merge'
1360 MERGE_CHECK = 'merge'
1336
1361
1337 def __init__(self):
1362 def __init__(self):
1338 self.review_status = None
1363 self.review_status = None
1339 self.merge_possible = None
1364 self.merge_possible = None
1340 self.merge_msg = ''
1365 self.merge_msg = ''
1341 self.failed = None
1366 self.failed = None
1342 self.errors = []
1367 self.errors = []
1343 self.error_details = OrderedDict()
1368 self.error_details = OrderedDict()
1344
1369
1345 def push_error(self, error_type, message, error_key, details):
1370 def push_error(self, error_type, message, error_key, details):
1346 self.failed = True
1371 self.failed = True
1347 self.errors.append([error_type, message])
1372 self.errors.append([error_type, message])
1348 self.error_details[error_key] = dict(
1373 self.error_details[error_key] = dict(
1349 details=details,
1374 details=details,
1350 error_type=error_type,
1375 error_type=error_type,
1351 message=message
1376 message=message
1352 )
1377 )
1353
1378
1354 @classmethod
1379 @classmethod
1355 def validate(cls, pull_request, user, fail_early=False, translator=None):
1380 def validate(cls, pull_request, user, fail_early=False, translator=None):
1356 # if migrated to pyramid...
1381 # if migrated to pyramid...
1357 # _ = lambda: translator or _ # use passed in translator if any
1382 # _ = lambda: translator or _ # use passed in translator if any
1358
1383
1359 merge_check = cls()
1384 merge_check = cls()
1360
1385
1361 # permissions to merge
1386 # permissions to merge
1362 user_allowed_to_merge = PullRequestModel().check_user_merge(
1387 user_allowed_to_merge = PullRequestModel().check_user_merge(
1363 pull_request, user)
1388 pull_request, user)
1364 if not user_allowed_to_merge:
1389 if not user_allowed_to_merge:
1365 log.debug("MergeCheck: cannot merge, approval is pending.")
1390 log.debug("MergeCheck: cannot merge, approval is pending.")
1366
1391
1367 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1392 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1368 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1393 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1369 if fail_early:
1394 if fail_early:
1370 return merge_check
1395 return merge_check
1371
1396
1372 # review status, must be always present
1397 # review status, must be always present
1373 review_status = pull_request.calculated_review_status()
1398 review_status = pull_request.calculated_review_status()
1374 merge_check.review_status = review_status
1399 merge_check.review_status = review_status
1375
1400
1376 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1401 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1377 if not status_approved:
1402 if not status_approved:
1378 log.debug("MergeCheck: cannot merge, approval is pending.")
1403 log.debug("MergeCheck: cannot merge, approval is pending.")
1379
1404
1380 msg = _('Pull request reviewer approval is pending.')
1405 msg = _('Pull request reviewer approval is pending.')
1381
1406
1382 merge_check.push_error(
1407 merge_check.push_error(
1383 'warning', msg, cls.REVIEW_CHECK, review_status)
1408 'warning', msg, cls.REVIEW_CHECK, review_status)
1384
1409
1385 if fail_early:
1410 if fail_early:
1386 return merge_check
1411 return merge_check
1387
1412
1388 # left over TODOs
1413 # left over TODOs
1389 todos = CommentsModel().get_unresolved_todos(pull_request)
1414 todos = CommentsModel().get_unresolved_todos(pull_request)
1390 if todos:
1415 if todos:
1391 log.debug("MergeCheck: cannot merge, {} "
1416 log.debug("MergeCheck: cannot merge, {} "
1392 "unresolved todos left.".format(len(todos)))
1417 "unresolved todos left.".format(len(todos)))
1393
1418
1394 if len(todos) == 1:
1419 if len(todos) == 1:
1395 msg = _('Cannot merge, {} TODO still not resolved.').format(
1420 msg = _('Cannot merge, {} TODO still not resolved.').format(
1396 len(todos))
1421 len(todos))
1397 else:
1422 else:
1398 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1423 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1399 len(todos))
1424 len(todos))
1400
1425
1401 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1426 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1402
1427
1403 if fail_early:
1428 if fail_early:
1404 return merge_check
1429 return merge_check
1405
1430
1406 # merge possible
1431 # merge possible
1407 merge_status, msg = PullRequestModel().merge_status(pull_request)
1432 merge_status, msg = PullRequestModel().merge_status(pull_request)
1408 merge_check.merge_possible = merge_status
1433 merge_check.merge_possible = merge_status
1409 merge_check.merge_msg = msg
1434 merge_check.merge_msg = msg
1410 if not merge_status:
1435 if not merge_status:
1411 log.debug(
1436 log.debug(
1412 "MergeCheck: cannot merge, pull request merge not possible.")
1437 "MergeCheck: cannot merge, pull request merge not possible.")
1413 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1438 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1414
1439
1415 if fail_early:
1440 if fail_early:
1416 return merge_check
1441 return merge_check
1417
1442
1418 return merge_check
1443 return merge_check
1419
1444
1420
1445
1421 ChangeTuple = namedtuple('ChangeTuple',
1446 ChangeTuple = namedtuple('ChangeTuple',
1422 ['added', 'common', 'removed', 'total'])
1447 ['added', 'common', 'removed', 'total'])
1423
1448
1424 FileChangeTuple = namedtuple('FileChangeTuple',
1449 FileChangeTuple = namedtuple('FileChangeTuple',
1425 ['added', 'modified', 'removed'])
1450 ['added', 'modified', 'removed'])
@@ -1,826 +1,827 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <span id="pr-title">
12 <span id="pr-title">
13 ${c.pull_request.title}
13 ${c.pull_request.title}
14 %if c.pull_request.is_closed():
14 %if c.pull_request.is_closed():
15 (${_('Closed')})
15 (${_('Closed')})
16 %endif
16 %endif
17 </span>
17 </span>
18 <div id="pr-title-edit" class="input" style="display: none;">
18 <div id="pr-title-edit" class="input" style="display: none;">
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 </div>
20 </div>
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='showpullrequest')}
28 ${self.repo_menu(active='showpullrequest')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32
32
33 <script type="text/javascript">
33 <script type="text/javascript">
34 // TODO: marcink switch this to pyroutes
34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 </script>
37 </script>
38 <div class="box">
38 <div class="box">
39
39
40 <div class="title">
40 <div class="title">
41 ${self.repo_page_title(c.rhodecode_db_repo)}
41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 </div>
42 </div>
43
43
44 ${self.breadcrumbs()}
44 ${self.breadcrumbs()}
45
45
46 <div class="box pr-summary">
46 <div class="box pr-summary">
47
47
48 <div class="summary-details block-left">
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
50 <div class="pr-details-title">
51 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %if c.allowed_to_update:
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
54 % if c.allowed_to_delete:
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
58 ${h.end_form()}
59 % else:
59 % else:
60 ${_('Delete')}
60 ${_('Delete')}
61 % endif
61 % endif
62 </div>
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
65 %endif
66 </div>
66 </div>
67
67
68 <div id="summary" class="fields pr-details-content">
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
69 <div class="field">
70 <div class="label-summary">
70 <div class="label-summary">
71 <label>${_('Origin')}:</label>
71 <label>${_('Origin')}:</label>
72 </div>
72 </div>
73 <div class="input">
73 <div class="input">
74 <div class="pr-origininfo">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
75 ## branch link is only valid if it is a branch
76 <span class="tag">
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
81 %endif
82 </span>
82 </span>
83 <span class="clone-url">
83 <span class="clone-url">
84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
85 </span>
86 <br/>
86 <br/>
87 % if c.ancestor_commit:
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
88 ${_('Common ancestor')}:
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
90 % endif
91 </div>
91 </div>
92 <div class="pr-pullinfo">
92 <div class="pr-pullinfo">
93 %if h.is_hg(c.pull_request.source_repo):
93 %if h.is_hg(c.pull_request.source_repo):
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 %elif h.is_git(c.pull_request.source_repo):
95 %elif h.is_git(c.pull_request.source_repo):
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 %endif
97 %endif
98 </div>
98 </div>
99 </div>
99 </div>
100 </div>
100 </div>
101 <div class="field">
101 <div class="field">
102 <div class="label-summary">
102 <div class="label-summary">
103 <label>${_('Target')}:</label>
103 <label>${_('Target')}:</label>
104 </div>
104 </div>
105 <div class="input">
105 <div class="input">
106 <div class="pr-targetinfo">
106 <div class="pr-targetinfo">
107 ## branch link is only valid if it is a branch
107 ## branch link is only valid if it is a branch
108 <span class="tag">
108 <span class="tag">
109 %if c.pull_request.target_ref_parts.type == 'branch':
109 %if c.pull_request.target_ref_parts.type == 'branch':
110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 %else:
111 %else:
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 %endif
113 %endif
114 </span>
114 </span>
115 <span class="clone-url">
115 <span class="clone-url">
116 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
116 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 </span>
117 </span>
118 </div>
118 </div>
119 </div>
119 </div>
120 </div>
120 </div>
121
121
122 ## Link to the shadow repository.
122 ## Link to the shadow repository.
123 <div class="field">
123 <div class="field">
124 <div class="label-summary">
124 <div class="label-summary">
125 <label>${_('Merge')}:</label>
125 <label>${_('Merge')}:</label>
126 </div>
126 </div>
127 <div class="input">
127 <div class="input">
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 <div class="pr-mergeinfo">
129 <div class="pr-mergeinfo">
130 %if h.is_hg(c.pull_request.target_repo):
130 %if h.is_hg(c.pull_request.target_repo):
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 %elif h.is_git(c.pull_request.target_repo):
132 %elif h.is_git(c.pull_request.target_repo):
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 %endif
134 %endif
135 </div>
135 </div>
136 % else:
136 % else:
137 <div class="">
137 <div class="">
138 ${_('Shadow repository data not available')}.
138 ${_('Shadow repository data not available')}.
139 </div>
139 </div>
140 % endif
140 % endif
141 </div>
141 </div>
142 </div>
142 </div>
143
143
144 <div class="field">
144 <div class="field">
145 <div class="label-summary">
145 <div class="label-summary">
146 <label>${_('Review')}:</label>
146 <label>${_('Review')}:</label>
147 </div>
147 </div>
148 <div class="input">
148 <div class="input">
149 %if c.pull_request_review_status:
149 %if c.pull_request_review_status:
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 <span class="changeset-status-lbl tooltip">
151 <span class="changeset-status-lbl tooltip">
152 %if c.pull_request.is_closed():
152 %if c.pull_request.is_closed():
153 ${_('Closed')},
153 ${_('Closed')},
154 %endif
154 %endif
155 ${h.commit_status_lbl(c.pull_request_review_status)}
155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 </span>
156 </span>
157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 %endif
158 %endif
159 </div>
159 </div>
160 </div>
160 </div>
161 <div class="field">
161 <div class="field">
162 <div class="pr-description-label label-summary">
162 <div class="pr-description-label label-summary">
163 <label>${_('Description')}:</label>
163 <label>${_('Description')}:</label>
164 </div>
164 </div>
165 <div id="pr-desc" class="input">
165 <div id="pr-desc" class="input">
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 </div>
167 </div>
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 </div>
170 </div>
171 </div>
171 </div>
172
172
173 <div class="field">
173 <div class="field">
174 <div class="label-summary">
174 <div class="label-summary">
175 <label>${_('Versions')}:</label>
175 <label>${_('Versions')}:</label>
176 </div>
176 </div>
177
177
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180
180
181 <div class="pr-versions">
181 <div class="pr-versions">
182 % if c.show_version_changes:
182 % if c.show_version_changes:
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 data-toggle-off="${_('Hide all versions of this pull request')}">
187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 </a>
189 </a>
190 <table>
190 <table>
191 ## SHOW ALL VERSIONS OF PR
191 ## SHOW ALL VERSIONS OF PR
192 <% ver_pr = None %>
192 <% ver_pr = None %>
193
193
194 % for data in reversed(list(enumerate(c.versions, 1))):
194 % for data in reversed(list(enumerate(c.versions, 1))):
195 <% ver_pos = data[0] %>
195 <% ver_pos = data[0] %>
196 <% ver = data[1] %>
196 <% ver = data[1] %>
197 <% ver_pr = ver.pull_request_version_id %>
197 <% ver_pr = ver.pull_request_version_id %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199
199
200 <tr class="version-pr" style="display: ${display_row}">
200 <tr class="version-pr" style="display: ${display_row}">
201 <td>
201 <td>
202 <code>
202 <code>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
204 </code>
204 </code>
205 </td>
205 </td>
206 <td>
206 <td>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 </td>
209 </td>
210 <td>
210 <td>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 </div>
213 </div>
214 </td>
214 </td>
215 <td>
215 <td>
216 % if c.at_version_num != ver_pr:
216 % if c.at_version_num != ver_pr:
217 <i class="icon-comment"></i>
217 <i class="icon-comment"></i>
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 </code>
220 </code>
221 % endif
221 % endif
222 </td>
222 </td>
223 <td>
223 <td>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 </td>
225 </td>
226 <td>
226 <td>
227 ${h.age_component(ver.updated_on, time_is_local=True)}
227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 </td>
228 </td>
229 </tr>
229 </tr>
230 % endfor
230 % endfor
231
231
232 <tr>
232 <tr>
233 <td colspan="6">
233 <td colspan="6">
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 data-label-text-locked="${_('select versions to show changes')}"
235 data-label-text-locked="${_('select versions to show changes')}"
236 data-label-text-diff="${_('show changes between versions')}"
236 data-label-text-diff="${_('show changes between versions')}"
237 data-label-text-show="${_('show pull request for this version')}"
237 data-label-text-show="${_('show pull request for this version')}"
238 >
238 >
239 ${_('select versions to show changes')}
239 ${_('select versions to show changes')}
240 </button>
240 </button>
241 </td>
241 </td>
242 </tr>
242 </tr>
243
243
244 ## show comment/inline comments summary
244 ## show comment/inline comments summary
245 <%def name="comments_summary()">
245 <%def name="comments_summary()">
246 <tr>
246 <tr>
247 <td colspan="6" class="comments-summary-td">
247 <td colspan="6" class="comments-summary-td">
248
248
249 % if c.at_version:
249 % if c.at_version:
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 ${_('Comments at this version')}:
252 ${_('Comments at this version')}:
253 % else:
253 % else:
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 ${_('Comments for this pull request')}:
256 ${_('Comments for this pull request')}:
257 % endif
257 % endif
258
258
259
259
260 %if general_comm_count_ver:
260 %if general_comm_count_ver:
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 %else:
262 %else:
263 ${_("%d General ") % general_comm_count_ver}
263 ${_("%d General ") % general_comm_count_ver}
264 %endif
264 %endif
265
265
266 %if inline_comm_count_ver:
266 %if inline_comm_count_ver:
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 %else:
268 %else:
269 , ${_("%d Inline") % inline_comm_count_ver}
269 , ${_("%d Inline") % inline_comm_count_ver}
270 %endif
270 %endif
271
271
272 %if outdated_comm_count_ver:
272 %if outdated_comm_count_ver:
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 %else:
276 %else:
277 , ${_("%d Outdated") % outdated_comm_count_ver}
277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 %endif
278 %endif
279 </td>
279 </td>
280 </tr>
280 </tr>
281 </%def>
281 </%def>
282 ${comments_summary()}
282 ${comments_summary()}
283 </table>
283 </table>
284 % else:
284 % else:
285 <div class="input">
285 <div class="input">
286 ${_('Pull request versions not available')}.
286 ${_('Pull request versions not available')}.
287 </div>
287 </div>
288 <div>
288 <div>
289 <table>
289 <table>
290 ${comments_summary()}
290 ${comments_summary()}
291 </table>
291 </table>
292 </div>
292 </div>
293 % endif
293 % endif
294 </div>
294 </div>
295 </div>
295 </div>
296
296
297 <div id="pr-save" class="field" style="display: none;">
297 <div id="pr-save" class="field" style="display: none;">
298 <div class="label-summary"></div>
298 <div class="label-summary"></div>
299 <div class="input">
299 <div class="input">
300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
301 </div>
301 </div>
302 </div>
302 </div>
303 </div>
303 </div>
304 </div>
304 </div>
305 <div>
305 <div>
306 ## AUTHOR
306 ## AUTHOR
307 <div class="reviewers-title block-right">
307 <div class="reviewers-title block-right">
308 <div class="pr-details-title">
308 <div class="pr-details-title">
309 ${_('Author')}
309 ${_('Author')}
310 </div>
310 </div>
311 </div>
311 </div>
312 <div class="block-right pr-details-content reviewers">
312 <div class="block-right pr-details-content reviewers">
313 <ul class="group_members">
313 <ul class="group_members">
314 <li>
314 <li>
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 </li>
316 </li>
317 </ul>
317 </ul>
318 </div>
318 </div>
319 ## REVIEWERS
319 ## REVIEWERS
320 <div class="reviewers-title block-right">
320 <div class="reviewers-title block-right">
321 <div class="pr-details-title">
321 <div class="pr-details-title">
322 ${_('Pull request reviewers')}
322 ${_('Pull request reviewers')}
323 %if c.allowed_to_update:
323 %if c.allowed_to_update:
324 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
324 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
326 %endif
326 %endif
327 </div>
327 </div>
328 </div>
328 </div>
329 <div id="reviewers" class="block-right pr-details-content reviewers">
329 <div id="reviewers" class="block-right pr-details-content reviewers">
330 ## members goes here !
330 ## members goes here !
331 <input type="hidden" name="__start__" value="review_members:sequence">
331 <input type="hidden" name="__start__" value="review_members:sequence">
332 <ul id="review_members" class="group_members">
332 <ul id="review_members" class="group_members">
333 %for member,reasons,status in c.pull_request_reviewers:
333 %for member,reasons,status in c.pull_request_reviewers:
334 <li id="reviewer_${member.user_id}">
334 <li id="reviewer_${member.user_id}">
335 <div class="reviewers_member">
335 <div class="reviewers_member">
336 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
336 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
337 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
337 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
338 </div>
338 </div>
339 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
339 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
340 ${self.gravatar_with_user(member.email, 16)}
340 ${self.gravatar_with_user(member.email, 16)}
341 </div>
341 </div>
342 <input type="hidden" name="__start__" value="reviewer:mapping">
342 <input type="hidden" name="__start__" value="reviewer:mapping">
343 <input type="hidden" name="__start__" value="reasons:sequence">
343 <input type="hidden" name="__start__" value="reasons:sequence">
344 %for reason in reasons:
344 %for reason in reasons:
345 <div class="reviewer_reason">- ${reason}</div>
345 <div class="reviewer_reason">- ${reason}</div>
346 <input type="hidden" name="reason" value="${reason}">
346 <input type="hidden" name="reason" value="${reason}">
347
347
348 %endfor
348 %endfor
349 <input type="hidden" name="__end__" value="reasons:sequence">
349 <input type="hidden" name="__end__" value="reasons:sequence">
350 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
350 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
351 <input type="hidden" name="__end__" value="reviewer:mapping">
351 <input type="hidden" name="__end__" value="reviewer:mapping">
352 %if c.allowed_to_update:
352 %if c.allowed_to_update:
353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
354 <i class="icon-remove-sign" ></i>
354 <i class="icon-remove-sign" ></i>
355 </div>
355 </div>
356 %endif
356 %endif
357 </div>
357 </div>
358 </li>
358 </li>
359 %endfor
359 %endfor
360 </ul>
360 </ul>
361 <input type="hidden" name="__end__" value="review_members:sequence">
361 <input type="hidden" name="__end__" value="review_members:sequence">
362 %if not c.pull_request.is_closed():
362 %if not c.pull_request.is_closed():
363 <div id="add_reviewer_input" class='ac' style="display: none;">
363 <div id="add_reviewer_input" class='ac' style="display: none;">
364 %if c.allowed_to_update:
364 %if c.allowed_to_update:
365 <div class="reviewer_ac">
365 <div class="reviewer_ac">
366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
367 <div id="reviewers_container"></div>
367 <div id="reviewers_container"></div>
368 </div>
368 </div>
369 <div>
369 <div>
370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
371 </div>
371 </div>
372 %endif
372 %endif
373 </div>
373 </div>
374 %endif
374 %endif
375 </div>
375 </div>
376 </div>
376 </div>
377 </div>
377 </div>
378 <div class="box">
378 <div class="box">
379 ##DIFF
379 ##DIFF
380 <div class="table" >
380 <div class="table" >
381 <div id="changeset_compare_view_content">
381 <div id="changeset_compare_view_content">
382 ##CS
382 ##CS
383 % if c.missing_requirements:
383 % if c.missing_requirements:
384 <div class="box">
384 <div class="box">
385 <div class="alert alert-warning">
385 <div class="alert alert-warning">
386 <div>
386 <div>
387 <strong>${_('Missing requirements:')}</strong>
387 <strong>${_('Missing requirements:')}</strong>
388 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
388 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
389 </div>
389 </div>
390 </div>
390 </div>
391 </div>
391 </div>
392 % elif c.missing_commits:
392 % elif c.missing_commits:
393 <div class="box">
393 <div class="box">
394 <div class="alert alert-warning">
394 <div class="alert alert-warning">
395 <div>
395 <div>
396 <strong>${_('Missing commits')}:</strong>
396 <strong>${_('Missing commits')}:</strong>
397 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
397 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
398 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
398 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
399 </div>
399 </div>
400 </div>
400 </div>
401 </div>
401 </div>
402 % endif
402 % endif
403
403
404 <div class="compare_view_commits_title">
404 <div class="compare_view_commits_title">
405 % if not c.compare_mode:
405 % if not c.compare_mode:
406
406
407 % if c.at_version_pos:
407 % if c.at_version_pos:
408 <h4>
408 <h4>
409 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
409 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
410 </h4>
410 </h4>
411 % endif
411 % endif
412
412
413 <div class="pull-left">
413 <div class="pull-left">
414 <div class="btn-group">
414 <div class="btn-group">
415 <a
415 <a
416 class="btn"
416 class="btn"
417 href="#"
417 href="#"
418 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
418 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
419 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
419 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
420 </a>
420 </a>
421 <a
421 <a
422 class="btn"
422 class="btn"
423 href="#"
423 href="#"
424 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
424 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
425 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
425 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
426 </a>
426 </a>
427 </div>
427 </div>
428 </div>
428 </div>
429
429
430 <div class="pull-right">
430 <div class="pull-right">
431 % if c.allowed_to_update and not c.pull_request.is_closed():
431 % if c.allowed_to_update and not c.pull_request.is_closed():
432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
433 % else:
433 % else:
434 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
434 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
435 % endif
435 % endif
436
436
437 </div>
437 </div>
438 % endif
438 % endif
439 </div>
439 </div>
440
440
441 % if not c.missing_commits:
441 % if not c.missing_commits:
442 % if c.compare_mode:
442 % if c.compare_mode:
443 % if c.at_version:
443 % if c.at_version:
444 <h4>
444 <h4>
445 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
445 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
446 </h4>
446 </h4>
447
447
448 <div class="subtitle-compare">
448 <div class="subtitle-compare">
449 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
449 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
450 </div>
450 </div>
451
451
452 <div class="container">
452 <div class="container">
453 <table class="rctable compare_view_commits">
453 <table class="rctable compare_view_commits">
454 <tr>
454 <tr>
455 <th></th>
455 <th></th>
456 <th>${_('Time')}</th>
456 <th>${_('Time')}</th>
457 <th>${_('Author')}</th>
457 <th>${_('Author')}</th>
458 <th>${_('Commit')}</th>
458 <th>${_('Commit')}</th>
459 <th></th>
459 <th></th>
460 <th>${_('Description')}</th>
460 <th>${_('Description')}</th>
461 </tr>
461 </tr>
462
462
463 % for c_type, commit in c.commit_changes:
463 % for c_type, commit in c.commit_changes:
464 % if c_type in ['a', 'r']:
464 % if c_type in ['a', 'r']:
465 <%
465 <%
466 if c_type == 'a':
466 if c_type == 'a':
467 cc_title = _('Commit added in displayed changes')
467 cc_title = _('Commit added in displayed changes')
468 elif c_type == 'r':
468 elif c_type == 'r':
469 cc_title = _('Commit removed in displayed changes')
469 cc_title = _('Commit removed in displayed changes')
470 else:
470 else:
471 cc_title = ''
471 cc_title = ''
472 %>
472 %>
473 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
473 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
474 <td>
474 <td>
475 <div class="commit-change-indicator color-${c_type}-border">
475 <div class="commit-change-indicator color-${c_type}-border">
476 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
476 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
477 ${c_type.upper()}
477 ${c_type.upper()}
478 </div>
478 </div>
479 </div>
479 </div>
480 </td>
480 </td>
481 <td class="td-time">
481 <td class="td-time">
482 ${h.age_component(commit.date)}
482 ${h.age_component(commit.date)}
483 </td>
483 </td>
484 <td class="td-user">
484 <td class="td-user">
485 ${base.gravatar_with_user(commit.author, 16)}
485 ${base.gravatar_with_user(commit.author, 16)}
486 </td>
486 </td>
487 <td class="td-hash">
487 <td class="td-hash">
488 <code>
488 <code>
489 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
489 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
490 r${commit.revision}:${h.short_id(commit.raw_id)}
490 r${commit.revision}:${h.short_id(commit.raw_id)}
491 </a>
491 </a>
492 ${h.hidden('revisions', commit.raw_id)}
492 ${h.hidden('revisions', commit.raw_id)}
493 </code>
493 </code>
494 </td>
494 </td>
495 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
495 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
496 <div class="show_more_col">
496 <div class="show_more_col">
497 <i class="show_more"></i>
497 <i class="show_more"></i>
498 </div>
498 </div>
499 </td>
499 </td>
500 <td class="mid td-description">
500 <td class="mid td-description">
501 <div class="log-container truncate-wrap">
501 <div class="log-container truncate-wrap">
502 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
502 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
503 ${h.urlify_commit_message(commit.message, c.repo_name)}
503 ${h.urlify_commit_message(commit.message, c.repo_name)}
504 </div>
504 </div>
505 </div>
505 </div>
506 </td>
506 </td>
507 </tr>
507 </tr>
508 % endif
508 % endif
509 % endfor
509 % endfor
510 </table>
510 </table>
511 </div>
511 </div>
512
512
513 <script>
513 <script>
514 $('.expand_commit').on('click',function(e){
514 $('.expand_commit').on('click',function(e){
515 var target_expand = $(this);
515 var target_expand = $(this);
516 var cid = target_expand.data('commitId');
516 var cid = target_expand.data('commitId');
517
517
518 if (target_expand.hasClass('open')){
518 if (target_expand.hasClass('open')){
519 $('#c-'+cid).css({
519 $('#c-'+cid).css({
520 'height': '1.5em',
520 'height': '1.5em',
521 'white-space': 'nowrap',
521 'white-space': 'nowrap',
522 'text-overflow': 'ellipsis',
522 'text-overflow': 'ellipsis',
523 'overflow':'hidden'
523 'overflow':'hidden'
524 });
524 });
525 target_expand.removeClass('open');
525 target_expand.removeClass('open');
526 }
526 }
527 else {
527 else {
528 $('#c-'+cid).css({
528 $('#c-'+cid).css({
529 'height': 'auto',
529 'height': 'auto',
530 'white-space': 'pre-line',
530 'white-space': 'pre-line',
531 'text-overflow': 'initial',
531 'text-overflow': 'initial',
532 'overflow':'visible'
532 'overflow':'visible'
533 });
533 });
534 target_expand.addClass('open');
534 target_expand.addClass('open');
535 }
535 }
536 });
536 });
537 </script>
537 </script>
538
538
539 % endif
539 % endif
540
540
541 % else:
541 % else:
542 <%include file="/compare/compare_commits.mako" />
542 <%include file="/compare/compare_commits.mako" />
543 % endif
543 % endif
544
544
545 <div class="cs_files">
545 <div class="cs_files">
546 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
546 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
547 ${cbdiffs.render_diffset_menu()}
547 ${cbdiffs.render_diffset_menu()}
548 ${cbdiffs.render_diffset(
548 ${cbdiffs.render_diffset(
549 c.diffset, use_comments=True,
549 c.diffset, use_comments=True,
550 collapse_when_files_over=30,
550 collapse_when_files_over=30,
551 disable_new_comments=not c.allowed_to_comment,
551 disable_new_comments=not c.allowed_to_comment,
552 deleted_files_comments=c.deleted_files_comments)}
552 deleted_files_comments=c.deleted_files_comments)}
553 </div>
553 </div>
554 % else:
554 % else:
555 ## skipping commits we need to clear the view for missing commits
555 ## skipping commits we need to clear the view for missing commits
556 <div style="clear:both;"></div>
556 <div style="clear:both;"></div>
557 % endif
557 % endif
558
558
559 </div>
559 </div>
560 </div>
560 </div>
561
561
562 ## template for inline comment form
562 ## template for inline comment form
563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
564
564
565 ## render general comments
565 ## render general comments
566
566
567 <div id="comment-tr-show">
567 <div id="comment-tr-show">
568 <div class="comment">
568 <div class="comment">
569 % if general_outdated_comm_count_ver:
569 % if general_outdated_comm_count_ver:
570 <div class="meta">
570 <div class="meta">
571 % if general_outdated_comm_count_ver == 1:
571 % if general_outdated_comm_count_ver == 1:
572 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
572 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
573 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
573 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
574 % else:
574 % else:
575 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
575 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
576 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
576 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
577 % endif
577 % endif
578 </div>
578 </div>
579 % endif
579 % endif
580 </div>
580 </div>
581 </div>
581 </div>
582
582
583 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
583 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
584
584
585 % if not c.pull_request.is_closed():
585 % if not c.pull_request.is_closed():
586 ## merge status, and merge action
586 ## merge status, and merge action
587 <div class="pull-request-merge">
587 <div class="pull-request-merge">
588 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
588 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
589 </div>
589 </div>
590
590
591 ## main comment form and it status
591 ## main comment form and it status
592 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
592 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
593 pull_request_id=c.pull_request.pull_request_id),
593 pull_request_id=c.pull_request.pull_request_id),
594 c.pull_request_review_status,
594 c.pull_request_review_status,
595 is_pull_request=True, change_status=c.allowed_to_change_status)}
595 is_pull_request=True, change_status=c.allowed_to_change_status)}
596 %endif
596 %endif
597
597
598 <script type="text/javascript">
598 <script type="text/javascript">
599 if (location.hash) {
599 if (location.hash) {
600 var result = splitDelimitedHash(location.hash);
600 var result = splitDelimitedHash(location.hash);
601 var line = $('html').find(result.loc);
601 var line = $('html').find(result.loc);
602 // show hidden comments if we use location.hash
602 // show hidden comments if we use location.hash
603 if (line.hasClass('comment-general')) {
603 if (line.hasClass('comment-general')) {
604 $(line).show();
604 $(line).show();
605 } else if (line.hasClass('comment-inline')) {
605 } else if (line.hasClass('comment-inline')) {
606 $(line).show();
606 $(line).show();
607 var $cb = $(line).closest('.cb');
607 var $cb = $(line).closest('.cb');
608 $cb.removeClass('cb-collapsed')
608 $cb.removeClass('cb-collapsed')
609 }
609 }
610 if (line.length > 0){
610 if (line.length > 0){
611 offsetScroll(line, 70);
611 offsetScroll(line, 70);
612 }
612 }
613 }
613 }
614
614
615 versionController = new VersionController();
615 versionController = new VersionController();
616 versionController.init();
616 versionController.init();
617
617
618
618
619 $(function(){
619 $(function(){
620 ReviewerAutoComplete('user');
620 ReviewerAutoComplete('user');
621 // custom code mirror
621 // custom code mirror
622 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
622 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
623
623
624 var PRDetails = {
624 var PRDetails = {
625 editButton: $('#open_edit_pullrequest'),
625 editButton: $('#open_edit_pullrequest'),
626 closeButton: $('#close_edit_pullrequest'),
626 closeButton: $('#close_edit_pullrequest'),
627 deleteButton: $('#delete_pullrequest'),
627 deleteButton: $('#delete_pullrequest'),
628 viewFields: $('#pr-desc, #pr-title'),
628 viewFields: $('#pr-desc, #pr-title'),
629 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
629 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
630
630
631 init: function() {
631 init: function() {
632 var that = this;
632 var that = this;
633 this.editButton.on('click', function(e) { that.edit(); });
633 this.editButton.on('click', function(e) { that.edit(); });
634 this.closeButton.on('click', function(e) { that.view(); });
634 this.closeButton.on('click', function(e) { that.view(); });
635 },
635 },
636
636
637 edit: function(event) {
637 edit: function(event) {
638 this.viewFields.hide();
638 this.viewFields.hide();
639 this.editButton.hide();
639 this.editButton.hide();
640 this.deleteButton.hide();
640 this.deleteButton.hide();
641 this.closeButton.show();
641 this.closeButton.show();
642 this.editFields.show();
642 this.editFields.show();
643 codeMirrorInstance.refresh();
643 codeMirrorInstance.refresh();
644 },
644 },
645
645
646 view: function(event) {
646 view: function(event) {
647 this.editButton.show();
647 this.editButton.show();
648 this.deleteButton.show();
648 this.deleteButton.show();
649 this.editFields.hide();
649 this.editFields.hide();
650 this.closeButton.hide();
650 this.closeButton.hide();
651 this.viewFields.show();
651 this.viewFields.show();
652 }
652 }
653 };
653 };
654
654
655 var ReviewersPanel = {
655 var ReviewersPanel = {
656 editButton: $('#open_edit_reviewers'),
656 editButton: $('#open_edit_reviewers'),
657 closeButton: $('#close_edit_reviewers'),
657 closeButton: $('#close_edit_reviewers'),
658 addButton: $('#add_reviewer_input'),
658 addButton: $('#add_reviewer_input'),
659 removeButtons: $('.reviewer_member_remove'),
659 removeButtons: $('.reviewer_member_remove'),
660
660
661 init: function() {
661 init: function() {
662 var that = this;
662 var that = this;
663 this.editButton.on('click', function(e) { that.edit(); });
663 this.editButton.on('click', function(e) { that.edit(); });
664 this.closeButton.on('click', function(e) { that.close(); });
664 this.closeButton.on('click', function(e) { that.close(); });
665 },
665 },
666
666
667 edit: function(event) {
667 edit: function(event) {
668 this.editButton.hide();
668 this.editButton.hide();
669 this.closeButton.show();
669 this.closeButton.show();
670 this.addButton.show();
670 this.addButton.show();
671 this.removeButtons.css('visibility', 'visible');
671 this.removeButtons.css('visibility', 'visible');
672 },
672 },
673
673
674 close: function(event) {
674 close: function(event) {
675 this.editButton.show();
675 this.editButton.show();
676 this.closeButton.hide();
676 this.closeButton.hide();
677 this.addButton.hide();
677 this.addButton.hide();
678 this.removeButtons.css('visibility', 'hidden');
678 this.removeButtons.css('visibility', 'hidden');
679 }
679 }
680 };
680 };
681
681
682 PRDetails.init();
682 PRDetails.init();
683 ReviewersPanel.init();
683 ReviewersPanel.init();
684
684
685 showOutdated = function(self){
685 showOutdated = function(self){
686 $('.comment-inline.comment-outdated').show();
686 $('.comment-inline.comment-outdated').show();
687 $('.filediff-outdated').show();
687 $('.filediff-outdated').show();
688 $('.showOutdatedComments').hide();
688 $('.showOutdatedComments').hide();
689 $('.hideOutdatedComments').show();
689 $('.hideOutdatedComments').show();
690 };
690 };
691
691
692 hideOutdated = function(self){
692 hideOutdated = function(self){
693 $('.comment-inline.comment-outdated').hide();
693 $('.comment-inline.comment-outdated').hide();
694 $('.filediff-outdated').hide();
694 $('.filediff-outdated').hide();
695 $('.hideOutdatedComments').hide();
695 $('.hideOutdatedComments').hide();
696 $('.showOutdatedComments').show();
696 $('.showOutdatedComments').show();
697 };
697 };
698
698
699 refreshMergeChecks = function(){
699 refreshMergeChecks = function(){
700 var loadUrl = "${h.url.current(merge_checks=1)}";
700 var loadUrl = "${h.url.current(merge_checks=1)}";
701 $('.pull-request-merge').css('opacity', 0.3);
701 $('.pull-request-merge').css('opacity', 0.3);
702 $('.action-buttons-extra').css('opacity', 0.3);
702 $('.action-buttons-extra').css('opacity', 0.3);
703
703
704 $('.pull-request-merge').load(
704 $('.pull-request-merge').load(
705 loadUrl, function() {
705 loadUrl, function() {
706 $('.pull-request-merge').css('opacity', 1);
706 $('.pull-request-merge').css('opacity', 1);
707
707
708 $('.action-buttons-extra').css('opacity', 1);
708 $('.action-buttons-extra').css('opacity', 1);
709 injectCloseAction();
709 injectCloseAction();
710 }
710 }
711 );
711 );
712 };
712 };
713
713
714 injectCloseAction = function() {
714 injectCloseAction = function() {
715 var closeAction = $('#close-pull-request-action').html();
715 var closeAction = $('#close-pull-request-action').html();
716 var $actionButtons = $('.action-buttons-extra');
716 var $actionButtons = $('.action-buttons-extra');
717 // clear the action before
717 // clear the action before
718 $actionButtons.html("");
718 $actionButtons.html("");
719 $actionButtons.html(closeAction);
719 $actionButtons.html(closeAction);
720 };
720 };
721
721
722 closePullRequest = function (status) {
722 closePullRequest = function (status) {
723 // inject closing flag
723 // inject closing flag
724 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
724 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
725 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
725 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
726 $(generalCommentForm.submitForm).submit();
726 $(generalCommentForm.submitForm).submit();
727 };
727 };
728
728
729 $('#show-outdated-comments').on('click', function(e){
729 $('#show-outdated-comments').on('click', function(e){
730 var button = $(this);
730 var button = $(this);
731 var outdated = $('.comment-outdated');
731 var outdated = $('.comment-outdated');
732
732
733 if (button.html() === "(Show)") {
733 if (button.html() === "(Show)") {
734 button.html("(Hide)");
734 button.html("(Hide)");
735 outdated.show();
735 outdated.show();
736 } else {
736 } else {
737 button.html("(Show)");
737 button.html("(Show)");
738 outdated.hide();
738 outdated.hide();
739 }
739 }
740 });
740 });
741
741
742 $('.show-inline-comments').on('change', function(e){
742 $('.show-inline-comments').on('change', function(e){
743 var show = 'none';
743 var show = 'none';
744 var target = e.currentTarget;
744 var target = e.currentTarget;
745 if(target.checked){
745 if(target.checked){
746 show = ''
746 show = ''
747 }
747 }
748 var boxid = $(target).attr('id_for');
748 var boxid = $(target).attr('id_for');
749 var comments = $('#{0} .inline-comments'.format(boxid));
749 var comments = $('#{0} .inline-comments'.format(boxid));
750 var fn_display = function(idx){
750 var fn_display = function(idx){
751 $(this).css('display', show);
751 $(this).css('display', show);
752 };
752 };
753 $(comments).each(fn_display);
753 $(comments).each(fn_display);
754 var btns = $('#{0} .inline-comments-button'.format(boxid));
754 var btns = $('#{0} .inline-comments-button'.format(boxid));
755 $(btns).each(fn_display);
755 $(btns).each(fn_display);
756 });
756 });
757
757
758 $('#merge_pull_request_form').submit(function() {
758 $('#merge_pull_request_form').submit(function() {
759 if (!$('#merge_pull_request').attr('disabled')) {
759 if (!$('#merge_pull_request').attr('disabled')) {
760 $('#merge_pull_request').attr('disabled', 'disabled');
760 $('#merge_pull_request').attr('disabled', 'disabled');
761 }
761 }
762 return true;
762 return true;
763 });
763 });
764
764
765 $('#edit_pull_request').on('click', function(e){
765 $('#edit_pull_request').on('click', function(e){
766 var title = $('#pr-title-input').val();
766 var title = $('#pr-title-input').val();
767 var description = codeMirrorInstance.getValue();
767 var description = codeMirrorInstance.getValue();
768 editPullRequest(
768 editPullRequest(
769 "${c.repo_name}", "${c.pull_request.pull_request_id}",
769 "${c.repo_name}", "${c.pull_request.pull_request_id}",
770 title, description);
770 title, description);
771 });
771 });
772
772
773 $('#update_pull_request').on('click', function(e){
773 $('#update_pull_request').on('click', function(e){
774 $(this).attr('disabled', 'disabled');
774 $(this).attr('disabled', 'disabled');
775 $(this).addClass('disabled');
775 $(this).addClass('disabled');
776 $(this).html(_gettext('saving...'));
776 $(this).html(_gettext('Saving...'));
777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
778 });
778 });
779
779
780 $('#update_commits').on('click', function(e){
780 $('#update_commits').on('click', function(e){
781 var isDisabled = !$(e.currentTarget).attr('disabled');
781 var isDisabled = !$(e.currentTarget).attr('disabled');
782 $(e.currentTarget).attr('disabled', 'disabled');
783 $(e.currentTarget).addClass('disabled');
784 $(e.currentTarget).removeClass('btn-primary');
782 $(e.currentTarget).text(_gettext('Updating...'));
785 $(e.currentTarget).text(_gettext('Updating...'));
783 $(e.currentTarget).attr('disabled', 'disabled');
784 if(isDisabled){
786 if(isDisabled){
785 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
787 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
786 }
788 }
787
788 });
789 });
789 // fixing issue with caches on firefox
790 // fixing issue with caches on firefox
790 $('#update_commits').removeAttr("disabled");
791 $('#update_commits').removeAttr("disabled");
791
792
792 $('#close_pull_request').on('click', function(e){
793 $('#close_pull_request').on('click', function(e){
793 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
794 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
794 });
795 });
795
796
796 $('.show-inline-comments').on('click', function(e){
797 $('.show-inline-comments').on('click', function(e){
797 var boxid = $(this).attr('data-comment-id');
798 var boxid = $(this).attr('data-comment-id');
798 var button = $(this);
799 var button = $(this);
799
800
800 if(button.hasClass("comments-visible")) {
801 if(button.hasClass("comments-visible")) {
801 $('#{0} .inline-comments'.format(boxid)).each(function(index){
802 $('#{0} .inline-comments'.format(boxid)).each(function(index){
802 $(this).hide();
803 $(this).hide();
803 });
804 });
804 button.removeClass("comments-visible");
805 button.removeClass("comments-visible");
805 } else {
806 } else {
806 $('#{0} .inline-comments'.format(boxid)).each(function(index){
807 $('#{0} .inline-comments'.format(boxid)).each(function(index){
807 $(this).show();
808 $(this).show();
808 });
809 });
809 button.addClass("comments-visible");
810 button.addClass("comments-visible");
810 }
811 }
811 });
812 });
812
813
813 // register submit callback on commentForm form to track TODOs
814 // register submit callback on commentForm form to track TODOs
814 window.commentFormGlobalSubmitSuccessCallback = function(){
815 window.commentFormGlobalSubmitSuccessCallback = function(){
815 refreshMergeChecks();
816 refreshMergeChecks();
816 };
817 };
817 // initial injection
818 // initial injection
818 injectCloseAction();
819 injectCloseAction();
819
820
820 })
821 })
821 </script>
822 </script>
822
823
823 </div>
824 </div>
824 </div>
825 </div>
825
826
826 </%def>
827 </%def>
General Comments 0
You need to be logged in to leave comments. Login now