##// END OF EJS Templates
pr-shadow: Adapt to new merge response object.
Martin Bornhold -
r1052:fd5a3923 default
parent child Browse files
Show More
@@ -1,1191 +1,1191 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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
34
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.markup_renderer import (
38 from rhodecode.lib.markup_renderer import (
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 from rhodecode.lib.utils import action_logger
40 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.vcs.backends.base import (
42 from rhodecode.lib.vcs.backends.base import (
43 Reference, MergeResponse, MergeFailureReason)
43 Reference, MergeResponse, MergeFailureReason)
44 from rhodecode.lib.vcs.conf import settings as vcs_settings
44 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, EmptyRepositoryError)
46 CommitDoesNotExistError, EmptyRepositoryError)
47 from rhodecode.model import BaseModel
47 from rhodecode.model import BaseModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.db import (
50 from rhodecode.model.db import (
51 PullRequest, PullRequestReviewers, ChangesetStatus,
51 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequestVersion, ChangesetComment)
52 PullRequestVersion, ChangesetComment)
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.notification import NotificationModel, \
54 from rhodecode.model.notification import NotificationModel, \
55 EmailNotificationModel
55 EmailNotificationModel
56 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.settings import VcsSettingsModel
57 from rhodecode.model.settings import VcsSettingsModel
58
58
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class PullRequestModel(BaseModel):
63 class PullRequestModel(BaseModel):
64
64
65 cls = PullRequest
65 cls = PullRequest
66
66
67 DIFF_CONTEXT = 3
67 DIFF_CONTEXT = 3
68
68
69 MERGE_STATUS_MESSAGES = {
69 MERGE_STATUS_MESSAGES = {
70 MergeFailureReason.NONE: lazy_ugettext(
70 MergeFailureReason.NONE: lazy_ugettext(
71 'This pull request can be automatically merged.'),
71 'This pull request can be automatically merged.'),
72 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 MergeFailureReason.UNKNOWN: lazy_ugettext(
73 'This pull request cannot be merged because of an unhandled'
73 'This pull request cannot be merged because of an unhandled'
74 ' exception.'),
74 ' exception.'),
75 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
76 'This pull request cannot be merged because of conflicts.'),
76 'This pull request cannot be merged because of conflicts.'),
77 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
78 'This pull request could not be merged because push to target'
78 'This pull request could not be merged because push to target'
79 ' failed.'),
79 ' failed.'),
80 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
81 'This pull request cannot be merged because the target is not a'
81 'This pull request cannot be merged because the target is not a'
82 ' head.'),
82 ' head.'),
83 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
84 'This pull request cannot be merged because the source contains'
84 'This pull request cannot be merged because the source contains'
85 ' more branches than the target.'),
85 ' more branches than the target.'),
86 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
87 'This pull request cannot be merged because the target has'
87 'This pull request cannot be merged because the target has'
88 ' multiple heads.'),
88 ' multiple heads.'),
89 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
90 'This pull request cannot be merged because the target repository'
90 'This pull request cannot be merged because the target repository'
91 ' is locked.'),
91 ' is locked.'),
92 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
93 'This pull request cannot be merged because the target or the '
93 'This pull request cannot be merged because the target or the '
94 'source reference is missing.'),
94 'source reference is missing.'),
95 }
95 }
96
96
97 def __get_pull_request(self, pull_request):
97 def __get_pull_request(self, pull_request):
98 return self._get_instance(PullRequest, pull_request)
98 return self._get_instance(PullRequest, pull_request)
99
99
100 def _check_perms(self, perms, pull_request, user, api=False):
100 def _check_perms(self, perms, pull_request, user, api=False):
101 if not api:
101 if not api:
102 return h.HasRepoPermissionAny(*perms)(
102 return h.HasRepoPermissionAny(*perms)(
103 user=user, repo_name=pull_request.target_repo.repo_name)
103 user=user, repo_name=pull_request.target_repo.repo_name)
104 else:
104 else:
105 return h.HasRepoPermissionAnyApi(*perms)(
105 return h.HasRepoPermissionAnyApi(*perms)(
106 user=user, repo_name=pull_request.target_repo.repo_name)
106 user=user, repo_name=pull_request.target_repo.repo_name)
107
107
108 def check_user_read(self, pull_request, user, api=False):
108 def check_user_read(self, pull_request, user, api=False):
109 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 _perms = ('repository.admin', 'repository.write', 'repository.read',)
110 return self._check_perms(_perms, pull_request, user, api)
110 return self._check_perms(_perms, pull_request, user, api)
111
111
112 def check_user_merge(self, pull_request, user, api=False):
112 def check_user_merge(self, pull_request, user, api=False):
113 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
113 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
114 return self._check_perms(_perms, pull_request, user, api)
114 return self._check_perms(_perms, pull_request, user, api)
115
115
116 def check_user_update(self, pull_request, user, api=False):
116 def check_user_update(self, pull_request, user, api=False):
117 owner = user.user_id == pull_request.user_id
117 owner = user.user_id == pull_request.user_id
118 return self.check_user_merge(pull_request, user, api) or owner
118 return self.check_user_merge(pull_request, user, api) or owner
119
119
120 def check_user_change_status(self, pull_request, user, api=False):
120 def check_user_change_status(self, pull_request, user, api=False):
121 reviewer = user.user_id in [x.user_id for x in
121 reviewer = user.user_id in [x.user_id for x in
122 pull_request.reviewers]
122 pull_request.reviewers]
123 return self.check_user_update(pull_request, user, api) or reviewer
123 return self.check_user_update(pull_request, user, api) or reviewer
124
124
125 def get(self, pull_request):
125 def get(self, pull_request):
126 return self.__get_pull_request(pull_request)
126 return self.__get_pull_request(pull_request)
127
127
128 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
129 opened_by=None, order_by=None,
129 opened_by=None, order_by=None,
130 order_dir='desc'):
130 order_dir='desc'):
131 repo = self._get_repo(repo_name)
131 repo = self._get_repo(repo_name)
132 q = PullRequest.query()
132 q = PullRequest.query()
133 # source or target
133 # source or target
134 if source:
134 if source:
135 q = q.filter(PullRequest.source_repo == repo)
135 q = q.filter(PullRequest.source_repo == repo)
136 else:
136 else:
137 q = q.filter(PullRequest.target_repo == repo)
137 q = q.filter(PullRequest.target_repo == repo)
138
138
139 # closed,opened
139 # closed,opened
140 if statuses:
140 if statuses:
141 q = q.filter(PullRequest.status.in_(statuses))
141 q = q.filter(PullRequest.status.in_(statuses))
142
142
143 # opened by filter
143 # opened by filter
144 if opened_by:
144 if opened_by:
145 q = q.filter(PullRequest.user_id.in_(opened_by))
145 q = q.filter(PullRequest.user_id.in_(opened_by))
146
146
147 if order_by:
147 if order_by:
148 order_map = {
148 order_map = {
149 'name_raw': PullRequest.pull_request_id,
149 'name_raw': PullRequest.pull_request_id,
150 'title': PullRequest.title,
150 'title': PullRequest.title,
151 'updated_on_raw': PullRequest.updated_on
151 'updated_on_raw': PullRequest.updated_on
152 }
152 }
153 if order_dir == 'asc':
153 if order_dir == 'asc':
154 q = q.order_by(order_map[order_by].asc())
154 q = q.order_by(order_map[order_by].asc())
155 else:
155 else:
156 q = q.order_by(order_map[order_by].desc())
156 q = q.order_by(order_map[order_by].desc())
157
157
158 return q
158 return q
159
159
160 def count_all(self, repo_name, source=False, statuses=None,
160 def count_all(self, repo_name, source=False, statuses=None,
161 opened_by=None):
161 opened_by=None):
162 """
162 """
163 Count the number of pull requests for a specific repository.
163 Count the number of pull requests for a specific repository.
164
164
165 :param repo_name: target or source repo
165 :param repo_name: target or source repo
166 :param source: boolean flag to specify if repo_name refers to source
166 :param source: boolean flag to specify if repo_name refers to source
167 :param statuses: list of pull request statuses
167 :param statuses: list of pull request statuses
168 :param opened_by: author user of the pull request
168 :param opened_by: author user of the pull request
169 :returns: int number of pull requests
169 :returns: int number of pull requests
170 """
170 """
171 q = self._prepare_get_all_query(
171 q = self._prepare_get_all_query(
172 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172 repo_name, source=source, statuses=statuses, opened_by=opened_by)
173
173
174 return q.count()
174 return q.count()
175
175
176 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
177 offset=0, length=None, order_by=None, order_dir='desc'):
177 offset=0, length=None, order_by=None, order_dir='desc'):
178 """
178 """
179 Get all pull requests for a specific repository.
179 Get all pull requests for a specific repository.
180
180
181 :param repo_name: target or source repo
181 :param repo_name: target or source repo
182 :param source: boolean flag to specify if repo_name refers to source
182 :param source: boolean flag to specify if repo_name refers to source
183 :param statuses: list of pull request statuses
183 :param statuses: list of pull request statuses
184 :param opened_by: author user of the pull request
184 :param opened_by: author user of the pull request
185 :param offset: pagination offset
185 :param offset: pagination offset
186 :param length: length of returned list
186 :param length: length of returned list
187 :param order_by: order of the returned list
187 :param order_by: order of the returned list
188 :param order_dir: 'asc' or 'desc' ordering direction
188 :param order_dir: 'asc' or 'desc' ordering direction
189 :returns: list of pull requests
189 :returns: list of pull requests
190 """
190 """
191 q = self._prepare_get_all_query(
191 q = self._prepare_get_all_query(
192 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 repo_name, source=source, statuses=statuses, opened_by=opened_by,
193 order_by=order_by, order_dir=order_dir)
193 order_by=order_by, order_dir=order_dir)
194
194
195 if length:
195 if length:
196 pull_requests = q.limit(length).offset(offset).all()
196 pull_requests = q.limit(length).offset(offset).all()
197 else:
197 else:
198 pull_requests = q.all()
198 pull_requests = q.all()
199
199
200 return pull_requests
200 return pull_requests
201
201
202 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 def count_awaiting_review(self, repo_name, source=False, statuses=None,
203 opened_by=None):
203 opened_by=None):
204 """
204 """
205 Count the number of pull requests for a specific repository that are
205 Count the number of pull requests for a specific repository that are
206 awaiting review.
206 awaiting review.
207
207
208 :param repo_name: target or source repo
208 :param repo_name: target or source repo
209 :param source: boolean flag to specify if repo_name refers to source
209 :param source: boolean flag to specify if repo_name refers to source
210 :param statuses: list of pull request statuses
210 :param statuses: list of pull request statuses
211 :param opened_by: author user of the pull request
211 :param opened_by: author user of the pull request
212 :returns: int number of pull requests
212 :returns: int number of pull requests
213 """
213 """
214 pull_requests = self.get_awaiting_review(
214 pull_requests = self.get_awaiting_review(
215 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215 repo_name, source=source, statuses=statuses, opened_by=opened_by)
216
216
217 return len(pull_requests)
217 return len(pull_requests)
218
218
219 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 def get_awaiting_review(self, repo_name, source=False, statuses=None,
220 opened_by=None, offset=0, length=None,
220 opened_by=None, offset=0, length=None,
221 order_by=None, order_dir='desc'):
221 order_by=None, order_dir='desc'):
222 """
222 """
223 Get all pull requests for a specific repository that are awaiting
223 Get all pull requests for a specific repository that are awaiting
224 review.
224 review.
225
225
226 :param repo_name: target or source repo
226 :param repo_name: target or source repo
227 :param source: boolean flag to specify if repo_name refers to source
227 :param source: boolean flag to specify if repo_name refers to source
228 :param statuses: list of pull request statuses
228 :param statuses: list of pull request statuses
229 :param opened_by: author user of the pull request
229 :param opened_by: author user of the pull request
230 :param offset: pagination offset
230 :param offset: pagination offset
231 :param length: length of returned list
231 :param length: length of returned list
232 :param order_by: order of the returned list
232 :param order_by: order of the returned list
233 :param order_dir: 'asc' or 'desc' ordering direction
233 :param order_dir: 'asc' or 'desc' ordering direction
234 :returns: list of pull requests
234 :returns: list of pull requests
235 """
235 """
236 pull_requests = self.get_all(
236 pull_requests = self.get_all(
237 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 repo_name, source=source, statuses=statuses, opened_by=opened_by,
238 order_by=order_by, order_dir=order_dir)
238 order_by=order_by, order_dir=order_dir)
239
239
240 _filtered_pull_requests = []
240 _filtered_pull_requests = []
241 for pr in pull_requests:
241 for pr in pull_requests:
242 status = pr.calculated_review_status()
242 status = pr.calculated_review_status()
243 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
244 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 ChangesetStatus.STATUS_UNDER_REVIEW]:
245 _filtered_pull_requests.append(pr)
245 _filtered_pull_requests.append(pr)
246 if length:
246 if length:
247 return _filtered_pull_requests[offset:offset+length]
247 return _filtered_pull_requests[offset:offset+length]
248 else:
248 else:
249 return _filtered_pull_requests
249 return _filtered_pull_requests
250
250
251 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
252 opened_by=None, user_id=None):
252 opened_by=None, user_id=None):
253 """
253 """
254 Count the number of pull requests for a specific repository that are
254 Count the number of pull requests for a specific repository that are
255 awaiting review from a specific user.
255 awaiting review from a specific user.
256
256
257 :param repo_name: target or source repo
257 :param repo_name: target or source repo
258 :param source: boolean flag to specify if repo_name refers to source
258 :param source: boolean flag to specify if repo_name refers to source
259 :param statuses: list of pull request statuses
259 :param statuses: list of pull request statuses
260 :param opened_by: author user of the pull request
260 :param opened_by: author user of the pull request
261 :param user_id: reviewer user of the pull request
261 :param user_id: reviewer user of the pull request
262 :returns: int number of pull requests
262 :returns: int number of pull requests
263 """
263 """
264 pull_requests = self.get_awaiting_my_review(
264 pull_requests = self.get_awaiting_my_review(
265 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 repo_name, source=source, statuses=statuses, opened_by=opened_by,
266 user_id=user_id)
266 user_id=user_id)
267
267
268 return len(pull_requests)
268 return len(pull_requests)
269
269
270 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
271 opened_by=None, user_id=None, offset=0,
271 opened_by=None, user_id=None, offset=0,
272 length=None, order_by=None, order_dir='desc'):
272 length=None, order_by=None, order_dir='desc'):
273 """
273 """
274 Get all pull requests for a specific repository that are awaiting
274 Get all pull requests for a specific repository that are awaiting
275 review from a specific user.
275 review from a specific user.
276
276
277 :param repo_name: target or source repo
277 :param repo_name: target or source repo
278 :param source: boolean flag to specify if repo_name refers to source
278 :param source: boolean flag to specify if repo_name refers to source
279 :param statuses: list of pull request statuses
279 :param statuses: list of pull request statuses
280 :param opened_by: author user of the pull request
280 :param opened_by: author user of the pull request
281 :param user_id: reviewer user of the pull request
281 :param user_id: reviewer user of the pull request
282 :param offset: pagination offset
282 :param offset: pagination offset
283 :param length: length of returned list
283 :param length: length of returned list
284 :param order_by: order of the returned list
284 :param order_by: order of the returned list
285 :param order_dir: 'asc' or 'desc' ordering direction
285 :param order_dir: 'asc' or 'desc' ordering direction
286 :returns: list of pull requests
286 :returns: list of pull requests
287 """
287 """
288 pull_requests = self.get_all(
288 pull_requests = self.get_all(
289 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 repo_name, source=source, statuses=statuses, opened_by=opened_by,
290 order_by=order_by, order_dir=order_dir)
290 order_by=order_by, order_dir=order_dir)
291
291
292 _my = PullRequestModel().get_not_reviewed(user_id)
292 _my = PullRequestModel().get_not_reviewed(user_id)
293 my_participation = []
293 my_participation = []
294 for pr in pull_requests:
294 for pr in pull_requests:
295 if pr in _my:
295 if pr in _my:
296 my_participation.append(pr)
296 my_participation.append(pr)
297 _filtered_pull_requests = my_participation
297 _filtered_pull_requests = my_participation
298 if length:
298 if length:
299 return _filtered_pull_requests[offset:offset+length]
299 return _filtered_pull_requests[offset:offset+length]
300 else:
300 else:
301 return _filtered_pull_requests
301 return _filtered_pull_requests
302
302
303 def get_not_reviewed(self, user_id):
303 def get_not_reviewed(self, user_id):
304 return [
304 return [
305 x.pull_request for x in PullRequestReviewers.query().filter(
305 x.pull_request for x in PullRequestReviewers.query().filter(
306 PullRequestReviewers.user_id == user_id).all()
306 PullRequestReviewers.user_id == user_id).all()
307 ]
307 ]
308
308
309 def get_versions(self, pull_request):
309 def get_versions(self, pull_request):
310 """
310 """
311 returns version of pull request sorted by ID descending
311 returns version of pull request sorted by ID descending
312 """
312 """
313 return PullRequestVersion.query()\
313 return PullRequestVersion.query()\
314 .filter(PullRequestVersion.pull_request == pull_request)\
314 .filter(PullRequestVersion.pull_request == pull_request)\
315 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 .order_by(PullRequestVersion.pull_request_version_id.asc())\
316 .all()
316 .all()
317
317
318 def create(self, created_by, source_repo, source_ref, target_repo,
318 def create(self, created_by, source_repo, source_ref, target_repo,
319 target_ref, revisions, reviewers, title, description=None):
319 target_ref, revisions, reviewers, title, description=None):
320 created_by_user = self._get_user(created_by)
320 created_by_user = self._get_user(created_by)
321 source_repo = self._get_repo(source_repo)
321 source_repo = self._get_repo(source_repo)
322 target_repo = self._get_repo(target_repo)
322 target_repo = self._get_repo(target_repo)
323
323
324 pull_request = PullRequest()
324 pull_request = PullRequest()
325 pull_request.source_repo = source_repo
325 pull_request.source_repo = source_repo
326 pull_request.source_ref = source_ref
326 pull_request.source_ref = source_ref
327 pull_request.target_repo = target_repo
327 pull_request.target_repo = target_repo
328 pull_request.target_ref = target_ref
328 pull_request.target_ref = target_ref
329 pull_request.revisions = revisions
329 pull_request.revisions = revisions
330 pull_request.title = title
330 pull_request.title = title
331 pull_request.description = description
331 pull_request.description = description
332 pull_request.author = created_by_user
332 pull_request.author = created_by_user
333
333
334 Session().add(pull_request)
334 Session().add(pull_request)
335 Session().flush()
335 Session().flush()
336
336
337 reviewer_ids = set()
337 reviewer_ids = set()
338 # members / reviewers
338 # members / reviewers
339 for reviewer_object in reviewers:
339 for reviewer_object in reviewers:
340 if isinstance(reviewer_object, tuple):
340 if isinstance(reviewer_object, tuple):
341 user_id, reasons = reviewer_object
341 user_id, reasons = reviewer_object
342 else:
342 else:
343 user_id, reasons = reviewer_object, []
343 user_id, reasons = reviewer_object, []
344
344
345 user = self._get_user(user_id)
345 user = self._get_user(user_id)
346 reviewer_ids.add(user.user_id)
346 reviewer_ids.add(user.user_id)
347
347
348 reviewer = PullRequestReviewers(user, pull_request, reasons)
348 reviewer = PullRequestReviewers(user, pull_request, reasons)
349 Session().add(reviewer)
349 Session().add(reviewer)
350
350
351 # Set approval status to "Under Review" for all commits which are
351 # Set approval status to "Under Review" for all commits which are
352 # part of this pull request.
352 # part of this pull request.
353 ChangesetStatusModel().set_status(
353 ChangesetStatusModel().set_status(
354 repo=target_repo,
354 repo=target_repo,
355 status=ChangesetStatus.STATUS_UNDER_REVIEW,
355 status=ChangesetStatus.STATUS_UNDER_REVIEW,
356 user=created_by_user,
356 user=created_by_user,
357 pull_request=pull_request
357 pull_request=pull_request
358 )
358 )
359
359
360 self.notify_reviewers(pull_request, reviewer_ids)
360 self.notify_reviewers(pull_request, reviewer_ids)
361 self._trigger_pull_request_hook(
361 self._trigger_pull_request_hook(
362 pull_request, created_by_user, 'create')
362 pull_request, created_by_user, 'create')
363
363
364 return pull_request
364 return pull_request
365
365
366 def _trigger_pull_request_hook(self, pull_request, user, action):
366 def _trigger_pull_request_hook(self, pull_request, user, action):
367 pull_request = self.__get_pull_request(pull_request)
367 pull_request = self.__get_pull_request(pull_request)
368 target_scm = pull_request.target_repo.scm_instance()
368 target_scm = pull_request.target_repo.scm_instance()
369 if action == 'create':
369 if action == 'create':
370 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
370 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
371 elif action == 'merge':
371 elif action == 'merge':
372 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
372 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
373 elif action == 'close':
373 elif action == 'close':
374 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
374 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
375 elif action == 'review_status_change':
375 elif action == 'review_status_change':
376 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
376 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
377 elif action == 'update':
377 elif action == 'update':
378 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
378 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
379 else:
379 else:
380 return
380 return
381
381
382 trigger_hook(
382 trigger_hook(
383 username=user.username,
383 username=user.username,
384 repo_name=pull_request.target_repo.repo_name,
384 repo_name=pull_request.target_repo.repo_name,
385 repo_alias=target_scm.alias,
385 repo_alias=target_scm.alias,
386 pull_request=pull_request)
386 pull_request=pull_request)
387
387
388 def _get_commit_ids(self, pull_request):
388 def _get_commit_ids(self, pull_request):
389 """
389 """
390 Return the commit ids of the merged pull request.
390 Return the commit ids of the merged pull request.
391
391
392 This method is not dealing correctly yet with the lack of autoupdates
392 This method is not dealing correctly yet with the lack of autoupdates
393 nor with the implicit target updates.
393 nor with the implicit target updates.
394 For example: if a commit in the source repo is already in the target it
394 For example: if a commit in the source repo is already in the target it
395 will be reported anyways.
395 will be reported anyways.
396 """
396 """
397 merge_rev = pull_request.merge_rev
397 merge_rev = pull_request.merge_rev
398 if merge_rev is None:
398 if merge_rev is None:
399 raise ValueError('This pull request was not merged yet')
399 raise ValueError('This pull request was not merged yet')
400
400
401 commit_ids = list(pull_request.revisions)
401 commit_ids = list(pull_request.revisions)
402 if merge_rev not in commit_ids:
402 if merge_rev not in commit_ids:
403 commit_ids.append(merge_rev)
403 commit_ids.append(merge_rev)
404
404
405 return commit_ids
405 return commit_ids
406
406
407 def merge(self, pull_request, user, extras):
407 def merge(self, pull_request, user, extras):
408 log.debug("Merging pull request %s", pull_request.pull_request_id)
408 log.debug("Merging pull request %s", pull_request.pull_request_id)
409 merge_state = self._merge_pull_request(pull_request, user, extras)
409 merge_state = self._merge_pull_request(pull_request, user, extras)
410 if merge_state.executed:
410 if merge_state.executed:
411 log.debug(
411 log.debug(
412 "Merge was successful, updating the pull request comments.")
412 "Merge was successful, updating the pull request comments.")
413 self._comment_and_close_pr(pull_request, user, merge_state)
413 self._comment_and_close_pr(pull_request, user, merge_state)
414 self._log_action('user_merged_pull_request', user, pull_request)
414 self._log_action('user_merged_pull_request', user, pull_request)
415 else:
415 else:
416 log.warn("Merge failed, not updating the pull request.")
416 log.warn("Merge failed, not updating the pull request.")
417 return merge_state
417 return merge_state
418
418
419 def _merge_pull_request(self, pull_request, user, extras):
419 def _merge_pull_request(self, pull_request, user, extras):
420 target_vcs = pull_request.target_repo.scm_instance()
420 target_vcs = pull_request.target_repo.scm_instance()
421 source_vcs = pull_request.source_repo.scm_instance()
421 source_vcs = pull_request.source_repo.scm_instance()
422 target_ref = self._refresh_reference(
422 target_ref = self._refresh_reference(
423 pull_request.target_ref_parts, target_vcs)
423 pull_request.target_ref_parts, target_vcs)
424
424
425 message = _(
425 message = _(
426 'Merge pull request #%(pr_id)s from '
426 'Merge pull request #%(pr_id)s from '
427 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
427 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
428 'pr_id': pull_request.pull_request_id,
428 'pr_id': pull_request.pull_request_id,
429 'source_repo': source_vcs.name,
429 'source_repo': source_vcs.name,
430 'source_ref_name': pull_request.source_ref_parts.name,
430 'source_ref_name': pull_request.source_ref_parts.name,
431 'pr_title': pull_request.title
431 'pr_title': pull_request.title
432 }
432 }
433
433
434 workspace_id = self._workspace_id(pull_request)
434 workspace_id = self._workspace_id(pull_request)
435 use_rebase = self._use_rebase_for_merging(pull_request)
435 use_rebase = self._use_rebase_for_merging(pull_request)
436
436
437 callback_daemon, extras = prepare_callback_daemon(
437 callback_daemon, extras = prepare_callback_daemon(
438 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
438 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
439 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
439 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
440
440
441 with callback_daemon:
441 with callback_daemon:
442 # TODO: johbo: Implement a clean way to run a config_override
442 # TODO: johbo: Implement a clean way to run a config_override
443 # for a single call.
443 # for a single call.
444 target_vcs.config.set(
444 target_vcs.config.set(
445 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
445 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
446 merge_state = target_vcs.merge(
446 merge_state = target_vcs.merge(
447 target_ref, source_vcs, pull_request.source_ref_parts,
447 target_ref, source_vcs, pull_request.source_ref_parts,
448 workspace_id, user_name=user.username,
448 workspace_id, user_name=user.username,
449 user_email=user.email, message=message, use_rebase=use_rebase)
449 user_email=user.email, message=message, use_rebase=use_rebase)
450 return merge_state
450 return merge_state
451
451
452 def _comment_and_close_pr(self, pull_request, user, merge_state):
452 def _comment_and_close_pr(self, pull_request, user, merge_state):
453 pull_request.merge_rev = merge_state.merge_commit_id
453 pull_request.merge_rev = merge_state.merge_ref.commit_id
454 pull_request.updated_on = datetime.datetime.now()
454 pull_request.updated_on = datetime.datetime.now()
455
455
456 ChangesetCommentsModel().create(
456 ChangesetCommentsModel().create(
457 text=unicode(_('Pull request merged and closed')),
457 text=unicode(_('Pull request merged and closed')),
458 repo=pull_request.target_repo.repo_id,
458 repo=pull_request.target_repo.repo_id,
459 user=user.user_id,
459 user=user.user_id,
460 pull_request=pull_request.pull_request_id,
460 pull_request=pull_request.pull_request_id,
461 f_path=None,
461 f_path=None,
462 line_no=None,
462 line_no=None,
463 closing_pr=True
463 closing_pr=True
464 )
464 )
465
465
466 Session().add(pull_request)
466 Session().add(pull_request)
467 Session().flush()
467 Session().flush()
468 # TODO: paris: replace invalidation with less radical solution
468 # TODO: paris: replace invalidation with less radical solution
469 ScmModel().mark_for_invalidation(
469 ScmModel().mark_for_invalidation(
470 pull_request.target_repo.repo_name)
470 pull_request.target_repo.repo_name)
471 self._trigger_pull_request_hook(pull_request, user, 'merge')
471 self._trigger_pull_request_hook(pull_request, user, 'merge')
472
472
473 def has_valid_update_type(self, pull_request):
473 def has_valid_update_type(self, pull_request):
474 source_ref_type = pull_request.source_ref_parts.type
474 source_ref_type = pull_request.source_ref_parts.type
475 return source_ref_type in ['book', 'branch', 'tag']
475 return source_ref_type in ['book', 'branch', 'tag']
476
476
477 def update_commits(self, pull_request):
477 def update_commits(self, pull_request):
478 """
478 """
479 Get the updated list of commits for the pull request
479 Get the updated list of commits for the pull request
480 and return the new pull request version and the list
480 and return the new pull request version and the list
481 of commits processed by this update action
481 of commits processed by this update action
482 """
482 """
483
483
484 pull_request = self.__get_pull_request(pull_request)
484 pull_request = self.__get_pull_request(pull_request)
485 source_ref_type = pull_request.source_ref_parts.type
485 source_ref_type = pull_request.source_ref_parts.type
486 source_ref_name = pull_request.source_ref_parts.name
486 source_ref_name = pull_request.source_ref_parts.name
487 source_ref_id = pull_request.source_ref_parts.commit_id
487 source_ref_id = pull_request.source_ref_parts.commit_id
488
488
489 if not self.has_valid_update_type(pull_request):
489 if not self.has_valid_update_type(pull_request):
490 log.debug(
490 log.debug(
491 "Skipping update of pull request %s due to ref type: %s",
491 "Skipping update of pull request %s due to ref type: %s",
492 pull_request, source_ref_type)
492 pull_request, source_ref_type)
493 return (None, None)
493 return (None, None)
494
494
495 source_repo = pull_request.source_repo.scm_instance()
495 source_repo = pull_request.source_repo.scm_instance()
496 source_commit = source_repo.get_commit(commit_id=source_ref_name)
496 source_commit = source_repo.get_commit(commit_id=source_ref_name)
497 if source_ref_id == source_commit.raw_id:
497 if source_ref_id == source_commit.raw_id:
498 log.debug("Nothing changed in pull request %s", pull_request)
498 log.debug("Nothing changed in pull request %s", pull_request)
499 return (None, None)
499 return (None, None)
500
500
501 # Finally there is a need for an update
501 # Finally there is a need for an update
502 pull_request_version = self._create_version_from_snapshot(pull_request)
502 pull_request_version = self._create_version_from_snapshot(pull_request)
503 self._link_comments_to_version(pull_request_version)
503 self._link_comments_to_version(pull_request_version)
504
504
505 target_ref_type = pull_request.target_ref_parts.type
505 target_ref_type = pull_request.target_ref_parts.type
506 target_ref_name = pull_request.target_ref_parts.name
506 target_ref_name = pull_request.target_ref_parts.name
507 target_ref_id = pull_request.target_ref_parts.commit_id
507 target_ref_id = pull_request.target_ref_parts.commit_id
508 target_repo = pull_request.target_repo.scm_instance()
508 target_repo = pull_request.target_repo.scm_instance()
509
509
510 if target_ref_type in ('tag', 'branch', 'book'):
510 if target_ref_type in ('tag', 'branch', 'book'):
511 target_commit = target_repo.get_commit(target_ref_name)
511 target_commit = target_repo.get_commit(target_ref_name)
512 else:
512 else:
513 target_commit = target_repo.get_commit(target_ref_id)
513 target_commit = target_repo.get_commit(target_ref_id)
514
514
515 # re-compute commit ids
515 # re-compute commit ids
516 old_commit_ids = set(pull_request.revisions)
516 old_commit_ids = set(pull_request.revisions)
517 pre_load = ["author", "branch", "date", "message"]
517 pre_load = ["author", "branch", "date", "message"]
518 commit_ranges = target_repo.compare(
518 commit_ranges = target_repo.compare(
519 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
519 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
520 pre_load=pre_load)
520 pre_load=pre_load)
521
521
522 ancestor = target_repo.get_common_ancestor(
522 ancestor = target_repo.get_common_ancestor(
523 target_commit.raw_id, source_commit.raw_id, source_repo)
523 target_commit.raw_id, source_commit.raw_id, source_repo)
524
524
525 pull_request.source_ref = '%s:%s:%s' % (
525 pull_request.source_ref = '%s:%s:%s' % (
526 source_ref_type, source_ref_name, source_commit.raw_id)
526 source_ref_type, source_ref_name, source_commit.raw_id)
527 pull_request.target_ref = '%s:%s:%s' % (
527 pull_request.target_ref = '%s:%s:%s' % (
528 target_ref_type, target_ref_name, ancestor)
528 target_ref_type, target_ref_name, ancestor)
529 pull_request.revisions = [
529 pull_request.revisions = [
530 commit.raw_id for commit in reversed(commit_ranges)]
530 commit.raw_id for commit in reversed(commit_ranges)]
531 pull_request.updated_on = datetime.datetime.now()
531 pull_request.updated_on = datetime.datetime.now()
532 Session().add(pull_request)
532 Session().add(pull_request)
533 new_commit_ids = set(pull_request.revisions)
533 new_commit_ids = set(pull_request.revisions)
534
534
535 changes = self._calculate_commit_id_changes(
535 changes = self._calculate_commit_id_changes(
536 old_commit_ids, new_commit_ids)
536 old_commit_ids, new_commit_ids)
537
537
538 old_diff_data, new_diff_data = self._generate_update_diffs(
538 old_diff_data, new_diff_data = self._generate_update_diffs(
539 pull_request, pull_request_version)
539 pull_request, pull_request_version)
540
540
541 ChangesetCommentsModel().outdate_comments(
541 ChangesetCommentsModel().outdate_comments(
542 pull_request, old_diff_data=old_diff_data,
542 pull_request, old_diff_data=old_diff_data,
543 new_diff_data=new_diff_data)
543 new_diff_data=new_diff_data)
544
544
545 file_changes = self._calculate_file_changes(
545 file_changes = self._calculate_file_changes(
546 old_diff_data, new_diff_data)
546 old_diff_data, new_diff_data)
547
547
548 # Add an automatic comment to the pull request
548 # Add an automatic comment to the pull request
549 update_comment = ChangesetCommentsModel().create(
549 update_comment = ChangesetCommentsModel().create(
550 text=self._render_update_message(changes, file_changes),
550 text=self._render_update_message(changes, file_changes),
551 repo=pull_request.target_repo,
551 repo=pull_request.target_repo,
552 user=pull_request.author,
552 user=pull_request.author,
553 pull_request=pull_request,
553 pull_request=pull_request,
554 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
554 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
555
555
556 # Update status to "Under Review" for added commits
556 # Update status to "Under Review" for added commits
557 for commit_id in changes.added:
557 for commit_id in changes.added:
558 ChangesetStatusModel().set_status(
558 ChangesetStatusModel().set_status(
559 repo=pull_request.source_repo,
559 repo=pull_request.source_repo,
560 status=ChangesetStatus.STATUS_UNDER_REVIEW,
560 status=ChangesetStatus.STATUS_UNDER_REVIEW,
561 comment=update_comment,
561 comment=update_comment,
562 user=pull_request.author,
562 user=pull_request.author,
563 pull_request=pull_request,
563 pull_request=pull_request,
564 revision=commit_id)
564 revision=commit_id)
565
565
566 log.debug(
566 log.debug(
567 'Updated pull request %s, added_ids: %s, common_ids: %s, '
567 'Updated pull request %s, added_ids: %s, common_ids: %s, '
568 'removed_ids: %s', pull_request.pull_request_id,
568 'removed_ids: %s', pull_request.pull_request_id,
569 changes.added, changes.common, changes.removed)
569 changes.added, changes.common, changes.removed)
570 log.debug('Updated pull request with the following file changes: %s',
570 log.debug('Updated pull request with the following file changes: %s',
571 file_changes)
571 file_changes)
572
572
573 log.info(
573 log.info(
574 "Updated pull request %s from commit %s to commit %s, "
574 "Updated pull request %s from commit %s to commit %s, "
575 "stored new version %s of this pull request.",
575 "stored new version %s of this pull request.",
576 pull_request.pull_request_id, source_ref_id,
576 pull_request.pull_request_id, source_ref_id,
577 pull_request.source_ref_parts.commit_id,
577 pull_request.source_ref_parts.commit_id,
578 pull_request_version.pull_request_version_id)
578 pull_request_version.pull_request_version_id)
579 Session().commit()
579 Session().commit()
580 self._trigger_pull_request_hook(pull_request, pull_request.author,
580 self._trigger_pull_request_hook(pull_request, pull_request.author,
581 'update')
581 'update')
582
582
583 return (pull_request_version, changes)
583 return (pull_request_version, changes)
584
584
585 def _create_version_from_snapshot(self, pull_request):
585 def _create_version_from_snapshot(self, pull_request):
586 version = PullRequestVersion()
586 version = PullRequestVersion()
587 version.title = pull_request.title
587 version.title = pull_request.title
588 version.description = pull_request.description
588 version.description = pull_request.description
589 version.status = pull_request.status
589 version.status = pull_request.status
590 version.created_on = pull_request.created_on
590 version.created_on = pull_request.created_on
591 version.updated_on = pull_request.updated_on
591 version.updated_on = pull_request.updated_on
592 version.user_id = pull_request.user_id
592 version.user_id = pull_request.user_id
593 version.source_repo = pull_request.source_repo
593 version.source_repo = pull_request.source_repo
594 version.source_ref = pull_request.source_ref
594 version.source_ref = pull_request.source_ref
595 version.target_repo = pull_request.target_repo
595 version.target_repo = pull_request.target_repo
596 version.target_ref = pull_request.target_ref
596 version.target_ref = pull_request.target_ref
597
597
598 version._last_merge_source_rev = pull_request._last_merge_source_rev
598 version._last_merge_source_rev = pull_request._last_merge_source_rev
599 version._last_merge_target_rev = pull_request._last_merge_target_rev
599 version._last_merge_target_rev = pull_request._last_merge_target_rev
600 version._last_merge_status = pull_request._last_merge_status
600 version._last_merge_status = pull_request._last_merge_status
601 version.last_merge_rev = pull_request.last_merge_rev
601 version.shadow_merge_ref = pull_request.shadow_merge_ref
602 version.merge_rev = pull_request.merge_rev
602 version.merge_rev = pull_request.merge_rev
603
603
604 version.revisions = pull_request.revisions
604 version.revisions = pull_request.revisions
605 version.pull_request = pull_request
605 version.pull_request = pull_request
606 Session().add(version)
606 Session().add(version)
607 Session().flush()
607 Session().flush()
608
608
609 return version
609 return version
610
610
611 def _generate_update_diffs(self, pull_request, pull_request_version):
611 def _generate_update_diffs(self, pull_request, pull_request_version):
612 diff_context = (
612 diff_context = (
613 self.DIFF_CONTEXT +
613 self.DIFF_CONTEXT +
614 ChangesetCommentsModel.needed_extra_diff_context())
614 ChangesetCommentsModel.needed_extra_diff_context())
615 old_diff = self._get_diff_from_pr_or_version(
615 old_diff = self._get_diff_from_pr_or_version(
616 pull_request_version, context=diff_context)
616 pull_request_version, context=diff_context)
617 new_diff = self._get_diff_from_pr_or_version(
617 new_diff = self._get_diff_from_pr_or_version(
618 pull_request, context=diff_context)
618 pull_request, context=diff_context)
619
619
620 old_diff_data = diffs.DiffProcessor(old_diff)
620 old_diff_data = diffs.DiffProcessor(old_diff)
621 old_diff_data.prepare()
621 old_diff_data.prepare()
622 new_diff_data = diffs.DiffProcessor(new_diff)
622 new_diff_data = diffs.DiffProcessor(new_diff)
623 new_diff_data.prepare()
623 new_diff_data.prepare()
624
624
625 return old_diff_data, new_diff_data
625 return old_diff_data, new_diff_data
626
626
627 def _link_comments_to_version(self, pull_request_version):
627 def _link_comments_to_version(self, pull_request_version):
628 """
628 """
629 Link all unlinked comments of this pull request to the given version.
629 Link all unlinked comments of this pull request to the given version.
630
630
631 :param pull_request_version: The `PullRequestVersion` to which
631 :param pull_request_version: The `PullRequestVersion` to which
632 the comments shall be linked.
632 the comments shall be linked.
633
633
634 """
634 """
635 pull_request = pull_request_version.pull_request
635 pull_request = pull_request_version.pull_request
636 comments = ChangesetComment.query().filter(
636 comments = ChangesetComment.query().filter(
637 # TODO: johbo: Should we query for the repo at all here?
637 # TODO: johbo: Should we query for the repo at all here?
638 # Pending decision on how comments of PRs are to be related
638 # Pending decision on how comments of PRs are to be related
639 # to either the source repo, the target repo or no repo at all.
639 # to either the source repo, the target repo or no repo at all.
640 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
640 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
641 ChangesetComment.pull_request == pull_request,
641 ChangesetComment.pull_request == pull_request,
642 ChangesetComment.pull_request_version == None)
642 ChangesetComment.pull_request_version == None)
643
643
644 # TODO: johbo: Find out why this breaks if it is done in a bulk
644 # TODO: johbo: Find out why this breaks if it is done in a bulk
645 # operation.
645 # operation.
646 for comment in comments:
646 for comment in comments:
647 comment.pull_request_version_id = (
647 comment.pull_request_version_id = (
648 pull_request_version.pull_request_version_id)
648 pull_request_version.pull_request_version_id)
649 Session().add(comment)
649 Session().add(comment)
650
650
651 def _calculate_commit_id_changes(self, old_ids, new_ids):
651 def _calculate_commit_id_changes(self, old_ids, new_ids):
652 added = new_ids.difference(old_ids)
652 added = new_ids.difference(old_ids)
653 common = old_ids.intersection(new_ids)
653 common = old_ids.intersection(new_ids)
654 removed = old_ids.difference(new_ids)
654 removed = old_ids.difference(new_ids)
655 return ChangeTuple(added, common, removed)
655 return ChangeTuple(added, common, removed)
656
656
657 def _calculate_file_changes(self, old_diff_data, new_diff_data):
657 def _calculate_file_changes(self, old_diff_data, new_diff_data):
658
658
659 old_files = OrderedDict()
659 old_files = OrderedDict()
660 for diff_data in old_diff_data.parsed_diff:
660 for diff_data in old_diff_data.parsed_diff:
661 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
661 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
662
662
663 added_files = []
663 added_files = []
664 modified_files = []
664 modified_files = []
665 removed_files = []
665 removed_files = []
666 for diff_data in new_diff_data.parsed_diff:
666 for diff_data in new_diff_data.parsed_diff:
667 new_filename = diff_data['filename']
667 new_filename = diff_data['filename']
668 new_hash = md5_safe(diff_data['raw_diff'])
668 new_hash = md5_safe(diff_data['raw_diff'])
669
669
670 old_hash = old_files.get(new_filename)
670 old_hash = old_files.get(new_filename)
671 if not old_hash:
671 if not old_hash:
672 # file is not present in old diff, means it's added
672 # file is not present in old diff, means it's added
673 added_files.append(new_filename)
673 added_files.append(new_filename)
674 else:
674 else:
675 if new_hash != old_hash:
675 if new_hash != old_hash:
676 modified_files.append(new_filename)
676 modified_files.append(new_filename)
677 # now remove a file from old, since we have seen it already
677 # now remove a file from old, since we have seen it already
678 del old_files[new_filename]
678 del old_files[new_filename]
679
679
680 # removed files is when there are present in old, but not in NEW,
680 # removed files is when there are present in old, but not in NEW,
681 # since we remove old files that are present in new diff, left-overs
681 # since we remove old files that are present in new diff, left-overs
682 # if any should be the removed files
682 # if any should be the removed files
683 removed_files.extend(old_files.keys())
683 removed_files.extend(old_files.keys())
684
684
685 return FileChangeTuple(added_files, modified_files, removed_files)
685 return FileChangeTuple(added_files, modified_files, removed_files)
686
686
687 def _render_update_message(self, changes, file_changes):
687 def _render_update_message(self, changes, file_changes):
688 """
688 """
689 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
689 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
690 so it's always looking the same disregarding on which default
690 so it's always looking the same disregarding on which default
691 renderer system is using.
691 renderer system is using.
692
692
693 :param changes: changes named tuple
693 :param changes: changes named tuple
694 :param file_changes: file changes named tuple
694 :param file_changes: file changes named tuple
695
695
696 """
696 """
697 new_status = ChangesetStatus.get_status_lbl(
697 new_status = ChangesetStatus.get_status_lbl(
698 ChangesetStatus.STATUS_UNDER_REVIEW)
698 ChangesetStatus.STATUS_UNDER_REVIEW)
699
699
700 changed_files = (
700 changed_files = (
701 file_changes.added + file_changes.modified + file_changes.removed)
701 file_changes.added + file_changes.modified + file_changes.removed)
702
702
703 params = {
703 params = {
704 'under_review_label': new_status,
704 'under_review_label': new_status,
705 'added_commits': changes.added,
705 'added_commits': changes.added,
706 'removed_commits': changes.removed,
706 'removed_commits': changes.removed,
707 'changed_files': changed_files,
707 'changed_files': changed_files,
708 'added_files': file_changes.added,
708 'added_files': file_changes.added,
709 'modified_files': file_changes.modified,
709 'modified_files': file_changes.modified,
710 'removed_files': file_changes.removed,
710 'removed_files': file_changes.removed,
711 }
711 }
712 renderer = RstTemplateRenderer()
712 renderer = RstTemplateRenderer()
713 return renderer.render('pull_request_update.mako', **params)
713 return renderer.render('pull_request_update.mako', **params)
714
714
715 def edit(self, pull_request, title, description):
715 def edit(self, pull_request, title, description):
716 pull_request = self.__get_pull_request(pull_request)
716 pull_request = self.__get_pull_request(pull_request)
717 if pull_request.is_closed():
717 if pull_request.is_closed():
718 raise ValueError('This pull request is closed')
718 raise ValueError('This pull request is closed')
719 if title:
719 if title:
720 pull_request.title = title
720 pull_request.title = title
721 pull_request.description = description
721 pull_request.description = description
722 pull_request.updated_on = datetime.datetime.now()
722 pull_request.updated_on = datetime.datetime.now()
723 Session().add(pull_request)
723 Session().add(pull_request)
724
724
725 def update_reviewers(self, pull_request, reviewer_data):
725 def update_reviewers(self, pull_request, reviewer_data):
726 """
726 """
727 Update the reviewers in the pull request
727 Update the reviewers in the pull request
728
728
729 :param pull_request: the pr to update
729 :param pull_request: the pr to update
730 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
730 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
731 """
731 """
732
732
733 reviewers_reasons = {}
733 reviewers_reasons = {}
734 for user_id, reasons in reviewer_data:
734 for user_id, reasons in reviewer_data:
735 if isinstance(user_id, (int, basestring)):
735 if isinstance(user_id, (int, basestring)):
736 user_id = self._get_user(user_id).user_id
736 user_id = self._get_user(user_id).user_id
737 reviewers_reasons[user_id] = reasons
737 reviewers_reasons[user_id] = reasons
738
738
739 reviewers_ids = set(reviewers_reasons.keys())
739 reviewers_ids = set(reviewers_reasons.keys())
740 pull_request = self.__get_pull_request(pull_request)
740 pull_request = self.__get_pull_request(pull_request)
741 current_reviewers = PullRequestReviewers.query()\
741 current_reviewers = PullRequestReviewers.query()\
742 .filter(PullRequestReviewers.pull_request ==
742 .filter(PullRequestReviewers.pull_request ==
743 pull_request).all()
743 pull_request).all()
744 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
744 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
745
745
746 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
746 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
747 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
747 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
748
748
749 log.debug("Adding %s reviewers", ids_to_add)
749 log.debug("Adding %s reviewers", ids_to_add)
750 log.debug("Removing %s reviewers", ids_to_remove)
750 log.debug("Removing %s reviewers", ids_to_remove)
751 changed = False
751 changed = False
752 for uid in ids_to_add:
752 for uid in ids_to_add:
753 changed = True
753 changed = True
754 _usr = self._get_user(uid)
754 _usr = self._get_user(uid)
755 reasons = reviewers_reasons[uid]
755 reasons = reviewers_reasons[uid]
756 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
756 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
757 Session().add(reviewer)
757 Session().add(reviewer)
758
758
759 self.notify_reviewers(pull_request, ids_to_add)
759 self.notify_reviewers(pull_request, ids_to_add)
760
760
761 for uid in ids_to_remove:
761 for uid in ids_to_remove:
762 changed = True
762 changed = True
763 reviewer = PullRequestReviewers.query()\
763 reviewer = PullRequestReviewers.query()\
764 .filter(PullRequestReviewers.user_id == uid,
764 .filter(PullRequestReviewers.user_id == uid,
765 PullRequestReviewers.pull_request == pull_request)\
765 PullRequestReviewers.pull_request == pull_request)\
766 .scalar()
766 .scalar()
767 if reviewer:
767 if reviewer:
768 Session().delete(reviewer)
768 Session().delete(reviewer)
769 if changed:
769 if changed:
770 pull_request.updated_on = datetime.datetime.now()
770 pull_request.updated_on = datetime.datetime.now()
771 Session().add(pull_request)
771 Session().add(pull_request)
772
772
773 return ids_to_add, ids_to_remove
773 return ids_to_add, ids_to_remove
774
774
775 def get_url(self, pull_request):
775 def get_url(self, pull_request):
776 return h.url('pullrequest_show',
776 return h.url('pullrequest_show',
777 repo_name=safe_str(pull_request.target_repo.repo_name),
777 repo_name=safe_str(pull_request.target_repo.repo_name),
778 pull_request_id=pull_request.pull_request_id,
778 pull_request_id=pull_request.pull_request_id,
779 qualified=True)
779 qualified=True)
780
780
781 def get_shadow_clone_url(self, pull_request):
781 def get_shadow_clone_url(self, pull_request):
782 """
782 """
783 Returns qualified url pointing to the shadow repository. If this pull
783 Returns qualified url pointing to the shadow repository. If this pull
784 request is closed there is no shadow repository and ``None`` will be
784 request is closed there is no shadow repository and ``None`` will be
785 returned.
785 returned.
786 """
786 """
787 if pull_request.is_closed():
787 if pull_request.is_closed():
788 return None
788 return None
789 else:
789 else:
790 pr_url = urllib.unquote(self.get_url(pull_request))
790 pr_url = urllib.unquote(self.get_url(pull_request))
791 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
791 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
792
792
793 def notify_reviewers(self, pull_request, reviewers_ids):
793 def notify_reviewers(self, pull_request, reviewers_ids):
794 # notification to reviewers
794 # notification to reviewers
795 if not reviewers_ids:
795 if not reviewers_ids:
796 return
796 return
797
797
798 pull_request_obj = pull_request
798 pull_request_obj = pull_request
799 # get the current participants of this pull request
799 # get the current participants of this pull request
800 recipients = reviewers_ids
800 recipients = reviewers_ids
801 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
801 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
802
802
803 pr_source_repo = pull_request_obj.source_repo
803 pr_source_repo = pull_request_obj.source_repo
804 pr_target_repo = pull_request_obj.target_repo
804 pr_target_repo = pull_request_obj.target_repo
805
805
806 pr_url = h.url(
806 pr_url = h.url(
807 'pullrequest_show',
807 'pullrequest_show',
808 repo_name=pr_target_repo.repo_name,
808 repo_name=pr_target_repo.repo_name,
809 pull_request_id=pull_request_obj.pull_request_id,
809 pull_request_id=pull_request_obj.pull_request_id,
810 qualified=True,)
810 qualified=True,)
811
811
812 # set some variables for email notification
812 # set some variables for email notification
813 pr_target_repo_url = h.url(
813 pr_target_repo_url = h.url(
814 'summary_home',
814 'summary_home',
815 repo_name=pr_target_repo.repo_name,
815 repo_name=pr_target_repo.repo_name,
816 qualified=True)
816 qualified=True)
817
817
818 pr_source_repo_url = h.url(
818 pr_source_repo_url = h.url(
819 'summary_home',
819 'summary_home',
820 repo_name=pr_source_repo.repo_name,
820 repo_name=pr_source_repo.repo_name,
821 qualified=True)
821 qualified=True)
822
822
823 # pull request specifics
823 # pull request specifics
824 pull_request_commits = [
824 pull_request_commits = [
825 (x.raw_id, x.message)
825 (x.raw_id, x.message)
826 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
826 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
827
827
828 kwargs = {
828 kwargs = {
829 'user': pull_request.author,
829 'user': pull_request.author,
830 'pull_request': pull_request_obj,
830 'pull_request': pull_request_obj,
831 'pull_request_commits': pull_request_commits,
831 'pull_request_commits': pull_request_commits,
832
832
833 'pull_request_target_repo': pr_target_repo,
833 'pull_request_target_repo': pr_target_repo,
834 'pull_request_target_repo_url': pr_target_repo_url,
834 'pull_request_target_repo_url': pr_target_repo_url,
835
835
836 'pull_request_source_repo': pr_source_repo,
836 'pull_request_source_repo': pr_source_repo,
837 'pull_request_source_repo_url': pr_source_repo_url,
837 'pull_request_source_repo_url': pr_source_repo_url,
838
838
839 'pull_request_url': pr_url,
839 'pull_request_url': pr_url,
840 }
840 }
841
841
842 # pre-generate the subject for notification itself
842 # pre-generate the subject for notification itself
843 (subject,
843 (subject,
844 _h, _e, # we don't care about those
844 _h, _e, # we don't care about those
845 body_plaintext) = EmailNotificationModel().render_email(
845 body_plaintext) = EmailNotificationModel().render_email(
846 notification_type, **kwargs)
846 notification_type, **kwargs)
847
847
848 # create notification objects, and emails
848 # create notification objects, and emails
849 NotificationModel().create(
849 NotificationModel().create(
850 created_by=pull_request.author,
850 created_by=pull_request.author,
851 notification_subject=subject,
851 notification_subject=subject,
852 notification_body=body_plaintext,
852 notification_body=body_plaintext,
853 notification_type=notification_type,
853 notification_type=notification_type,
854 recipients=recipients,
854 recipients=recipients,
855 email_kwargs=kwargs,
855 email_kwargs=kwargs,
856 )
856 )
857
857
858 def delete(self, pull_request):
858 def delete(self, pull_request):
859 pull_request = self.__get_pull_request(pull_request)
859 pull_request = self.__get_pull_request(pull_request)
860 self._cleanup_merge_workspace(pull_request)
860 self._cleanup_merge_workspace(pull_request)
861 Session().delete(pull_request)
861 Session().delete(pull_request)
862
862
863 def close_pull_request(self, pull_request, user):
863 def close_pull_request(self, pull_request, user):
864 pull_request = self.__get_pull_request(pull_request)
864 pull_request = self.__get_pull_request(pull_request)
865 self._cleanup_merge_workspace(pull_request)
865 self._cleanup_merge_workspace(pull_request)
866 pull_request.status = PullRequest.STATUS_CLOSED
866 pull_request.status = PullRequest.STATUS_CLOSED
867 pull_request.updated_on = datetime.datetime.now()
867 pull_request.updated_on = datetime.datetime.now()
868 Session().add(pull_request)
868 Session().add(pull_request)
869 self._trigger_pull_request_hook(
869 self._trigger_pull_request_hook(
870 pull_request, pull_request.author, 'close')
870 pull_request, pull_request.author, 'close')
871 self._log_action('user_closed_pull_request', user, pull_request)
871 self._log_action('user_closed_pull_request', user, pull_request)
872
872
873 def close_pull_request_with_comment(self, pull_request, user, repo,
873 def close_pull_request_with_comment(self, pull_request, user, repo,
874 message=None):
874 message=None):
875 status = ChangesetStatus.STATUS_REJECTED
875 status = ChangesetStatus.STATUS_REJECTED
876
876
877 if not message:
877 if not message:
878 message = (
878 message = (
879 _('Status change %(transition_icon)s %(status)s') % {
879 _('Status change %(transition_icon)s %(status)s') % {
880 'transition_icon': '>',
880 'transition_icon': '>',
881 'status': ChangesetStatus.get_status_lbl(status)})
881 'status': ChangesetStatus.get_status_lbl(status)})
882
882
883 internal_message = _('Closing with') + ' ' + message
883 internal_message = _('Closing with') + ' ' + message
884
884
885 comm = ChangesetCommentsModel().create(
885 comm = ChangesetCommentsModel().create(
886 text=internal_message,
886 text=internal_message,
887 repo=repo.repo_id,
887 repo=repo.repo_id,
888 user=user.user_id,
888 user=user.user_id,
889 pull_request=pull_request.pull_request_id,
889 pull_request=pull_request.pull_request_id,
890 f_path=None,
890 f_path=None,
891 line_no=None,
891 line_no=None,
892 status_change=ChangesetStatus.get_status_lbl(status),
892 status_change=ChangesetStatus.get_status_lbl(status),
893 status_change_type=status,
893 status_change_type=status,
894 closing_pr=True
894 closing_pr=True
895 )
895 )
896
896
897 ChangesetStatusModel().set_status(
897 ChangesetStatusModel().set_status(
898 repo.repo_id,
898 repo.repo_id,
899 status,
899 status,
900 user.user_id,
900 user.user_id,
901 comm,
901 comm,
902 pull_request=pull_request.pull_request_id
902 pull_request=pull_request.pull_request_id
903 )
903 )
904 Session().flush()
904 Session().flush()
905
905
906 PullRequestModel().close_pull_request(
906 PullRequestModel().close_pull_request(
907 pull_request.pull_request_id, user)
907 pull_request.pull_request_id, user)
908
908
909 def merge_status(self, pull_request):
909 def merge_status(self, pull_request):
910 if not self._is_merge_enabled(pull_request):
910 if not self._is_merge_enabled(pull_request):
911 return False, _('Server-side pull request merging is disabled.')
911 return False, _('Server-side pull request merging is disabled.')
912 if pull_request.is_closed():
912 if pull_request.is_closed():
913 return False, _('This pull request is closed.')
913 return False, _('This pull request is closed.')
914 merge_possible, msg = self._check_repo_requirements(
914 merge_possible, msg = self._check_repo_requirements(
915 target=pull_request.target_repo, source=pull_request.source_repo)
915 target=pull_request.target_repo, source=pull_request.source_repo)
916 if not merge_possible:
916 if not merge_possible:
917 return merge_possible, msg
917 return merge_possible, msg
918
918
919 try:
919 try:
920 resp = self._try_merge(pull_request)
920 resp = self._try_merge(pull_request)
921 status = resp.possible, self.merge_status_message(
921 status = resp.possible, self.merge_status_message(
922 resp.failure_reason)
922 resp.failure_reason)
923 except NotImplementedError:
923 except NotImplementedError:
924 status = False, _('Pull request merging is not supported.')
924 status = False, _('Pull request merging is not supported.')
925
925
926 return status
926 return status
927
927
928 def _check_repo_requirements(self, target, source):
928 def _check_repo_requirements(self, target, source):
929 """
929 """
930 Check if `target` and `source` have compatible requirements.
930 Check if `target` and `source` have compatible requirements.
931
931
932 Currently this is just checking for largefiles.
932 Currently this is just checking for largefiles.
933 """
933 """
934 target_has_largefiles = self._has_largefiles(target)
934 target_has_largefiles = self._has_largefiles(target)
935 source_has_largefiles = self._has_largefiles(source)
935 source_has_largefiles = self._has_largefiles(source)
936 merge_possible = True
936 merge_possible = True
937 message = u''
937 message = u''
938
938
939 if target_has_largefiles != source_has_largefiles:
939 if target_has_largefiles != source_has_largefiles:
940 merge_possible = False
940 merge_possible = False
941 if source_has_largefiles:
941 if source_has_largefiles:
942 message = _(
942 message = _(
943 'Target repository large files support is disabled.')
943 'Target repository large files support is disabled.')
944 else:
944 else:
945 message = _(
945 message = _(
946 'Source repository large files support is disabled.')
946 'Source repository large files support is disabled.')
947
947
948 return merge_possible, message
948 return merge_possible, message
949
949
950 def _has_largefiles(self, repo):
950 def _has_largefiles(self, repo):
951 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
951 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
952 'extensions', 'largefiles')
952 'extensions', 'largefiles')
953 return largefiles_ui and largefiles_ui[0].active
953 return largefiles_ui and largefiles_ui[0].active
954
954
955 def _try_merge(self, pull_request):
955 def _try_merge(self, pull_request):
956 """
956 """
957 Try to merge the pull request and return the merge status.
957 Try to merge the pull request and return the merge status.
958 """
958 """
959 log.debug(
959 log.debug(
960 "Trying out if the pull request %s can be merged.",
960 "Trying out if the pull request %s can be merged.",
961 pull_request.pull_request_id)
961 pull_request.pull_request_id)
962 target_vcs = pull_request.target_repo.scm_instance()
962 target_vcs = pull_request.target_repo.scm_instance()
963 target_ref = self._refresh_reference(
963 target_ref = self._refresh_reference(
964 pull_request.target_ref_parts, target_vcs)
964 pull_request.target_ref_parts, target_vcs)
965
965
966 target_locked = pull_request.target_repo.locked
966 target_locked = pull_request.target_repo.locked
967 if target_locked and target_locked[0]:
967 if target_locked and target_locked[0]:
968 log.debug("The target repository is locked.")
968 log.debug("The target repository is locked.")
969 merge_state = MergeResponse(
969 merge_state = MergeResponse(
970 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
970 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
971 elif self._needs_merge_state_refresh(pull_request, target_ref):
971 elif self._needs_merge_state_refresh(pull_request, target_ref):
972 log.debug("Refreshing the merge status of the repository.")
972 log.debug("Refreshing the merge status of the repository.")
973 merge_state = self._refresh_merge_state(
973 merge_state = self._refresh_merge_state(
974 pull_request, target_vcs, target_ref)
974 pull_request, target_vcs, target_ref)
975 else:
975 else:
976 possible = pull_request.\
976 possible = pull_request.\
977 _last_merge_status == MergeFailureReason.NONE
977 _last_merge_status == MergeFailureReason.NONE
978 merge_state = MergeResponse(
978 merge_state = MergeResponse(
979 possible, False, None, pull_request._last_merge_status)
979 possible, False, None, pull_request._last_merge_status)
980 log.debug("Merge response: %s", merge_state)
980 log.debug("Merge response: %s", merge_state)
981 return merge_state
981 return merge_state
982
982
983 def _refresh_reference(self, reference, vcs_repository):
983 def _refresh_reference(self, reference, vcs_repository):
984 if reference.type in ('branch', 'book'):
984 if reference.type in ('branch', 'book'):
985 name_or_id = reference.name
985 name_or_id = reference.name
986 else:
986 else:
987 name_or_id = reference.commit_id
987 name_or_id = reference.commit_id
988 refreshed_commit = vcs_repository.get_commit(name_or_id)
988 refreshed_commit = vcs_repository.get_commit(name_or_id)
989 refreshed_reference = Reference(
989 refreshed_reference = Reference(
990 reference.type, reference.name, refreshed_commit.raw_id)
990 reference.type, reference.name, refreshed_commit.raw_id)
991 return refreshed_reference
991 return refreshed_reference
992
992
993 def _needs_merge_state_refresh(self, pull_request, target_reference):
993 def _needs_merge_state_refresh(self, pull_request, target_reference):
994 return not(
994 return not(
995 pull_request.revisions and
995 pull_request.revisions and
996 pull_request.revisions[0] == pull_request._last_merge_source_rev and
996 pull_request.revisions[0] == pull_request._last_merge_source_rev and
997 target_reference.commit_id == pull_request._last_merge_target_rev)
997 target_reference.commit_id == pull_request._last_merge_target_rev)
998
998
999 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
999 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1000 workspace_id = self._workspace_id(pull_request)
1000 workspace_id = self._workspace_id(pull_request)
1001 source_vcs = pull_request.source_repo.scm_instance()
1001 source_vcs = pull_request.source_repo.scm_instance()
1002 use_rebase = self._use_rebase_for_merging(pull_request)
1002 use_rebase = self._use_rebase_for_merging(pull_request)
1003 merge_state = target_vcs.merge(
1003 merge_state = target_vcs.merge(
1004 target_reference, source_vcs, pull_request.source_ref_parts,
1004 target_reference, source_vcs, pull_request.source_ref_parts,
1005 workspace_id, dry_run=True, use_rebase=use_rebase)
1005 workspace_id, dry_run=True, use_rebase=use_rebase)
1006
1006
1007 # Do not store the response if there was an unknown error.
1007 # Do not store the response if there was an unknown error.
1008 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1008 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1009 pull_request._last_merge_source_rev = \
1009 pull_request._last_merge_source_rev = \
1010 pull_request.source_ref_parts.commit_id
1010 pull_request.source_ref_parts.commit_id
1011 pull_request._last_merge_target_rev = target_reference.commit_id
1011 pull_request._last_merge_target_rev = target_reference.commit_id
1012 pull_request._last_merge_status = merge_state.failure_reason
1012 pull_request._last_merge_status = merge_state.failure_reason
1013 pull_request.last_merge_rev = merge_state.merge_commit_id
1013 pull_request.shadow_merge_ref = merge_state.merge_ref
1014 Session().add(pull_request)
1014 Session().add(pull_request)
1015 Session().commit()
1015 Session().commit()
1016
1016
1017 return merge_state
1017 return merge_state
1018
1018
1019 def _workspace_id(self, pull_request):
1019 def _workspace_id(self, pull_request):
1020 workspace_id = 'pr-%s' % pull_request.pull_request_id
1020 workspace_id = 'pr-%s' % pull_request.pull_request_id
1021 return workspace_id
1021 return workspace_id
1022
1022
1023 def merge_status_message(self, status_code):
1023 def merge_status_message(self, status_code):
1024 """
1024 """
1025 Return a human friendly error message for the given merge status code.
1025 Return a human friendly error message for the given merge status code.
1026 """
1026 """
1027 return self.MERGE_STATUS_MESSAGES[status_code]
1027 return self.MERGE_STATUS_MESSAGES[status_code]
1028
1028
1029 def generate_repo_data(self, repo, commit_id=None, branch=None,
1029 def generate_repo_data(self, repo, commit_id=None, branch=None,
1030 bookmark=None):
1030 bookmark=None):
1031 all_refs, selected_ref = \
1031 all_refs, selected_ref = \
1032 self._get_repo_pullrequest_sources(
1032 self._get_repo_pullrequest_sources(
1033 repo.scm_instance(), commit_id=commit_id,
1033 repo.scm_instance(), commit_id=commit_id,
1034 branch=branch, bookmark=bookmark)
1034 branch=branch, bookmark=bookmark)
1035
1035
1036 refs_select2 = []
1036 refs_select2 = []
1037 for element in all_refs:
1037 for element in all_refs:
1038 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1038 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1039 refs_select2.append({'text': element[1], 'children': children})
1039 refs_select2.append({'text': element[1], 'children': children})
1040
1040
1041 return {
1041 return {
1042 'user': {
1042 'user': {
1043 'user_id': repo.user.user_id,
1043 'user_id': repo.user.user_id,
1044 'username': repo.user.username,
1044 'username': repo.user.username,
1045 'firstname': repo.user.firstname,
1045 'firstname': repo.user.firstname,
1046 'lastname': repo.user.lastname,
1046 'lastname': repo.user.lastname,
1047 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1047 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1048 },
1048 },
1049 'description': h.chop_at_smart(repo.description, '\n'),
1049 'description': h.chop_at_smart(repo.description, '\n'),
1050 'refs': {
1050 'refs': {
1051 'all_refs': all_refs,
1051 'all_refs': all_refs,
1052 'selected_ref': selected_ref,
1052 'selected_ref': selected_ref,
1053 'select2_refs': refs_select2
1053 'select2_refs': refs_select2
1054 }
1054 }
1055 }
1055 }
1056
1056
1057 def generate_pullrequest_title(self, source, source_ref, target):
1057 def generate_pullrequest_title(self, source, source_ref, target):
1058 return u'{source}#{at_ref} to {target}'.format(
1058 return u'{source}#{at_ref} to {target}'.format(
1059 source=source,
1059 source=source,
1060 at_ref=source_ref,
1060 at_ref=source_ref,
1061 target=target,
1061 target=target,
1062 )
1062 )
1063
1063
1064 def _cleanup_merge_workspace(self, pull_request):
1064 def _cleanup_merge_workspace(self, pull_request):
1065 # Merging related cleanup
1065 # Merging related cleanup
1066 target_scm = pull_request.target_repo.scm_instance()
1066 target_scm = pull_request.target_repo.scm_instance()
1067 workspace_id = 'pr-%s' % pull_request.pull_request_id
1067 workspace_id = 'pr-%s' % pull_request.pull_request_id
1068
1068
1069 try:
1069 try:
1070 target_scm.cleanup_merge_workspace(workspace_id)
1070 target_scm.cleanup_merge_workspace(workspace_id)
1071 except NotImplementedError:
1071 except NotImplementedError:
1072 pass
1072 pass
1073
1073
1074 def _get_repo_pullrequest_sources(
1074 def _get_repo_pullrequest_sources(
1075 self, repo, commit_id=None, branch=None, bookmark=None):
1075 self, repo, commit_id=None, branch=None, bookmark=None):
1076 """
1076 """
1077 Return a structure with repo's interesting commits, suitable for
1077 Return a structure with repo's interesting commits, suitable for
1078 the selectors in pullrequest controller
1078 the selectors in pullrequest controller
1079
1079
1080 :param commit_id: a commit that must be in the list somehow
1080 :param commit_id: a commit that must be in the list somehow
1081 and selected by default
1081 and selected by default
1082 :param branch: a branch that must be in the list and selected
1082 :param branch: a branch that must be in the list and selected
1083 by default - even if closed
1083 by default - even if closed
1084 :param bookmark: a bookmark that must be in the list and selected
1084 :param bookmark: a bookmark that must be in the list and selected
1085 """
1085 """
1086
1086
1087 commit_id = safe_str(commit_id) if commit_id else None
1087 commit_id = safe_str(commit_id) if commit_id else None
1088 branch = safe_str(branch) if branch else None
1088 branch = safe_str(branch) if branch else None
1089 bookmark = safe_str(bookmark) if bookmark else None
1089 bookmark = safe_str(bookmark) if bookmark else None
1090
1090
1091 selected = None
1091 selected = None
1092
1092
1093 # order matters: first source that has commit_id in it will be selected
1093 # order matters: first source that has commit_id in it will be selected
1094 sources = []
1094 sources = []
1095 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1095 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1096 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1096 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1097
1097
1098 if commit_id:
1098 if commit_id:
1099 ref_commit = (h.short_id(commit_id), commit_id)
1099 ref_commit = (h.short_id(commit_id), commit_id)
1100 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1100 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1101
1101
1102 sources.append(
1102 sources.append(
1103 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1103 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1104 )
1104 )
1105
1105
1106 groups = []
1106 groups = []
1107 for group_key, ref_list, group_name, match in sources:
1107 for group_key, ref_list, group_name, match in sources:
1108 group_refs = []
1108 group_refs = []
1109 for ref_name, ref_id in ref_list:
1109 for ref_name, ref_id in ref_list:
1110 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1110 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1111 group_refs.append((ref_key, ref_name))
1111 group_refs.append((ref_key, ref_name))
1112
1112
1113 if not selected:
1113 if not selected:
1114 if set([commit_id, match]) & set([ref_id, ref_name]):
1114 if set([commit_id, match]) & set([ref_id, ref_name]):
1115 selected = ref_key
1115 selected = ref_key
1116
1116
1117 if group_refs:
1117 if group_refs:
1118 groups.append((group_refs, group_name))
1118 groups.append((group_refs, group_name))
1119
1119
1120 if not selected:
1120 if not selected:
1121 ref = commit_id or branch or bookmark
1121 ref = commit_id or branch or bookmark
1122 if ref:
1122 if ref:
1123 raise CommitDoesNotExistError(
1123 raise CommitDoesNotExistError(
1124 'No commit refs could be found matching: %s' % ref)
1124 'No commit refs could be found matching: %s' % ref)
1125 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1125 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1126 selected = 'branch:%s:%s' % (
1126 selected = 'branch:%s:%s' % (
1127 repo.DEFAULT_BRANCH_NAME,
1127 repo.DEFAULT_BRANCH_NAME,
1128 repo.branches[repo.DEFAULT_BRANCH_NAME]
1128 repo.branches[repo.DEFAULT_BRANCH_NAME]
1129 )
1129 )
1130 elif repo.commit_ids:
1130 elif repo.commit_ids:
1131 rev = repo.commit_ids[0]
1131 rev = repo.commit_ids[0]
1132 selected = 'rev:%s:%s' % (rev, rev)
1132 selected = 'rev:%s:%s' % (rev, rev)
1133 else:
1133 else:
1134 raise EmptyRepositoryError()
1134 raise EmptyRepositoryError()
1135 return groups, selected
1135 return groups, selected
1136
1136
1137 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1137 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1138 pull_request = self.__get_pull_request(pull_request)
1138 pull_request = self.__get_pull_request(pull_request)
1139 return self._get_diff_from_pr_or_version(pull_request, context=context)
1139 return self._get_diff_from_pr_or_version(pull_request, context=context)
1140
1140
1141 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1141 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1142 source_repo = pr_or_version.source_repo
1142 source_repo = pr_or_version.source_repo
1143
1143
1144 # we swap org/other ref since we run a simple diff on one repo
1144 # we swap org/other ref since we run a simple diff on one repo
1145 target_ref_id = pr_or_version.target_ref_parts.commit_id
1145 target_ref_id = pr_or_version.target_ref_parts.commit_id
1146 source_ref_id = pr_or_version.source_ref_parts.commit_id
1146 source_ref_id = pr_or_version.source_ref_parts.commit_id
1147 target_commit = source_repo.get_commit(
1147 target_commit = source_repo.get_commit(
1148 commit_id=safe_str(target_ref_id))
1148 commit_id=safe_str(target_ref_id))
1149 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1149 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1150 vcs_repo = source_repo.scm_instance()
1150 vcs_repo = source_repo.scm_instance()
1151
1151
1152 # TODO: johbo: In the context of an update, we cannot reach
1152 # TODO: johbo: In the context of an update, we cannot reach
1153 # the old commit anymore with our normal mechanisms. It needs
1153 # the old commit anymore with our normal mechanisms. It needs
1154 # some sort of special support in the vcs layer to avoid this
1154 # some sort of special support in the vcs layer to avoid this
1155 # workaround.
1155 # workaround.
1156 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1156 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1157 vcs_repo.alias == 'git'):
1157 vcs_repo.alias == 'git'):
1158 source_commit.raw_id = safe_str(source_ref_id)
1158 source_commit.raw_id = safe_str(source_ref_id)
1159
1159
1160 log.debug('calculating diff between '
1160 log.debug('calculating diff between '
1161 'source_ref:%s and target_ref:%s for repo `%s`',
1161 'source_ref:%s and target_ref:%s for repo `%s`',
1162 target_ref_id, source_ref_id,
1162 target_ref_id, source_ref_id,
1163 safe_unicode(vcs_repo.path))
1163 safe_unicode(vcs_repo.path))
1164
1164
1165 vcs_diff = vcs_repo.get_diff(
1165 vcs_diff = vcs_repo.get_diff(
1166 commit1=target_commit, commit2=source_commit, context=context)
1166 commit1=target_commit, commit2=source_commit, context=context)
1167 return vcs_diff
1167 return vcs_diff
1168
1168
1169 def _is_merge_enabled(self, pull_request):
1169 def _is_merge_enabled(self, pull_request):
1170 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1170 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1171 settings = settings_model.get_general_settings()
1171 settings = settings_model.get_general_settings()
1172 return settings.get('rhodecode_pr_merge_enabled', False)
1172 return settings.get('rhodecode_pr_merge_enabled', False)
1173
1173
1174 def _use_rebase_for_merging(self, pull_request):
1174 def _use_rebase_for_merging(self, pull_request):
1175 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1175 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1176 settings = settings_model.get_general_settings()
1176 settings = settings_model.get_general_settings()
1177 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1177 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1178
1178
1179 def _log_action(self, action, user, pull_request):
1179 def _log_action(self, action, user, pull_request):
1180 action_logger(
1180 action_logger(
1181 user,
1181 user,
1182 '{action}:{pr_id}'.format(
1182 '{action}:{pr_id}'.format(
1183 action=action, pr_id=pull_request.pull_request_id),
1183 action=action, pr_id=pull_request.pull_request_id),
1184 pull_request.target_repo)
1184 pull_request.target_repo)
1185
1185
1186
1186
1187 ChangeTuple = namedtuple('ChangeTuple',
1187 ChangeTuple = namedtuple('ChangeTuple',
1188 ['added', 'common', 'removed'])
1188 ['added', 'common', 'removed'])
1189
1189
1190 FileChangeTuple = namedtuple('FileChangeTuple',
1190 FileChangeTuple = namedtuple('FileChangeTuple',
1191 ['added', 'modified', 'removed'])
1191 ['added', 'modified', 'removed'])
@@ -1,621 +1,621 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 <span id="pr-title">
11 <span id="pr-title">
12 ${c.pull_request.title}
12 ${c.pull_request.title}
13 %if c.pull_request.is_closed():
13 %if c.pull_request.is_closed():
14 (${_('Closed')})
14 (${_('Closed')})
15 %endif
15 %endif
16 </span>
16 </span>
17 <div id="pr-title-edit" class="input" style="display: none;">
17 <div id="pr-title-edit" class="input" style="display: none;">
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 </div>
19 </div>
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_nav()">
22 <%def name="menu_bar_nav()">
23 ${self.menu_items(active='repositories')}
23 ${self.menu_items(active='repositories')}
24 </%def>
24 </%def>
25
25
26 <%def name="menu_bar_subnav()">
26 <%def name="menu_bar_subnav()">
27 ${self.repo_menu(active='showpullrequest')}
27 ${self.repo_menu(active='showpullrequest')}
28 </%def>
28 </%def>
29
29
30 <%def name="main()">
30 <%def name="main()">
31 <script type="text/javascript">
31 <script type="text/javascript">
32 // TODO: marcink switch this to pyroutes
32 // TODO: marcink switch this to pyroutes
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 </script>
35 </script>
36 <div class="box">
36 <div class="box">
37 <div class="title">
37 <div class="title">
38 ${self.repo_page_title(c.rhodecode_db_repo)}
38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 </div>
39 </div>
40
40
41 ${self.breadcrumbs()}
41 ${self.breadcrumbs()}
42
42
43
43
44 <div class="box pr-summary">
44 <div class="box pr-summary">
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <span id="open_edit_pullrequest" class="block-right action_button">${_('Edit')}</span>
50 <span id="open_edit_pullrequest" class="block-right action_button">${_('Edit')}</span>
51 <span id="close_edit_pullrequest" class="block-right action_button" style="display: none;">${_('Close')}</span>
51 <span id="close_edit_pullrequest" class="block-right action_button" style="display: none;">${_('Close')}</span>
52 %endif
52 %endif
53 </div>
53 </div>
54
54
55 <div id="summary" class="fields pr-details-content">
55 <div id="summary" class="fields pr-details-content">
56 <div class="field">
56 <div class="field">
57 <div class="label-summary">
57 <div class="label-summary">
58 <label>${_('Origin')}:</label>
58 <label>${_('Origin')}:</label>
59 </div>
59 </div>
60 <div class="input">
60 <div class="input">
61 <div class="pr-origininfo">
61 <div class="pr-origininfo">
62 ## branch link is only valid if it is a branch
62 ## branch link is only valid if it is a branch
63 <span class="tag">
63 <span class="tag">
64 %if c.pull_request.source_ref_parts.type == 'branch':
64 %if c.pull_request.source_ref_parts.type == 'branch':
65 <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>
65 <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>
66 %else:
66 %else:
67 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
67 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
68 %endif
68 %endif
69 </span>
69 </span>
70 <span class="clone-url">
70 <span class="clone-url">
71 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
71 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
72 </span>
72 </span>
73 </div>
73 </div>
74 <div class="pr-pullinfo">
74 <div class="pr-pullinfo">
75 %if h.is_hg(c.pull_request.source_repo):
75 %if h.is_hg(c.pull_request.source_repo):
76 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
76 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
77 %elif h.is_git(c.pull_request.source_repo):
77 %elif h.is_git(c.pull_request.source_repo):
78 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
78 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
79 %endif
79 %endif
80 </div>
80 </div>
81 </div>
81 </div>
82 </div>
82 </div>
83 <div class="field">
83 <div class="field">
84 <div class="label-summary">
84 <div class="label-summary">
85 <label>${_('Target')}:</label>
85 <label>${_('Target')}:</label>
86 </div>
86 </div>
87 <div class="input">
87 <div class="input">
88 <div class="pr-targetinfo">
88 <div class="pr-targetinfo">
89 ## branch link is only valid if it is a branch
89 ## branch link is only valid if it is a branch
90 <span class="tag">
90 <span class="tag">
91 %if c.pull_request.target_ref_parts.type == 'branch':
91 %if c.pull_request.target_ref_parts.type == 'branch':
92 <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>
92 <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>
93 %else:
93 %else:
94 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
94 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
95 %endif
95 %endif
96 </span>
96 </span>
97 <span class="clone-url">
97 <span class="clone-url">
98 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
98 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
99 </span>
99 </span>
100 </div>
100 </div>
101 </div>
101 </div>
102 </div>
102 </div>
103
103
104 ## Link to the shadow repository.
104 ## Link to the shadow repository.
105 %if not c.pull_request.is_closed() and c.pull_request.last_merge_rev:
105 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
106 <div class="field">
106 <div class="field">
107 <div class="label-summary">
107 <div class="label-summary">
108 <label>Merge:</label>
108 <label>Merge:</label>
109 </div>
109 </div>
110 <div class="input">
110 <div class="input">
111 <div class="pr-mergeinfo">
111 <div class="pr-mergeinfo">
112 %if h.is_hg(c.pull_request.target_repo):
112 %if h.is_hg(c.pull_request.target_repo):
113 <input type="text" value="hg clone -u pr-merge ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
113 <input type="text" 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">
114 %elif h.is_git(c.pull_request.target_repo):
114 %elif h.is_git(c.pull_request.target_repo):
115 <input type="text" value="git clone --branch pr-merge ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
115 <input type="text" 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">
116 %endif
116 %endif
117 </div>
117 </div>
118 </div>
118 </div>
119 </div>
119 </div>
120 %endif
120 %endif
121
121
122 <div class="field">
122 <div class="field">
123 <div class="label-summary">
123 <div class="label-summary">
124 <label>${_('Review')}:</label>
124 <label>${_('Review')}:</label>
125 </div>
125 </div>
126 <div class="input">
126 <div class="input">
127 %if c.pull_request_review_status:
127 %if c.pull_request_review_status:
128 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
128 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
129 <span class="changeset-status-lbl tooltip">
129 <span class="changeset-status-lbl tooltip">
130 %if c.pull_request.is_closed():
130 %if c.pull_request.is_closed():
131 ${_('Closed')},
131 ${_('Closed')},
132 %endif
132 %endif
133 ${h.commit_status_lbl(c.pull_request_review_status)}
133 ${h.commit_status_lbl(c.pull_request_review_status)}
134 </span>
134 </span>
135 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
135 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
136 %endif
136 %endif
137 </div>
137 </div>
138 </div>
138 </div>
139 <div class="field">
139 <div class="field">
140 <div class="pr-description-label label-summary">
140 <div class="pr-description-label label-summary">
141 <label>${_('Description')}:</label>
141 <label>${_('Description')}:</label>
142 </div>
142 </div>
143 <div id="pr-desc" class="input">
143 <div id="pr-desc" class="input">
144 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
144 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
145 </div>
145 </div>
146 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
146 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
147 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
147 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
148 </div>
148 </div>
149 </div>
149 </div>
150 <div class="field">
150 <div class="field">
151 <div class="label-summary">
151 <div class="label-summary">
152 <label>${_('Comments')}:</label>
152 <label>${_('Comments')}:</label>
153 </div>
153 </div>
154 <div class="input">
154 <div class="input">
155 <div>
155 <div>
156 <div class="comments-number">
156 <div class="comments-number">
157 %if c.comments:
157 %if c.comments:
158 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
158 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
159 %else:
159 %else:
160 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
160 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
161 %endif
161 %endif
162 %if c.inline_cnt:
162 %if c.inline_cnt:
163 ## this is replaced with a proper link to first comment via JS linkifyComments() func
163 ## this is replaced with a proper link to first comment via JS linkifyComments() func
164 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
164 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
165 %else:
165 %else:
166 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
166 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
167 %endif
167 %endif
168
168
169 % if c.outdated_cnt:
169 % if c.outdated_cnt:
170 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
170 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
171 % endif
171 % endif
172 </div>
172 </div>
173 </div>
173 </div>
174 </div>
174 </div>
175 </div>
175 </div>
176 <div id="pr-save" class="field" style="display: none;">
176 <div id="pr-save" class="field" style="display: none;">
177 <div class="label-summary"></div>
177 <div class="label-summary"></div>
178 <div class="input">
178 <div class="input">
179 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
179 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
180 </div>
180 </div>
181 </div>
181 </div>
182 </div>
182 </div>
183 </div>
183 </div>
184 <div>
184 <div>
185 ## AUTHOR
185 ## AUTHOR
186 <div class="reviewers-title block-right">
186 <div class="reviewers-title block-right">
187 <div class="pr-details-title">
187 <div class="pr-details-title">
188 ${_('Author')}
188 ${_('Author')}
189 </div>
189 </div>
190 </div>
190 </div>
191 <div class="block-right pr-details-content reviewers">
191 <div class="block-right pr-details-content reviewers">
192 <ul class="group_members">
192 <ul class="group_members">
193 <li>
193 <li>
194 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
194 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
195 </li>
195 </li>
196 </ul>
196 </ul>
197 </div>
197 </div>
198 ## REVIEWERS
198 ## REVIEWERS
199 <div class="reviewers-title block-right">
199 <div class="reviewers-title block-right">
200 <div class="pr-details-title">
200 <div class="pr-details-title">
201 ${_('Pull request reviewers')}
201 ${_('Pull request reviewers')}
202 %if c.allowed_to_update:
202 %if c.allowed_to_update:
203 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
203 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
204 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
204 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
205 %endif
205 %endif
206 </div>
206 </div>
207 </div>
207 </div>
208 <div id="reviewers" class="block-right pr-details-content reviewers">
208 <div id="reviewers" class="block-right pr-details-content reviewers">
209 ## members goes here !
209 ## members goes here !
210 <input type="hidden" name="__start__" value="review_members:sequence">
210 <input type="hidden" name="__start__" value="review_members:sequence">
211 <ul id="review_members" class="group_members">
211 <ul id="review_members" class="group_members">
212 %for member,reasons,status in c.pull_request_reviewers:
212 %for member,reasons,status in c.pull_request_reviewers:
213 <li id="reviewer_${member.user_id}">
213 <li id="reviewer_${member.user_id}">
214 <div class="reviewers_member">
214 <div class="reviewers_member">
215 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
215 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
216 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
216 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
217 </div>
217 </div>
218 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
218 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
219 ${self.gravatar_with_user(member.email, 16)}
219 ${self.gravatar_with_user(member.email, 16)}
220 </div>
220 </div>
221 <input type="hidden" name="__start__" value="reviewer:mapping">
221 <input type="hidden" name="__start__" value="reviewer:mapping">
222 <input type="hidden" name="__start__" value="reasons:sequence">
222 <input type="hidden" name="__start__" value="reasons:sequence">
223 %for reason in reasons:
223 %for reason in reasons:
224 <div class="reviewer_reason">- ${reason}</div>
224 <div class="reviewer_reason">- ${reason}</div>
225 <input type="hidden" name="reason" value="${reason}">
225 <input type="hidden" name="reason" value="${reason}">
226
226
227 %endfor
227 %endfor
228 <input type="hidden" name="__end__" value="reasons:sequence">
228 <input type="hidden" name="__end__" value="reasons:sequence">
229 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
229 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
230 <input type="hidden" name="__end__" value="reviewer:mapping">
230 <input type="hidden" name="__end__" value="reviewer:mapping">
231 %if c.allowed_to_update:
231 %if c.allowed_to_update:
232 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
232 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
233 <i class="icon-remove-sign" ></i>
233 <i class="icon-remove-sign" ></i>
234 </div>
234 </div>
235 %endif
235 %endif
236 </div>
236 </div>
237 </li>
237 </li>
238 %endfor
238 %endfor
239 </ul>
239 </ul>
240 <input type="hidden" name="__end__" value="review_members:sequence">
240 <input type="hidden" name="__end__" value="review_members:sequence">
241 %if not c.pull_request.is_closed():
241 %if not c.pull_request.is_closed():
242 <div id="add_reviewer_input" class='ac' style="display: none;">
242 <div id="add_reviewer_input" class='ac' style="display: none;">
243 %if c.allowed_to_update:
243 %if c.allowed_to_update:
244 <div class="reviewer_ac">
244 <div class="reviewer_ac">
245 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
245 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
246 <div id="reviewers_container"></div>
246 <div id="reviewers_container"></div>
247 </div>
247 </div>
248 <div>
248 <div>
249 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
249 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
250 </div>
250 </div>
251 %endif
251 %endif
252 </div>
252 </div>
253 %endif
253 %endif
254 </div>
254 </div>
255 </div>
255 </div>
256 </div>
256 </div>
257 <div class="box">
257 <div class="box">
258 ##DIFF
258 ##DIFF
259 <div class="table" >
259 <div class="table" >
260 <div id="changeset_compare_view_content">
260 <div id="changeset_compare_view_content">
261 ##CS
261 ##CS
262 % if c.missing_requirements:
262 % if c.missing_requirements:
263 <div class="box">
263 <div class="box">
264 <div class="alert alert-warning">
264 <div class="alert alert-warning">
265 <div>
265 <div>
266 <strong>${_('Missing requirements:')}</strong>
266 <strong>${_('Missing requirements:')}</strong>
267 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
267 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
268 </div>
268 </div>
269 </div>
269 </div>
270 </div>
270 </div>
271 % elif c.missing_commits:
271 % elif c.missing_commits:
272 <div class="box">
272 <div class="box">
273 <div class="alert alert-warning">
273 <div class="alert alert-warning">
274 <div>
274 <div>
275 <strong>${_('Missing commits')}:</strong>
275 <strong>${_('Missing commits')}:</strong>
276 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
276 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
277 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
277 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
278 </div>
278 </div>
279 </div>
279 </div>
280 </div>
280 </div>
281 % endif
281 % endif
282 <div class="compare_view_commits_title">
282 <div class="compare_view_commits_title">
283 % if c.allowed_to_update and not c.pull_request.is_closed():
283 % if c.allowed_to_update and not c.pull_request.is_closed():
284 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
284 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
285 % endif
285 % endif
286 % if len(c.commit_ranges):
286 % if len(c.commit_ranges):
287 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
287 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
288 % endif
288 % endif
289 </div>
289 </div>
290 % if not c.missing_commits:
290 % if not c.missing_commits:
291 <%include file="/compare/compare_commits.html" />
291 <%include file="/compare/compare_commits.html" />
292 ## FILES
292 ## FILES
293 <div class="cs_files_title">
293 <div class="cs_files_title">
294 <span class="cs_files_expand">
294 <span class="cs_files_expand">
295 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
295 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
296 </span>
296 </span>
297 <h2>
297 <h2>
298 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
298 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
299 </h2>
299 </h2>
300 </div>
300 </div>
301 % endif
301 % endif
302 <div class="cs_files">
302 <div class="cs_files">
303 %if not c.files and not c.missing_commits:
303 %if not c.files and not c.missing_commits:
304 <span class="empty_data">${_('No files')}</span>
304 <span class="empty_data">${_('No files')}</span>
305 %endif
305 %endif
306 <table class="compare_view_files">
306 <table class="compare_view_files">
307 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
307 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
308 %for FID, change, path, stats in c.files:
308 %for FID, change, path, stats in c.files:
309 <tr class="cs_${change} collapse_file" fid="${FID}">
309 <tr class="cs_${change} collapse_file" fid="${FID}">
310 <td class="cs_icon_td">
310 <td class="cs_icon_td">
311 <span class="collapse_file_icon" fid="${FID}"></span>
311 <span class="collapse_file_icon" fid="${FID}"></span>
312 </td>
312 </td>
313 <td class="cs_icon_td">
313 <td class="cs_icon_td">
314 <div class="flag_status not_reviewed hidden"></div>
314 <div class="flag_status not_reviewed hidden"></div>
315 </td>
315 </td>
316 <td class="cs_${change}" id="a_${FID}">
316 <td class="cs_${change}" id="a_${FID}">
317 <div class="node">
317 <div class="node">
318 <a href="#a_${FID}">
318 <a href="#a_${FID}">
319 <i class="icon-file-${change.lower()}"></i>
319 <i class="icon-file-${change.lower()}"></i>
320 ${h.safe_unicode(path)}
320 ${h.safe_unicode(path)}
321 </a>
321 </a>
322 </div>
322 </div>
323 </td>
323 </td>
324 <td>
324 <td>
325 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
325 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
326 <div class="comment-bubble pull-right" data-path="${path}">
326 <div class="comment-bubble pull-right" data-path="${path}">
327 <i class="icon-comment"></i>
327 <i class="icon-comment"></i>
328 </div>
328 </div>
329 </td>
329 </td>
330 </tr>
330 </tr>
331 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
331 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
332 <td></td>
332 <td></td>
333 <td></td>
333 <td></td>
334 <td class="cs_${change}">
334 <td class="cs_${change}">
335 %if c.target_repo.repo_name == c.repo_name:
335 %if c.target_repo.repo_name == c.repo_name:
336 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
336 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
337 %else:
337 %else:
338 ## this is slightly different case later, since the other repo can have this
338 ## this is slightly different case later, since the other repo can have this
339 ## file in other state than the origin repo
339 ## file in other state than the origin repo
340 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
340 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
341 %endif
341 %endif
342 </td>
342 </td>
343 <td class="td-actions rc-form">
343 <td class="td-actions rc-form">
344 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
344 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
345 <span class="comments-show">${_('Show comments')}</span>
345 <span class="comments-show">${_('Show comments')}</span>
346 <span class="comments-hide">${_('Hide comments')}</span>
346 <span class="comments-hide">${_('Hide comments')}</span>
347 </div>
347 </div>
348 </td>
348 </td>
349 </tr>
349 </tr>
350 <tr id="tr_${FID}">
350 <tr id="tr_${FID}">
351 <td></td>
351 <td></td>
352 <td></td>
352 <td></td>
353 <td class="injected_diff" colspan="2">
353 <td class="injected_diff" colspan="2">
354 ${diff_block.diff_block_simple([c.changes[FID]])}
354 ${diff_block.diff_block_simple([c.changes[FID]])}
355 </td>
355 </td>
356 </tr>
356 </tr>
357
357
358 ## Loop through inline comments
358 ## Loop through inline comments
359 % if c.outdated_comments.get(path,False):
359 % if c.outdated_comments.get(path,False):
360 <tr class="outdated">
360 <tr class="outdated">
361 <td></td>
361 <td></td>
362 <td></td>
362 <td></td>
363 <td colspan="2">
363 <td colspan="2">
364 <p>${_('Outdated Inline Comments')}:</p>
364 <p>${_('Outdated Inline Comments')}:</p>
365 </td>
365 </td>
366 </tr>
366 </tr>
367 <tr class="outdated">
367 <tr class="outdated">
368 <td></td>
368 <td></td>
369 <td></td>
369 <td></td>
370 <td colspan="2" class="outdated_comment_block">
370 <td colspan="2" class="outdated_comment_block">
371 % for line, comments in c.outdated_comments[path].iteritems():
371 % for line, comments in c.outdated_comments[path].iteritems():
372 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
372 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
373 % for co in comments:
373 % for co in comments:
374 ${comment.comment_block_outdated(co)}
374 ${comment.comment_block_outdated(co)}
375 % endfor
375 % endfor
376 </div>
376 </div>
377 % endfor
377 % endfor
378 </td>
378 </td>
379 </tr>
379 </tr>
380 % endif
380 % endif
381 %endfor
381 %endfor
382 ## Loop through inline comments for deleted files
382 ## Loop through inline comments for deleted files
383 %for path in c.deleted_files:
383 %for path in c.deleted_files:
384 <tr class="outdated deleted">
384 <tr class="outdated deleted">
385 <td></td>
385 <td></td>
386 <td></td>
386 <td></td>
387 <td>${path}</td>
387 <td>${path}</td>
388 </tr>
388 </tr>
389 <tr class="outdated deleted">
389 <tr class="outdated deleted">
390 <td></td>
390 <td></td>
391 <td></td>
391 <td></td>
392 <td>(${_('Removed')})</td>
392 <td>(${_('Removed')})</td>
393 </tr>
393 </tr>
394 % if path in c.outdated_comments:
394 % if path in c.outdated_comments:
395 <tr class="outdated deleted">
395 <tr class="outdated deleted">
396 <td></td>
396 <td></td>
397 <td></td>
397 <td></td>
398 <td colspan="2">
398 <td colspan="2">
399 <p>${_('Outdated Inline Comments')}:</p>
399 <p>${_('Outdated Inline Comments')}:</p>
400 </td>
400 </td>
401 </tr>
401 </tr>
402 <tr class="outdated">
402 <tr class="outdated">
403 <td></td>
403 <td></td>
404 <td></td>
404 <td></td>
405 <td colspan="2" class="outdated_comment_block">
405 <td colspan="2" class="outdated_comment_block">
406 % for line, comments in c.outdated_comments[path].iteritems():
406 % for line, comments in c.outdated_comments[path].iteritems():
407 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
407 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
408 % for co in comments:
408 % for co in comments:
409 ${comment.comment_block_outdated(co)}
409 ${comment.comment_block_outdated(co)}
410 % endfor
410 % endfor
411 </div>
411 </div>
412 % endfor
412 % endfor
413 </td>
413 </td>
414 </tr>
414 </tr>
415 % endif
415 % endif
416 %endfor
416 %endfor
417 </table>
417 </table>
418 </div>
418 </div>
419 % if c.limited_diff:
419 % if c.limited_diff:
420 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
420 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
421 % endif
421 % endif
422 </div>
422 </div>
423 </div>
423 </div>
424
424
425 % if c.limited_diff:
425 % if c.limited_diff:
426 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
426 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
427 % endif
427 % endif
428
428
429 ## template for inline comment form
429 ## template for inline comment form
430 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
430 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
431 ${comment.comment_inline_form()}
431 ${comment.comment_inline_form()}
432
432
433 ## render comments and inlines
433 ## render comments and inlines
434 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
434 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
435
435
436 % if not c.pull_request.is_closed():
436 % if not c.pull_request.is_closed():
437 ## main comment form and it status
437 ## main comment form and it status
438 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
438 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
439 pull_request_id=c.pull_request.pull_request_id),
439 pull_request_id=c.pull_request.pull_request_id),
440 c.pull_request_review_status,
440 c.pull_request_review_status,
441 is_pull_request=True, change_status=c.allowed_to_change_status)}
441 is_pull_request=True, change_status=c.allowed_to_change_status)}
442 %endif
442 %endif
443
443
444 <script type="text/javascript">
444 <script type="text/javascript">
445 if (location.hash) {
445 if (location.hash) {
446 var result = splitDelimitedHash(location.hash);
446 var result = splitDelimitedHash(location.hash);
447 var line = $('html').find(result.loc);
447 var line = $('html').find(result.loc);
448 if (line.length > 0){
448 if (line.length > 0){
449 offsetScroll(line, 70);
449 offsetScroll(line, 70);
450 }
450 }
451 }
451 }
452 $(function(){
452 $(function(){
453 ReviewerAutoComplete('user');
453 ReviewerAutoComplete('user');
454 // custom code mirror
454 // custom code mirror
455 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
455 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
456
456
457 var PRDetails = {
457 var PRDetails = {
458 editButton: $('#open_edit_pullrequest'),
458 editButton: $('#open_edit_pullrequest'),
459 closeButton: $('#close_edit_pullrequest'),
459 closeButton: $('#close_edit_pullrequest'),
460 viewFields: $('#pr-desc, #pr-title'),
460 viewFields: $('#pr-desc, #pr-title'),
461 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
461 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
462
462
463 init: function() {
463 init: function() {
464 var that = this;
464 var that = this;
465 this.editButton.on('click', function(e) { that.edit(); });
465 this.editButton.on('click', function(e) { that.edit(); });
466 this.closeButton.on('click', function(e) { that.view(); });
466 this.closeButton.on('click', function(e) { that.view(); });
467 },
467 },
468
468
469 edit: function(event) {
469 edit: function(event) {
470 this.viewFields.hide();
470 this.viewFields.hide();
471 this.editButton.hide();
471 this.editButton.hide();
472 this.editFields.show();
472 this.editFields.show();
473 codeMirrorInstance.refresh();
473 codeMirrorInstance.refresh();
474 },
474 },
475
475
476 view: function(event) {
476 view: function(event) {
477 this.editFields.hide();
477 this.editFields.hide();
478 this.closeButton.hide();
478 this.closeButton.hide();
479 this.viewFields.show();
479 this.viewFields.show();
480 }
480 }
481 };
481 };
482
482
483 var ReviewersPanel = {
483 var ReviewersPanel = {
484 editButton: $('#open_edit_reviewers'),
484 editButton: $('#open_edit_reviewers'),
485 closeButton: $('#close_edit_reviewers'),
485 closeButton: $('#close_edit_reviewers'),
486 addButton: $('#add_reviewer_input'),
486 addButton: $('#add_reviewer_input'),
487 removeButtons: $('.reviewer_member_remove'),
487 removeButtons: $('.reviewer_member_remove'),
488
488
489 init: function() {
489 init: function() {
490 var that = this;
490 var that = this;
491 this.editButton.on('click', function(e) { that.edit(); });
491 this.editButton.on('click', function(e) { that.edit(); });
492 this.closeButton.on('click', function(e) { that.close(); });
492 this.closeButton.on('click', function(e) { that.close(); });
493 },
493 },
494
494
495 edit: function(event) {
495 edit: function(event) {
496 this.editButton.hide();
496 this.editButton.hide();
497 this.closeButton.show();
497 this.closeButton.show();
498 this.addButton.show();
498 this.addButton.show();
499 this.removeButtons.css('visibility', 'visible');
499 this.removeButtons.css('visibility', 'visible');
500 },
500 },
501
501
502 close: function(event) {
502 close: function(event) {
503 this.editButton.show();
503 this.editButton.show();
504 this.closeButton.hide();
504 this.closeButton.hide();
505 this.addButton.hide();
505 this.addButton.hide();
506 this.removeButtons.css('visibility', 'hidden');
506 this.removeButtons.css('visibility', 'hidden');
507 }
507 }
508 };
508 };
509
509
510 PRDetails.init();
510 PRDetails.init();
511 ReviewersPanel.init();
511 ReviewersPanel.init();
512
512
513 $('#show-outdated-comments').on('click', function(e){
513 $('#show-outdated-comments').on('click', function(e){
514 var button = $(this);
514 var button = $(this);
515 var outdated = $('.outdated');
515 var outdated = $('.outdated');
516 if (button.html() === "(Show)") {
516 if (button.html() === "(Show)") {
517 button.html("(Hide)");
517 button.html("(Hide)");
518 outdated.show();
518 outdated.show();
519 } else {
519 } else {
520 button.html("(Show)");
520 button.html("(Show)");
521 outdated.hide();
521 outdated.hide();
522 }
522 }
523 });
523 });
524
524
525 $('.show-inline-comments').on('change', function(e){
525 $('.show-inline-comments').on('change', function(e){
526 var show = 'none';
526 var show = 'none';
527 var target = e.currentTarget;
527 var target = e.currentTarget;
528 if(target.checked){
528 if(target.checked){
529 show = ''
529 show = ''
530 }
530 }
531 var boxid = $(target).attr('id_for');
531 var boxid = $(target).attr('id_for');
532 var comments = $('#{0} .inline-comments'.format(boxid));
532 var comments = $('#{0} .inline-comments'.format(boxid));
533 var fn_display = function(idx){
533 var fn_display = function(idx){
534 $(this).css('display', show);
534 $(this).css('display', show);
535 };
535 };
536 $(comments).each(fn_display);
536 $(comments).each(fn_display);
537 var btns = $('#{0} .inline-comments-button'.format(boxid));
537 var btns = $('#{0} .inline-comments-button'.format(boxid));
538 $(btns).each(fn_display);
538 $(btns).each(fn_display);
539 });
539 });
540
540
541 // inject comments into their proper positions
541 // inject comments into their proper positions
542 var file_comments = $('.inline-comment-placeholder');
542 var file_comments = $('.inline-comment-placeholder');
543 %if c.pull_request.is_closed():
543 %if c.pull_request.is_closed():
544 renderInlineComments(file_comments, false);
544 renderInlineComments(file_comments, false);
545 %else:
545 %else:
546 renderInlineComments(file_comments, true);
546 renderInlineComments(file_comments, true);
547 %endif
547 %endif
548 var commentTotals = {};
548 var commentTotals = {};
549 $.each(file_comments, function(i, comment) {
549 $.each(file_comments, function(i, comment) {
550 var path = $(comment).attr('path');
550 var path = $(comment).attr('path');
551 var comms = $(comment).children().length;
551 var comms = $(comment).children().length;
552 if (path in commentTotals) {
552 if (path in commentTotals) {
553 commentTotals[path] += comms;
553 commentTotals[path] += comms;
554 } else {
554 } else {
555 commentTotals[path] = comms;
555 commentTotals[path] = comms;
556 }
556 }
557 });
557 });
558 $.each(commentTotals, function(path, total) {
558 $.each(commentTotals, function(path, total) {
559 var elem = $('.comment-bubble[data-path="'+ path +'"]');
559 var elem = $('.comment-bubble[data-path="'+ path +'"]');
560 elem.css('visibility', 'visible');
560 elem.css('visibility', 'visible');
561 elem.html(elem.html() + ' ' + total );
561 elem.html(elem.html() + ' ' + total );
562 });
562 });
563
563
564 $('#merge_pull_request_form').submit(function() {
564 $('#merge_pull_request_form').submit(function() {
565 if (!$('#merge_pull_request').attr('disabled')) {
565 if (!$('#merge_pull_request').attr('disabled')) {
566 $('#merge_pull_request').attr('disabled', 'disabled');
566 $('#merge_pull_request').attr('disabled', 'disabled');
567 }
567 }
568 return true;
568 return true;
569 });
569 });
570
570
571 $('#edit_pull_request').on('click', function(e){
571 $('#edit_pull_request').on('click', function(e){
572 var title = $('#pr-title-input').val();
572 var title = $('#pr-title-input').val();
573 var description = codeMirrorInstance.getValue();
573 var description = codeMirrorInstance.getValue();
574 editPullRequest(
574 editPullRequest(
575 "${c.repo_name}", "${c.pull_request.pull_request_id}",
575 "${c.repo_name}", "${c.pull_request.pull_request_id}",
576 title, description);
576 title, description);
577 });
577 });
578
578
579 $('#update_pull_request').on('click', function(e){
579 $('#update_pull_request').on('click', function(e){
580 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
580 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
581 });
581 });
582
582
583 $('#update_commits').on('click', function(e){
583 $('#update_commits').on('click', function(e){
584 var isDisabled = !$(e.currentTarget).attr('disabled');
584 var isDisabled = !$(e.currentTarget).attr('disabled');
585 $(e.currentTarget).text(_gettext('Updating...'));
585 $(e.currentTarget).text(_gettext('Updating...'));
586 $(e.currentTarget).attr('disabled', 'disabled');
586 $(e.currentTarget).attr('disabled', 'disabled');
587 if(isDisabled){
587 if(isDisabled){
588 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
588 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
589 }
589 }
590
590
591 });
591 });
592 // fixing issue with caches on firefox
592 // fixing issue with caches on firefox
593 $('#update_commits').removeAttr("disabled");
593 $('#update_commits').removeAttr("disabled");
594
594
595 $('#close_pull_request').on('click', function(e){
595 $('#close_pull_request').on('click', function(e){
596 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
596 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
597 });
597 });
598
598
599 $('.show-inline-comments').on('click', function(e){
599 $('.show-inline-comments').on('click', function(e){
600 var boxid = $(this).attr('data-comment-id');
600 var boxid = $(this).attr('data-comment-id');
601 var button = $(this);
601 var button = $(this);
602
602
603 if(button.hasClass("comments-visible")) {
603 if(button.hasClass("comments-visible")) {
604 $('#{0} .inline-comments'.format(boxid)).each(function(index){
604 $('#{0} .inline-comments'.format(boxid)).each(function(index){
605 $(this).hide();
605 $(this).hide();
606 })
606 })
607 button.removeClass("comments-visible");
607 button.removeClass("comments-visible");
608 } else {
608 } else {
609 $('#{0} .inline-comments'.format(boxid)).each(function(index){
609 $('#{0} .inline-comments'.format(boxid)).each(function(index){
610 $(this).show();
610 $(this).show();
611 })
611 })
612 button.addClass("comments-visible");
612 button.addClass("comments-visible");
613 }
613 }
614 });
614 });
615 })
615 })
616 </script>
616 </script>
617
617
618 </div>
618 </div>
619 </div>
619 </div>
620
620
621 </%def>
621 </%def>
General Comments 0
You need to be logged in to leave comments. Login now