##// END OF EJS Templates
pull-requests: prepare the migration of pull request to pyramid....
marcink -
r1813:07e2beb0 default
parent child Browse files
Show More
@@ -1,184 +1,584 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import collections
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
23 from pyramid.view import view_config
25 from pyramid.view import view_config
24
26
25 from rhodecode.apps._base import RepoAppView, DataGridAppView
27 from rhodecode.apps._base import RepoAppView, DataGridAppView
26 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h, diffs, codeblocks
27 from rhodecode.lib import audit_logger
28 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
29 LoginRequired, HasRepoPermissionAnyDecorator)
30 LoginRequired, HasRepoPermissionAnyDecorator)
30 from rhodecode.lib.utils import PartialRenderer
31 from rhodecode.lib.utils import PartialRenderer
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
33 from rhodecode.lib.vcs.backends.base import EmptyCommit
34 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \
35 RepositoryRequirementError, NodeDoesNotExistError
32 from rhodecode.model.comment import CommentsModel
36 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest
37 from rhodecode.model.db import PullRequest, PullRequestVersion, \
34 from rhodecode.model.pull_request import PullRequestModel
38 ChangesetComment, ChangesetStatus
39 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
35
40
36 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
37
42
38
43
39 class RepoPullRequestsView(RepoAppView, DataGridAppView):
44 class RepoPullRequestsView(RepoAppView, DataGridAppView):
40
45
41 def load_default_context(self):
46 def load_default_context(self):
42 c = self._get_local_tmpl_context()
47 c = self._get_local_tmpl_context(include_app_defaults=True)
43
44 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
48 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
45 c.repo_info = self.db_repo
49 c.repo_info = self.db_repo
46
50 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
51 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
47 self._register_global_c(c)
52 self._register_global_c(c)
48 return c
53 return c
49
54
50 def _get_pull_requests_list(
55 def _get_pull_requests_list(
51 self, repo_name, source, filter_type, opened_by, statuses):
56 self, repo_name, source, filter_type, opened_by, statuses):
52
57
53 draw, start, limit = self._extract_chunk(self.request)
58 draw, start, limit = self._extract_chunk(self.request)
54 search_q, order_by, order_dir = self._extract_ordering(self.request)
59 search_q, order_by, order_dir = self._extract_ordering(self.request)
55 _render = PartialRenderer('data_table/_dt_elements.mako')
60 _render = PartialRenderer('data_table/_dt_elements.mako')
56
61
57 # pagination
62 # pagination
58
63
59 if filter_type == 'awaiting_review':
64 if filter_type == 'awaiting_review':
60 pull_requests = PullRequestModel().get_awaiting_review(
65 pull_requests = PullRequestModel().get_awaiting_review(
61 repo_name, source=source, opened_by=opened_by,
66 repo_name, source=source, opened_by=opened_by,
62 statuses=statuses, offset=start, length=limit,
67 statuses=statuses, offset=start, length=limit,
63 order_by=order_by, order_dir=order_dir)
68 order_by=order_by, order_dir=order_dir)
64 pull_requests_total_count = PullRequestModel().count_awaiting_review(
69 pull_requests_total_count = PullRequestModel().count_awaiting_review(
65 repo_name, source=source, statuses=statuses,
70 repo_name, source=source, statuses=statuses,
66 opened_by=opened_by)
71 opened_by=opened_by)
67 elif filter_type == 'awaiting_my_review':
72 elif filter_type == 'awaiting_my_review':
68 pull_requests = PullRequestModel().get_awaiting_my_review(
73 pull_requests = PullRequestModel().get_awaiting_my_review(
69 repo_name, source=source, opened_by=opened_by,
74 repo_name, source=source, opened_by=opened_by,
70 user_id=self._rhodecode_user.user_id, statuses=statuses,
75 user_id=self._rhodecode_user.user_id, statuses=statuses,
71 offset=start, length=limit, order_by=order_by,
76 offset=start, length=limit, order_by=order_by,
72 order_dir=order_dir)
77 order_dir=order_dir)
73 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
78 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
74 repo_name, source=source, user_id=self._rhodecode_user.user_id,
79 repo_name, source=source, user_id=self._rhodecode_user.user_id,
75 statuses=statuses, opened_by=opened_by)
80 statuses=statuses, opened_by=opened_by)
76 else:
81 else:
77 pull_requests = PullRequestModel().get_all(
82 pull_requests = PullRequestModel().get_all(
78 repo_name, source=source, opened_by=opened_by,
83 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
84 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
85 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_all(
86 pull_requests_total_count = PullRequestModel().count_all(
82 repo_name, source=source, statuses=statuses,
87 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
88 opened_by=opened_by)
84
89
85 data = []
90 data = []
86 comments_model = CommentsModel()
91 comments_model = CommentsModel()
87 for pr in pull_requests:
92 for pr in pull_requests:
88 comments = comments_model.get_all_comments(
93 comments = comments_model.get_all_comments(
89 self.db_repo.repo_id, pull_request=pr)
94 self.db_repo.repo_id, pull_request=pr)
90
95
91 data.append({
96 data.append({
92 'name': _render('pullrequest_name',
97 'name': _render('pullrequest_name',
93 pr.pull_request_id, pr.target_repo.repo_name),
98 pr.pull_request_id, pr.target_repo.repo_name),
94 'name_raw': pr.pull_request_id,
99 'name_raw': pr.pull_request_id,
95 'status': _render('pullrequest_status',
100 'status': _render('pullrequest_status',
96 pr.calculated_review_status()),
101 pr.calculated_review_status()),
97 'title': _render(
102 'title': _render(
98 'pullrequest_title', pr.title, pr.description),
103 'pullrequest_title', pr.title, pr.description),
99 'description': h.escape(pr.description),
104 'description': h.escape(pr.description),
100 'updated_on': _render('pullrequest_updated_on',
105 'updated_on': _render('pullrequest_updated_on',
101 h.datetime_to_time(pr.updated_on)),
106 h.datetime_to_time(pr.updated_on)),
102 'updated_on_raw': h.datetime_to_time(pr.updated_on),
107 'updated_on_raw': h.datetime_to_time(pr.updated_on),
103 'created_on': _render('pullrequest_updated_on',
108 'created_on': _render('pullrequest_updated_on',
104 h.datetime_to_time(pr.created_on)),
109 h.datetime_to_time(pr.created_on)),
105 'created_on_raw': h.datetime_to_time(pr.created_on),
110 'created_on_raw': h.datetime_to_time(pr.created_on),
106 'author': _render('pullrequest_author',
111 'author': _render('pullrequest_author',
107 pr.author.full_contact, ),
112 pr.author.full_contact, ),
108 'author_raw': pr.author.full_name,
113 'author_raw': pr.author.full_name,
109 'comments': _render('pullrequest_comments', len(comments)),
114 'comments': _render('pullrequest_comments', len(comments)),
110 'comments_raw': len(comments),
115 'comments_raw': len(comments),
111 'closed': pr.is_closed(),
116 'closed': pr.is_closed(),
112 })
117 })
113
118
114 data = ({
119 data = ({
115 'draw': draw,
120 'draw': draw,
116 'data': data,
121 'data': data,
117 'recordsTotal': pull_requests_total_count,
122 'recordsTotal': pull_requests_total_count,
118 'recordsFiltered': pull_requests_total_count,
123 'recordsFiltered': pull_requests_total_count,
119 })
124 })
120 return data
125 return data
121
126
122 @LoginRequired()
127 @LoginRequired()
123 @HasRepoPermissionAnyDecorator(
128 @HasRepoPermissionAnyDecorator(
124 'repository.read', 'repository.write', 'repository.admin')
129 'repository.read', 'repository.write', 'repository.admin')
125 @view_config(
130 @view_config(
126 route_name='pullrequest_show_all', request_method='GET',
131 route_name='pullrequest_show_all', request_method='GET',
127 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
132 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
128 def pull_request_list(self):
133 def pull_request_list(self):
129 c = self.load_default_context()
134 c = self.load_default_context()
130
135
131 req_get = self.request.GET
136 req_get = self.request.GET
132 c.source = str2bool(req_get.get('source'))
137 c.source = str2bool(req_get.get('source'))
133 c.closed = str2bool(req_get.get('closed'))
138 c.closed = str2bool(req_get.get('closed'))
134 c.my = str2bool(req_get.get('my'))
139 c.my = str2bool(req_get.get('my'))
135 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
140 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
136 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
141 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
137
142
138 c.active = 'open'
143 c.active = 'open'
139 if c.my:
144 if c.my:
140 c.active = 'my'
145 c.active = 'my'
141 if c.closed:
146 if c.closed:
142 c.active = 'closed'
147 c.active = 'closed'
143 if c.awaiting_review and not c.source:
148 if c.awaiting_review and not c.source:
144 c.active = 'awaiting'
149 c.active = 'awaiting'
145 if c.source and not c.awaiting_review:
150 if c.source and not c.awaiting_review:
146 c.active = 'source'
151 c.active = 'source'
147 if c.awaiting_my_review:
152 if c.awaiting_my_review:
148 c.active = 'awaiting_my'
153 c.active = 'awaiting_my'
149
154
150 return self._get_template_context(c)
155 return self._get_template_context(c)
151
156
152 @LoginRequired()
157 @LoginRequired()
153 @HasRepoPermissionAnyDecorator(
158 @HasRepoPermissionAnyDecorator(
154 'repository.read', 'repository.write', 'repository.admin')
159 'repository.read', 'repository.write', 'repository.admin')
155 @view_config(
160 @view_config(
156 route_name='pullrequest_show_all_data', request_method='GET',
161 route_name='pullrequest_show_all_data', request_method='GET',
157 renderer='json_ext', xhr=True)
162 renderer='json_ext', xhr=True)
158 def pull_request_list_data(self):
163 def pull_request_list_data(self):
159
164
160 # additional filters
165 # additional filters
161 req_get = self.request.GET
166 req_get = self.request.GET
162 source = str2bool(req_get.get('source'))
167 source = str2bool(req_get.get('source'))
163 closed = str2bool(req_get.get('closed'))
168 closed = str2bool(req_get.get('closed'))
164 my = str2bool(req_get.get('my'))
169 my = str2bool(req_get.get('my'))
165 awaiting_review = str2bool(req_get.get('awaiting_review'))
170 awaiting_review = str2bool(req_get.get('awaiting_review'))
166 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
171 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
167
172
168 filter_type = 'awaiting_review' if awaiting_review \
173 filter_type = 'awaiting_review' if awaiting_review \
169 else 'awaiting_my_review' if awaiting_my_review \
174 else 'awaiting_my_review' if awaiting_my_review \
170 else None
175 else None
171
176
172 opened_by = None
177 opened_by = None
173 if my:
178 if my:
174 opened_by = [self._rhodecode_user.user_id]
179 opened_by = [self._rhodecode_user.user_id]
175
180
176 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
177 if closed:
182 if closed:
178 statuses = [PullRequest.STATUS_CLOSED]
183 statuses = [PullRequest.STATUS_CLOSED]
179
184
180 data = self._get_pull_requests_list(
185 data = self._get_pull_requests_list(
181 repo_name=self.db_repo_name, source=source,
186 repo_name=self.db_repo_name, source=source,
182 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
187 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
183
188
184 return data
189 return data
190
191 def _get_pr_version(self, pull_request_id, version=None):
192 pull_request_id = safe_int(pull_request_id)
193 at_version = None
194
195 if version and version == 'latest':
196 pull_request_ver = PullRequest.get(pull_request_id)
197 pull_request_obj = pull_request_ver
198 _org_pull_request_obj = pull_request_obj
199 at_version = 'latest'
200 elif version:
201 pull_request_ver = PullRequestVersion.get_or_404(version)
202 pull_request_obj = pull_request_ver
203 _org_pull_request_obj = pull_request_ver.pull_request
204 at_version = pull_request_ver.pull_request_version_id
205 else:
206 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
207 pull_request_id)
208
209 pull_request_display_obj = PullRequest.get_pr_display_object(
210 pull_request_obj, _org_pull_request_obj)
211
212 return _org_pull_request_obj, pull_request_obj, \
213 pull_request_display_obj, at_version
214
215 def _get_diffset(self, source_repo_name, source_repo,
216 source_ref_id, target_ref_id,
217 target_commit, source_commit, diff_limit, fulldiff,
218 file_limit, display_inline_comments):
219
220 vcs_diff = PullRequestModel().get_diff(
221 source_repo, source_ref_id, target_ref_id)
222
223 diff_processor = diffs.DiffProcessor(
224 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 file_limit=file_limit, show_full_diff=fulldiff)
226
227 _parsed = diff_processor.prepare()
228
229 def _node_getter(commit):
230 def get_node(fname):
231 try:
232 return commit.get_node(fname)
233 except NodeDoesNotExistError:
234 return None
235
236 return get_node
237
238 diffset = codeblocks.DiffSet(
239 repo_name=self.db_repo_name,
240 source_repo_name=source_repo_name,
241 source_node_getter=_node_getter(target_commit),
242 target_node_getter=_node_getter(source_commit),
243 comments=display_inline_comments
244 )
245 diffset = diffset.render_patchset(
246 _parsed, target_commit.raw_id, source_commit.raw_id)
247
248 return diffset
249
250 @LoginRequired()
251 @HasRepoPermissionAnyDecorator(
252 'repository.read', 'repository.write', 'repository.admin')
253 # @view_config(
254 # route_name='pullrequest_show', request_method='GET',
255 # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
256 def pull_request_show(self):
257 pull_request_id = safe_int(
258 self.request.matchdict.get('pull_request_id'))
259 c = self.load_default_context()
260
261 version = self.request.GET.get('version')
262 from_version = self.request.GET.get('from_version') or version
263 merge_checks = self.request.GET.get('merge_checks')
264 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
265
266 (pull_request_latest,
267 pull_request_at_ver,
268 pull_request_display_obj,
269 at_version) = self._get_pr_version(
270 pull_request_id, version=version)
271 pr_closed = pull_request_latest.is_closed()
272
273 if pr_closed and (version or from_version):
274 # not allow to browse versions
275 raise HTTPFound(h.route_path(
276 'pullrequest_show', repo_name=self.db_repo_name,
277 pull_request_id=pull_request_id))
278
279 versions = pull_request_display_obj.versions()
280
281 c.at_version = at_version
282 c.at_version_num = (at_version
283 if at_version and at_version != 'latest'
284 else None)
285 c.at_version_pos = ChangesetComment.get_index_from_version(
286 c.at_version_num, versions)
287
288 (prev_pull_request_latest,
289 prev_pull_request_at_ver,
290 prev_pull_request_display_obj,
291 prev_at_version) = self._get_pr_version(
292 pull_request_id, version=from_version)
293
294 c.from_version = prev_at_version
295 c.from_version_num = (prev_at_version
296 if prev_at_version and prev_at_version != 'latest'
297 else None)
298 c.from_version_pos = ChangesetComment.get_index_from_version(
299 c.from_version_num, versions)
300
301 # define if we're in COMPARE mode or VIEW at version mode
302 compare = at_version != prev_at_version
303
304 # pull_requests repo_name we opened it against
305 # ie. target_repo must match
306 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
307 raise HTTPNotFound()
308
309 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
310 pull_request_at_ver)
311
312 c.pull_request = pull_request_display_obj
313 c.pull_request_latest = pull_request_latest
314
315 if compare or (at_version and not at_version == 'latest'):
316 c.allowed_to_change_status = False
317 c.allowed_to_update = False
318 c.allowed_to_merge = False
319 c.allowed_to_delete = False
320 c.allowed_to_comment = False
321 c.allowed_to_close = False
322 else:
323 can_change_status = PullRequestModel().check_user_change_status(
324 pull_request_at_ver, self._rhodecode_user)
325 c.allowed_to_change_status = can_change_status and not pr_closed
326
327 c.allowed_to_update = PullRequestModel().check_user_update(
328 pull_request_latest, self._rhodecode_user) and not pr_closed
329 c.allowed_to_merge = PullRequestModel().check_user_merge(
330 pull_request_latest, self._rhodecode_user) and not pr_closed
331 c.allowed_to_delete = PullRequestModel().check_user_delete(
332 pull_request_latest, self._rhodecode_user) and not pr_closed
333 c.allowed_to_comment = not pr_closed
334 c.allowed_to_close = c.allowed_to_merge and not pr_closed
335
336 c.forbid_adding_reviewers = False
337 c.forbid_author_to_review = False
338 c.forbid_commit_author_to_review = False
339
340 if pull_request_latest.reviewer_data and \
341 'rules' in pull_request_latest.reviewer_data:
342 rules = pull_request_latest.reviewer_data['rules'] or {}
343 try:
344 c.forbid_adding_reviewers = rules.get(
345 'forbid_adding_reviewers')
346 c.forbid_author_to_review = rules.get(
347 'forbid_author_to_review')
348 c.forbid_commit_author_to_review = rules.get(
349 'forbid_commit_author_to_review')
350 except Exception:
351 pass
352
353 # check merge capabilities
354 _merge_check = MergeCheck.validate(
355 pull_request_latest, user=self._rhodecode_user)
356 c.pr_merge_errors = _merge_check.error_details
357 c.pr_merge_possible = not _merge_check.failed
358 c.pr_merge_message = _merge_check.merge_msg
359
360 c.pull_request_review_status = _merge_check.review_status
361 if merge_checks:
362 self.request.override_renderer = \
363 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
364 return self._get_template_context(c)
365
366 comments_model = CommentsModel()
367
368 # reviewers and statuses
369 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
370 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
371
372 # GENERAL COMMENTS with versions #
373 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
374 q = q.order_by(ChangesetComment.comment_id.asc())
375 general_comments = q
376
377 # pick comments we want to render at current version
378 c.comment_versions = comments_model.aggregate_comments(
379 general_comments, versions, c.at_version_num)
380 c.comments = c.comment_versions[c.at_version_num]['until']
381
382 # INLINE COMMENTS with versions #
383 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
384 q = q.order_by(ChangesetComment.comment_id.asc())
385 inline_comments = q
386
387 c.inline_versions = comments_model.aggregate_comments(
388 inline_comments, versions, c.at_version_num, inline=True)
389
390 # inject latest version
391 latest_ver = PullRequest.get_pr_display_object(
392 pull_request_latest, pull_request_latest)
393
394 c.versions = versions + [latest_ver]
395
396 # if we use version, then do not show later comments
397 # than current version
398 display_inline_comments = collections.defaultdict(
399 lambda: collections.defaultdict(list))
400 for co in inline_comments:
401 if c.at_version_num:
402 # pick comments that are at least UPTO given version, so we
403 # don't render comments for higher version
404 should_render = co.pull_request_version_id and \
405 co.pull_request_version_id <= c.at_version_num
406 else:
407 # showing all, for 'latest'
408 should_render = True
409
410 if should_render:
411 display_inline_comments[co.f_path][co.line_no].append(co)
412
413 # load diff data into template context, if we use compare mode then
414 # diff is calculated based on changes between versions of PR
415
416 source_repo = pull_request_at_ver.source_repo
417 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
418
419 target_repo = pull_request_at_ver.target_repo
420 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
421
422 if compare:
423 # in compare switch the diff base to latest commit from prev version
424 target_ref_id = prev_pull_request_display_obj.revisions[0]
425
426 # despite opening commits for bookmarks/branches/tags, we always
427 # convert this to rev to prevent changes after bookmark or branch change
428 c.source_ref_type = 'rev'
429 c.source_ref = source_ref_id
430
431 c.target_ref_type = 'rev'
432 c.target_ref = target_ref_id
433
434 c.source_repo = source_repo
435 c.target_repo = target_repo
436
437 c.commit_ranges = []
438 source_commit = EmptyCommit()
439 target_commit = EmptyCommit()
440 c.missing_requirements = False
441
442 source_scm = source_repo.scm_instance()
443 target_scm = target_repo.scm_instance()
444
445 # try first shadow repo, fallback to regular repo
446 try:
447 commits_source_repo = pull_request_latest.get_shadow_repo()
448 except Exception:
449 log.debug('Failed to get shadow repo', exc_info=True)
450 commits_source_repo = source_scm
451
452 c.commits_source_repo = commits_source_repo
453 commit_cache = {}
454 try:
455 pre_load = ["author", "branch", "date", "message"]
456 show_revs = pull_request_at_ver.revisions
457 for rev in show_revs:
458 comm = commits_source_repo.get_commit(
459 commit_id=rev, pre_load=pre_load)
460 c.commit_ranges.append(comm)
461 commit_cache[comm.raw_id] = comm
462
463 # Order here matters, we first need to get target, and then
464 # the source
465 target_commit = commits_source_repo.get_commit(
466 commit_id=safe_str(target_ref_id))
467
468 source_commit = commits_source_repo.get_commit(
469 commit_id=safe_str(source_ref_id))
470
471 except CommitDoesNotExistError:
472 log.warning(
473 'Failed to get commit from `{}` repo'.format(
474 commits_source_repo), exc_info=True)
475 except RepositoryRequirementError:
476 log.warning(
477 'Failed to get all required data from repo', exc_info=True)
478 c.missing_requirements = True
479
480 c.ancestor = None # set it to None, to hide it from PR view
481
482 try:
483 ancestor_id = source_scm.get_common_ancestor(
484 source_commit.raw_id, target_commit.raw_id, target_scm)
485 c.ancestor_commit = source_scm.get_commit(ancestor_id)
486 except Exception:
487 c.ancestor_commit = None
488
489 c.statuses = source_repo.statuses(
490 [x.raw_id for x in c.commit_ranges])
491
492 # auto collapse if we have more than limit
493 collapse_limit = diffs.DiffProcessor._collapse_commits_over
494 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
495 c.compare_mode = compare
496
497 # diff_limit is the old behavior, will cut off the whole diff
498 # if the limit is applied otherwise will just hide the
499 # big files from the front-end
500 diff_limit = c.visual.cut_off_limit_diff
501 file_limit = c.visual.cut_off_limit_file
502
503 c.missing_commits = False
504 if (c.missing_requirements
505 or isinstance(source_commit, EmptyCommit)
506 or source_commit == target_commit):
507
508 c.missing_commits = True
509 else:
510
511 c.diffset = self._get_diffset(
512 c.source_repo.repo_name, commits_source_repo,
513 source_ref_id, target_ref_id,
514 target_commit, source_commit,
515 diff_limit, c.fulldiff, file_limit, display_inline_comments)
516
517 c.limited_diff = c.diffset.limited_diff
518
519 # calculate removed files that are bound to comments
520 comment_deleted_files = [
521 fname for fname in display_inline_comments
522 if fname not in c.diffset.file_stats]
523
524 c.deleted_files_comments = collections.defaultdict(dict)
525 for fname, per_line_comments in display_inline_comments.items():
526 if fname in comment_deleted_files:
527 c.deleted_files_comments[fname]['stats'] = 0
528 c.deleted_files_comments[fname]['comments'] = list()
529 for lno, comments in per_line_comments.items():
530 c.deleted_files_comments[fname]['comments'].extend(
531 comments)
532
533 # this is a hack to properly display links, when creating PR, the
534 # compare view and others uses different notation, and
535 # compare_commits.mako renders links based on the target_repo.
536 # We need to swap that here to generate it properly on the html side
537 c.target_repo = c.source_repo
538
539 c.commit_statuses = ChangesetStatus.STATUSES
540
541 c.show_version_changes = not pr_closed
542 if c.show_version_changes:
543 cur_obj = pull_request_at_ver
544 prev_obj = prev_pull_request_at_ver
545
546 old_commit_ids = prev_obj.revisions
547 new_commit_ids = cur_obj.revisions
548 commit_changes = PullRequestModel()._calculate_commit_id_changes(
549 old_commit_ids, new_commit_ids)
550 c.commit_changes_summary = commit_changes
551
552 # calculate the diff for commits between versions
553 c.commit_changes = []
554 mark = lambda cs, fw: list(
555 h.itertools.izip_longest([], cs, fillvalue=fw))
556 for c_type, raw_id in mark(commit_changes.added, 'a') \
557 + mark(commit_changes.removed, 'r') \
558 + mark(commit_changes.common, 'c'):
559
560 if raw_id in commit_cache:
561 commit = commit_cache[raw_id]
562 else:
563 try:
564 commit = commits_source_repo.get_commit(raw_id)
565 except CommitDoesNotExistError:
566 # in case we fail extracting still use "dummy" commit
567 # for display in commit diff
568 commit = h.AttributeDict(
569 {'raw_id': raw_id,
570 'message': 'EMPTY or MISSING COMMIT'})
571 c.commit_changes.append([c_type, commit])
572
573 # current user review statuses for each version
574 c.review_versions = {}
575 if self._rhodecode_user.user_id in allowed_reviewers:
576 for co in general_comments:
577 if co.author.user_id == self._rhodecode_user.user_id:
578 # each comment has a status change
579 status = co.status_change
580 if status:
581 _ver_pr = status[0].comment.pull_request_version_id
582 c.review_versions[_ver_pr] = status[0]
583
584 return self._get_template_context(c)
@@ -1,1009 +1,1012 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 @LoginRequired()
75 @LoginRequired()
76 @NotAnonymous()
76 @NotAnonymous()
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 'repository.admin')
78 'repository.admin')
79 @HasAcceptedRepoType('git', 'hg')
79 @HasAcceptedRepoType('git', 'hg')
80 def index(self):
80 def index(self):
81 source_repo = c.rhodecode_db_repo
81 source_repo = c.rhodecode_db_repo
82
82
83 try:
83 try:
84 source_repo.scm_instance().get_commit()
84 source_repo.scm_instance().get_commit()
85 except EmptyRepositoryError:
85 except EmptyRepositoryError:
86 h.flash(h.literal(_('There are no commits yet')),
86 h.flash(h.literal(_('There are no commits yet')),
87 category='warning')
87 category='warning')
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
89
89
90 commit_id = request.GET.get('commit')
90 commit_id = request.GET.get('commit')
91 branch_ref = request.GET.get('branch')
91 branch_ref = request.GET.get('branch')
92 bookmark_ref = request.GET.get('bookmark')
92 bookmark_ref = request.GET.get('bookmark')
93
93
94 try:
94 try:
95 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo_data = PullRequestModel().generate_repo_data(
96 source_repo, commit_id=commit_id,
96 source_repo, commit_id=commit_id,
97 branch=branch_ref, bookmark=bookmark_ref)
97 branch=branch_ref, bookmark=bookmark_ref)
98 except CommitDoesNotExistError as e:
98 except CommitDoesNotExistError as e:
99 log.exception(e)
99 log.exception(e)
100 h.flash(_('Commit does not exist'), 'error')
100 h.flash(_('Commit does not exist'), 'error')
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
102
102
103 default_target_repo = source_repo
103 default_target_repo = source_repo
104
104
105 if source_repo.parent:
105 if source_repo.parent:
106 parent_vcs_obj = source_repo.parent.scm_instance()
106 parent_vcs_obj = source_repo.parent.scm_instance()
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 if parent_vcs_obj and not parent_vcs_obj.is_empty():
108 # change default if we have a parent repo
108 # change default if we have a parent repo
109 default_target_repo = source_repo.parent
109 default_target_repo = source_repo.parent
110
110
111 target_repo_data = PullRequestModel().generate_repo_data(
111 target_repo_data = PullRequestModel().generate_repo_data(
112 default_target_repo)
112 default_target_repo)
113
113
114 selected_source_ref = source_repo_data['refs']['selected_ref']
114 selected_source_ref = source_repo_data['refs']['selected_ref']
115
115
116 title_source_ref = selected_source_ref.split(':', 2)[1]
116 title_source_ref = selected_source_ref.split(':', 2)[1]
117 c.default_title = PullRequestModel().generate_pullrequest_title(
117 c.default_title = PullRequestModel().generate_pullrequest_title(
118 source=source_repo.repo_name,
118 source=source_repo.repo_name,
119 source_ref=title_source_ref,
119 source_ref=title_source_ref,
120 target=default_target_repo.repo_name
120 target=default_target_repo.repo_name
121 )
121 )
122
122
123 c.default_repo_data = {
123 c.default_repo_data = {
124 'source_repo_name': source_repo.repo_name,
124 'source_repo_name': source_repo.repo_name,
125 'source_refs_json': json.dumps(source_repo_data),
125 'source_refs_json': json.dumps(source_repo_data),
126 'target_repo_name': default_target_repo.repo_name,
126 'target_repo_name': default_target_repo.repo_name,
127 'target_refs_json': json.dumps(target_repo_data),
127 'target_refs_json': json.dumps(target_repo_data),
128 }
128 }
129 c.default_source_ref = selected_source_ref
129 c.default_source_ref = selected_source_ref
130
130
131 return render('/pullrequests/pullrequest.mako')
131 return render('/pullrequests/pullrequest.mako')
132
132
133 @LoginRequired()
133 @LoginRequired()
134 @NotAnonymous()
134 @NotAnonymous()
135 @XHRRequired()
135 @XHRRequired()
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 'repository.admin')
137 'repository.admin')
138 @jsonify
138 @jsonify
139 def get_repo_refs(self, repo_name, target_repo_name):
139 def get_repo_refs(self, repo_name, target_repo_name):
140 repo = Repository.get_by_repo_name(target_repo_name)
140 repo = Repository.get_by_repo_name(target_repo_name)
141 if not repo:
141 if not repo:
142 raise HTTPNotFound
142 raise HTTPNotFound
143 return PullRequestModel().generate_repo_data(repo)
143 return PullRequestModel().generate_repo_data(repo)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @XHRRequired()
147 @XHRRequired()
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
149 'repository.admin')
149 'repository.admin')
150 @jsonify
150 @jsonify
151 def get_repo_destinations(self, repo_name):
151 def get_repo_destinations(self, repo_name):
152 repo = Repository.get_by_repo_name(repo_name)
152 repo = Repository.get_by_repo_name(repo_name)
153 if not repo:
153 if not repo:
154 raise HTTPNotFound
154 raise HTTPNotFound
155 filter_query = request.GET.get('query')
155 filter_query = request.GET.get('query')
156
156
157 query = Repository.query() \
157 query = Repository.query() \
158 .order_by(func.length(Repository.repo_name)) \
158 .order_by(func.length(Repository.repo_name)) \
159 .filter(or_(
159 .filter(or_(
160 Repository.repo_name == repo.repo_name,
160 Repository.repo_name == repo.repo_name,
161 Repository.fork_id == repo.repo_id))
161 Repository.fork_id == repo.repo_id))
162
162
163 if filter_query:
163 if filter_query:
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
165 query = query.filter(
165 query = query.filter(
166 Repository.repo_name.ilike(ilike_expression))
166 Repository.repo_name.ilike(ilike_expression))
167
167
168 add_parent = False
168 add_parent = False
169 if repo.parent:
169 if repo.parent:
170 if filter_query in repo.parent.repo_name:
170 if filter_query in repo.parent.repo_name:
171 parent_vcs_obj = repo.parent.scm_instance()
171 parent_vcs_obj = repo.parent.scm_instance()
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 if parent_vcs_obj and not parent_vcs_obj.is_empty():
173 add_parent = True
173 add_parent = True
174
174
175 limit = 20 - 1 if add_parent else 20
175 limit = 20 - 1 if add_parent else 20
176 all_repos = query.limit(limit).all()
176 all_repos = query.limit(limit).all()
177 if add_parent:
177 if add_parent:
178 all_repos += [repo.parent]
178 all_repos += [repo.parent]
179
179
180 repos = []
180 repos = []
181 for obj in self.scm_model.get_repos(all_repos):
181 for obj in self.scm_model.get_repos(all_repos):
182 repos.append({
182 repos.append({
183 'id': obj['name'],
183 'id': obj['name'],
184 'text': obj['name'],
184 'text': obj['name'],
185 'type': 'repo',
185 'type': 'repo',
186 'obj': obj['dbrepo']
186 'obj': obj['dbrepo']
187 })
187 })
188
188
189 data = {
189 data = {
190 'more': False,
190 'more': False,
191 'results': [{
191 'results': [{
192 'text': _('Repositories'),
192 'text': _('Repositories'),
193 'children': repos
193 'children': repos
194 }] if repos else []
194 }] if repos else []
195 }
195 }
196 return data
196 return data
197
197
198 @LoginRequired()
198 @LoginRequired()
199 @NotAnonymous()
199 @NotAnonymous()
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 'repository.admin')
201 'repository.admin')
202 @HasAcceptedRepoType('git', 'hg')
202 @HasAcceptedRepoType('git', 'hg')
203 @auth.CSRFRequired()
203 @auth.CSRFRequired()
204 def create(self, repo_name):
204 def create(self, repo_name):
205 repo = Repository.get_by_repo_name(repo_name)
205 repo = Repository.get_by_repo_name(repo_name)
206 if not repo:
206 if not repo:
207 raise HTTPNotFound
207 raise HTTPNotFound
208
208
209 controls = peppercorn.parse(request.POST.items())
209 controls = peppercorn.parse(request.POST.items())
210
210
211 try:
211 try:
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 _form = PullRequestForm(repo.repo_id)().to_python(controls)
213 except formencode.Invalid as errors:
213 except formencode.Invalid as errors:
214 if errors.error_dict.get('revisions'):
214 if errors.error_dict.get('revisions'):
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 msg = 'Revisions: %s' % errors.error_dict['revisions']
216 elif errors.error_dict.get('pullrequest_title'):
216 elif errors.error_dict.get('pullrequest_title'):
217 msg = _('Pull request requires a title with min. 3 chars')
217 msg = _('Pull request requires a title with min. 3 chars')
218 else:
218 else:
219 msg = _('Error creating pull request: {}').format(errors)
219 msg = _('Error creating pull request: {}').format(errors)
220 log.exception(msg)
220 log.exception(msg)
221 h.flash(msg, 'error')
221 h.flash(msg, 'error')
222
222
223 # would rather just go back to form ...
223 # would rather just go back to form ...
224 return redirect(url('pullrequest_home', repo_name=repo_name))
224 return redirect(url('pullrequest_home', repo_name=repo_name))
225
225
226 source_repo = _form['source_repo']
226 source_repo = _form['source_repo']
227 source_ref = _form['source_ref']
227 source_ref = _form['source_ref']
228 target_repo = _form['target_repo']
228 target_repo = _form['target_repo']
229 target_ref = _form['target_ref']
229 target_ref = _form['target_ref']
230 commit_ids = _form['revisions'][::-1]
230 commit_ids = _form['revisions'][::-1]
231
231
232 # find the ancestor for this pr
232 # find the ancestor for this pr
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
235
235
236 source_scm = source_db_repo.scm_instance()
236 source_scm = source_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
237 target_scm = target_db_repo.scm_instance()
238
238
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
241
241
242 ancestor = source_scm.get_common_ancestor(
242 ancestor = source_scm.get_common_ancestor(
243 source_commit.raw_id, target_commit.raw_id, target_scm)
243 source_commit.raw_id, target_commit.raw_id, target_scm)
244
244
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
247
247
248 pullrequest_title = _form['pullrequest_title']
248 pullrequest_title = _form['pullrequest_title']
249 title_source_ref = source_ref.split(':', 2)[1]
249 title_source_ref = source_ref.split(':', 2)[1]
250 if not pullrequest_title:
250 if not pullrequest_title:
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 pullrequest_title = PullRequestModel().generate_pullrequest_title(
252 source=source_repo,
252 source=source_repo,
253 source_ref=title_source_ref,
253 source_ref=title_source_ref,
254 target=target_repo
254 target=target_repo
255 )
255 )
256
256
257 description = _form['pullrequest_desc']
257 description = _form['pullrequest_desc']
258
258
259 get_default_reviewers_data, validate_default_reviewers = \
259 get_default_reviewers_data, validate_default_reviewers = \
260 PullRequestModel().get_reviewer_functions()
260 PullRequestModel().get_reviewer_functions()
261
261
262 # recalculate reviewers logic, to make sure we can validate this
262 # recalculate reviewers logic, to make sure we can validate this
263 reviewer_rules = get_default_reviewers_data(
263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user.get_instance(), source_db_repo,
264 c.rhodecode_user.get_instance(), source_db_repo,
265 source_commit, target_db_repo, target_commit)
265 source_commit, target_db_repo, target_commit)
266
266
267 given_reviewers = _form['review_members']
267 given_reviewers = _form['review_members']
268 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
268 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
269
269
270 try:
270 try:
271 pull_request = PullRequestModel().create(
271 pull_request = PullRequestModel().create(
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
273 target_ref, commit_ids, reviewers, pullrequest_title,
273 target_ref, commit_ids, reviewers, pullrequest_title,
274 description, reviewer_rules
274 description, reviewer_rules
275 )
275 )
276 Session().commit()
276 Session().commit()
277 h.flash(_('Successfully opened new pull request'),
277 h.flash(_('Successfully opened new pull request'),
278 category='success')
278 category='success')
279 except Exception as e:
279 except Exception as e:
280 msg = _('Error occurred during creation of this pull request.')
280 msg = _('Error occurred during creation of this pull request.')
281 log.exception(msg)
281 log.exception(msg)
282 h.flash(msg, category='error')
282 h.flash(msg, category='error')
283 return redirect(url('pullrequest_home', repo_name=repo_name))
283 return redirect(url('pullrequest_home', repo_name=repo_name))
284
284
285 return redirect(url('pullrequest_show', repo_name=target_repo,
285 raise HTTPFound(
286 pull_request_id=pull_request.pull_request_id))
286 h.route_path('pullrequest_show', repo_name=target_repo,
287 pull_request_id=pull_request.pull_request_id))
287
288
288 @LoginRequired()
289 @LoginRequired()
289 @NotAnonymous()
290 @NotAnonymous()
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 'repository.admin')
292 'repository.admin')
292 @auth.CSRFRequired()
293 @auth.CSRFRequired()
293 @jsonify
294 @jsonify
294 def update(self, repo_name, pull_request_id):
295 def update(self, repo_name, pull_request_id):
295 pull_request_id = safe_int(pull_request_id)
296 pull_request_id = safe_int(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
297 pull_request = PullRequest.get_or_404(pull_request_id)
297 # only owner or admin can update it
298 # only owner or admin can update it
298 allowed_to_update = PullRequestModel().check_user_update(
299 allowed_to_update = PullRequestModel().check_user_update(
299 pull_request, c.rhodecode_user)
300 pull_request, c.rhodecode_user)
300 if allowed_to_update:
301 if allowed_to_update:
301 controls = peppercorn.parse(request.POST.items())
302 controls = peppercorn.parse(request.POST.items())
302
303
303 if 'review_members' in controls:
304 if 'review_members' in controls:
304 self._update_reviewers(
305 self._update_reviewers(
305 pull_request_id, controls['review_members'],
306 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
307 pull_request.reviewer_data)
307 elif str2bool(request.POST.get('update_commits', 'false')):
308 elif str2bool(request.POST.get('update_commits', 'false')):
308 self._update_commits(pull_request)
309 self._update_commits(pull_request)
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 self._edit_pull_request(pull_request)
311 self._edit_pull_request(pull_request)
311 else:
312 else:
312 raise HTTPBadRequest()
313 raise HTTPBadRequest()
313 return True
314 return True
314 raise HTTPForbidden()
315 raise HTTPForbidden()
315
316
316 def _edit_pull_request(self, pull_request):
317 def _edit_pull_request(self, pull_request):
317 try:
318 try:
318 PullRequestModel().edit(
319 PullRequestModel().edit(
319 pull_request, request.POST.get('title'),
320 pull_request, request.POST.get('title'),
320 request.POST.get('description'), c.rhodecode_user)
321 request.POST.get('description'), c.rhodecode_user)
321 except ValueError:
322 except ValueError:
322 msg = _(u'Cannot update closed pull requests.')
323 msg = _(u'Cannot update closed pull requests.')
323 h.flash(msg, category='error')
324 h.flash(msg, category='error')
324 return
325 return
325 else:
326 else:
326 Session().commit()
327 Session().commit()
327
328
328 msg = _(u'Pull request title & description updated.')
329 msg = _(u'Pull request title & description updated.')
329 h.flash(msg, category='success')
330 h.flash(msg, category='success')
330 return
331 return
331
332
332 def _update_commits(self, pull_request):
333 def _update_commits(self, pull_request):
333 resp = PullRequestModel().update_commits(pull_request)
334 resp = PullRequestModel().update_commits(pull_request)
334
335
335 if resp.executed:
336 if resp.executed:
336
337
337 if resp.target_changed and resp.source_changed:
338 if resp.target_changed and resp.source_changed:
338 changed = 'target and source repositories'
339 changed = 'target and source repositories'
339 elif resp.target_changed and not resp.source_changed:
340 elif resp.target_changed and not resp.source_changed:
340 changed = 'target repository'
341 changed = 'target repository'
341 elif not resp.target_changed and resp.source_changed:
342 elif not resp.target_changed and resp.source_changed:
342 changed = 'source repository'
343 changed = 'source repository'
343 else:
344 else:
344 changed = 'nothing'
345 changed = 'nothing'
345
346
346 msg = _(
347 msg = _(
347 u'Pull request updated to "{source_commit_id}" with '
348 u'Pull request updated to "{source_commit_id}" with '
348 u'{count_added} added, {count_removed} removed commits. '
349 u'{count_added} added, {count_removed} removed commits. '
349 u'Source of changes: {change_source}')
350 u'Source of changes: {change_source}')
350 msg = msg.format(
351 msg = msg.format(
351 source_commit_id=pull_request.source_ref_parts.commit_id,
352 source_commit_id=pull_request.source_ref_parts.commit_id,
352 count_added=len(resp.changes.added),
353 count_added=len(resp.changes.added),
353 count_removed=len(resp.changes.removed),
354 count_removed=len(resp.changes.removed),
354 change_source=changed)
355 change_source=changed)
355 h.flash(msg, category='success')
356 h.flash(msg, category='success')
356
357
357 registry = get_current_registry()
358 registry = get_current_registry()
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 if channelstream_config.get('enabled'):
361 if channelstream_config.get('enabled'):
361 message = msg + (
362 message = msg + (
362 ' - <a onclick="window.location.reload()">'
363 ' - <a onclick="window.location.reload()">'
363 '<strong>{}</strong></a>'.format(_('Reload page')))
364 '<strong>{}</strong></a>'.format(_('Reload page')))
364 channel = '/repo${}$/pr/{}'.format(
365 channel = '/repo${}$/pr/{}'.format(
365 pull_request.target_repo.repo_name,
366 pull_request.target_repo.repo_name,
366 pull_request.pull_request_id
367 pull_request.pull_request_id
367 )
368 )
368 payload = {
369 payload = {
369 'type': 'message',
370 'type': 'message',
370 'user': 'system',
371 'user': 'system',
371 'exclude_users': [request.user.username],
372 'exclude_users': [request.user.username],
372 'channel': channel,
373 'channel': channel,
373 'message': {
374 'message': {
374 'message': message,
375 'message': message,
375 'level': 'success',
376 'level': 'success',
376 'topic': '/notifications'
377 'topic': '/notifications'
377 }
378 }
378 }
379 }
379 channelstream_request(
380 channelstream_request(
380 channelstream_config, [payload], '/message',
381 channelstream_config, [payload], '/message',
381 raise_exc=False)
382 raise_exc=False)
382 else:
383 else:
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 warning_reasons = [
385 warning_reasons = [
385 UpdateFailureReason.NO_CHANGE,
386 UpdateFailureReason.NO_CHANGE,
386 UpdateFailureReason.WRONG_REF_TYPE,
387 UpdateFailureReason.WRONG_REF_TYPE,
387 ]
388 ]
388 category = 'warning' if resp.reason in warning_reasons else 'error'
389 category = 'warning' if resp.reason in warning_reasons else 'error'
389 h.flash(msg, category=category)
390 h.flash(msg, category=category)
390
391
391 @auth.CSRFRequired()
392 @auth.CSRFRequired()
392 @LoginRequired()
393 @LoginRequired()
393 @NotAnonymous()
394 @NotAnonymous()
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 'repository.admin')
396 'repository.admin')
396 def merge(self, repo_name, pull_request_id):
397 def merge(self, repo_name, pull_request_id):
397 """
398 """
398 POST /{repo_name}/pull-request/{pull_request_id}
399 POST /{repo_name}/pull-request/{pull_request_id}
399
400
400 Merge will perform a server-side merge of the specified
401 Merge will perform a server-side merge of the specified
401 pull request, if the pull request is approved and mergeable.
402 pull request, if the pull request is approved and mergeable.
402 After successful merging, the pull request is automatically
403 After successful merging, the pull request is automatically
403 closed, with a relevant comment.
404 closed, with a relevant comment.
404 """
405 """
405 pull_request_id = safe_int(pull_request_id)
406 pull_request_id = safe_int(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
407 pull_request = PullRequest.get_or_404(pull_request_id)
407 user = c.rhodecode_user
408 user = c.rhodecode_user
408
409
409 check = MergeCheck.validate(pull_request, user)
410 check = MergeCheck.validate(pull_request, user)
410 merge_possible = not check.failed
411 merge_possible = not check.failed
411
412
412 for err_type, error_msg in check.errors:
413 for err_type, error_msg in check.errors:
413 h.flash(error_msg, category=err_type)
414 h.flash(error_msg, category=err_type)
414
415
415 if merge_possible:
416 if merge_possible:
416 log.debug("Pre-conditions checked, trying to merge.")
417 log.debug("Pre-conditions checked, trying to merge.")
417 extras = vcs_operation_context(
418 extras = vcs_operation_context(
418 request.environ, repo_name=pull_request.target_repo.repo_name,
419 request.environ, repo_name=pull_request.target_repo.repo_name,
419 username=user.username, action='push',
420 username=user.username, action='push',
420 scm=pull_request.target_repo.repo_type)
421 scm=pull_request.target_repo.repo_type)
421 self._merge_pull_request(pull_request, user, extras)
422 self._merge_pull_request(pull_request, user, extras)
422
423
423 return redirect(url(
424 raise HTTPFound(
424 'pullrequest_show',
425 h.route_path('pullrequest_show',
425 repo_name=pull_request.target_repo.repo_name,
426 repo_name=pull_request.target_repo.repo_name,
426 pull_request_id=pull_request.pull_request_id))
427 pull_request_id=pull_request.pull_request_id))
427
428
428 def _merge_pull_request(self, pull_request, user, extras):
429 def _merge_pull_request(self, pull_request, user, extras):
429 merge_resp = PullRequestModel().merge(
430 merge_resp = PullRequestModel().merge(
430 pull_request, user, extras=extras)
431 pull_request, user, extras=extras)
431
432
432 if merge_resp.executed:
433 if merge_resp.executed:
433 log.debug("The merge was successful, closing the pull request.")
434 log.debug("The merge was successful, closing the pull request.")
434 PullRequestModel().close_pull_request(
435 PullRequestModel().close_pull_request(
435 pull_request.pull_request_id, user)
436 pull_request.pull_request_id, user)
436 Session().commit()
437 Session().commit()
437 msg = _('Pull request was successfully merged and closed.')
438 msg = _('Pull request was successfully merged and closed.')
438 h.flash(msg, category='success')
439 h.flash(msg, category='success')
439 else:
440 else:
440 log.debug(
441 log.debug(
441 "The merge was not successful. Merge response: %s",
442 "The merge was not successful. Merge response: %s",
442 merge_resp)
443 merge_resp)
443 msg = PullRequestModel().merge_status_message(
444 msg = PullRequestModel().merge_status_message(
444 merge_resp.failure_reason)
445 merge_resp.failure_reason)
445 h.flash(msg, category='error')
446 h.flash(msg, category='error')
446
447
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448
449
449 get_default_reviewers_data, validate_default_reviewers = \
450 get_default_reviewers_data, validate_default_reviewers = \
450 PullRequestModel().get_reviewer_functions()
451 PullRequestModel().get_reviewer_functions()
451
452
452 try:
453 try:
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 except ValueError as e:
455 except ValueError as e:
455 log.error('Reviewers Validation: {}'.format(e))
456 log.error('Reviewers Validation: {}'.format(e))
456 h.flash(e, category='error')
457 h.flash(e, category='error')
457 return
458 return
458
459
459 PullRequestModel().update_reviewers(
460 PullRequestModel().update_reviewers(
460 pull_request_id, reviewers, c.rhodecode_user)
461 pull_request_id, reviewers, c.rhodecode_user)
461 h.flash(_('Pull request reviewers updated.'), category='success')
462 h.flash(_('Pull request reviewers updated.'), category='success')
462 Session().commit()
463 Session().commit()
463
464
464 @LoginRequired()
465 @LoginRequired()
465 @NotAnonymous()
466 @NotAnonymous()
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 'repository.admin')
468 'repository.admin')
468 @auth.CSRFRequired()
469 @auth.CSRFRequired()
469 @jsonify
470 @jsonify
470 def delete(self, repo_name, pull_request_id):
471 def delete(self, repo_name, pull_request_id):
471 pull_request_id = safe_int(pull_request_id)
472 pull_request_id = safe_int(pull_request_id)
472 pull_request = PullRequest.get_or_404(pull_request_id)
473 pull_request = PullRequest.get_or_404(pull_request_id)
473
474
474 pr_closed = pull_request.is_closed()
475 pr_closed = pull_request.is_closed()
475 allowed_to_delete = PullRequestModel().check_user_delete(
476 allowed_to_delete = PullRequestModel().check_user_delete(
476 pull_request, c.rhodecode_user) and not pr_closed
477 pull_request, c.rhodecode_user) and not pr_closed
477
478
478 # only owner can delete it !
479 # only owner can delete it !
479 if allowed_to_delete:
480 if allowed_to_delete:
480 PullRequestModel().delete(pull_request, c.rhodecode_user)
481 PullRequestModel().delete(pull_request, c.rhodecode_user)
481 Session().commit()
482 Session().commit()
482 h.flash(_('Successfully deleted pull request'),
483 h.flash(_('Successfully deleted pull request'),
483 category='success')
484 category='success')
484 return redirect(url('my_account_pullrequests'))
485 return redirect(url('my_account_pullrequests'))
485
486
486 h.flash(_('Your are not allowed to delete this pull request'),
487 h.flash(_('Your are not allowed to delete this pull request'),
487 category='error')
488 category='error')
488 raise HTTPForbidden()
489 raise HTTPForbidden()
489
490
490 def _get_pr_version(self, pull_request_id, version=None):
491 def _get_pr_version(self, pull_request_id, version=None):
491 pull_request_id = safe_int(pull_request_id)
492 pull_request_id = safe_int(pull_request_id)
492 at_version = None
493 at_version = None
493
494
494 if version and version == 'latest':
495 if version and version == 'latest':
495 pull_request_ver = PullRequest.get(pull_request_id)
496 pull_request_ver = PullRequest.get(pull_request_id)
496 pull_request_obj = pull_request_ver
497 pull_request_obj = pull_request_ver
497 _org_pull_request_obj = pull_request_obj
498 _org_pull_request_obj = pull_request_obj
498 at_version = 'latest'
499 at_version = 'latest'
499 elif version:
500 elif version:
500 pull_request_ver = PullRequestVersion.get_or_404(version)
501 pull_request_ver = PullRequestVersion.get_or_404(version)
501 pull_request_obj = pull_request_ver
502 pull_request_obj = pull_request_ver
502 _org_pull_request_obj = pull_request_ver.pull_request
503 _org_pull_request_obj = pull_request_ver.pull_request
503 at_version = pull_request_ver.pull_request_version_id
504 at_version = pull_request_ver.pull_request_version_id
504 else:
505 else:
505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
506 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
506 pull_request_id)
507 pull_request_id)
507
508
508 pull_request_display_obj = PullRequest.get_pr_display_object(
509 pull_request_display_obj = PullRequest.get_pr_display_object(
509 pull_request_obj, _org_pull_request_obj)
510 pull_request_obj, _org_pull_request_obj)
510
511
511 return _org_pull_request_obj, pull_request_obj, \
512 return _org_pull_request_obj, pull_request_obj, \
512 pull_request_display_obj, at_version
513 pull_request_display_obj, at_version
513
514
514 def _get_diffset(
515 def _get_diffset(
515 self, source_repo, source_ref_id, target_ref_id, target_commit,
516 self, source_repo, source_ref_id, target_ref_id, target_commit,
516 source_commit, diff_limit, file_limit, display_inline_comments):
517 source_commit, diff_limit, file_limit, display_inline_comments):
517 vcs_diff = PullRequestModel().get_diff(
518 vcs_diff = PullRequestModel().get_diff(
518 source_repo, source_ref_id, target_ref_id)
519 source_repo, source_ref_id, target_ref_id)
519
520
520 diff_processor = diffs.DiffProcessor(
521 diff_processor = diffs.DiffProcessor(
521 vcs_diff, format='newdiff', diff_limit=diff_limit,
522 vcs_diff, format='newdiff', diff_limit=diff_limit,
522 file_limit=file_limit, show_full_diff=c.fulldiff)
523 file_limit=file_limit, show_full_diff=c.fulldiff)
523
524
524 _parsed = diff_processor.prepare()
525 _parsed = diff_processor.prepare()
525
526
526 def _node_getter(commit):
527 def _node_getter(commit):
527 def get_node(fname):
528 def get_node(fname):
528 try:
529 try:
529 return commit.get_node(fname)
530 return commit.get_node(fname)
530 except NodeDoesNotExistError:
531 except NodeDoesNotExistError:
531 return None
532 return None
532
533
533 return get_node
534 return get_node
534
535
535 diffset = codeblocks.DiffSet(
536 diffset = codeblocks.DiffSet(
536 repo_name=c.repo_name,
537 repo_name=c.repo_name,
537 source_repo_name=c.source_repo.repo_name,
538 source_repo_name=c.source_repo.repo_name,
538 source_node_getter=_node_getter(target_commit),
539 source_node_getter=_node_getter(target_commit),
539 target_node_getter=_node_getter(source_commit),
540 target_node_getter=_node_getter(source_commit),
540 comments=display_inline_comments
541 comments=display_inline_comments
541 )
542 )
542 diffset = diffset.render_patchset(
543 diffset = diffset.render_patchset(
543 _parsed, target_commit.raw_id, source_commit.raw_id)
544 _parsed, target_commit.raw_id, source_commit.raw_id)
544
545
545 return diffset
546 return diffset
546
547
547 @LoginRequired()
548 @LoginRequired()
548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 'repository.admin')
550 'repository.admin')
550 def show(self, repo_name, pull_request_id):
551 def show(self, repo_name, pull_request_id):
551 pull_request_id = safe_int(pull_request_id)
552 pull_request_id = safe_int(pull_request_id)
552 version = request.GET.get('version')
553 version = request.GET.get('version')
553 from_version = request.GET.get('from_version') or version
554 from_version = request.GET.get('from_version') or version
554 merge_checks = request.GET.get('merge_checks')
555 merge_checks = request.GET.get('merge_checks')
555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
556 c.fulldiff = str2bool(request.GET.get('fulldiff'))
556
557
557 (pull_request_latest,
558 (pull_request_latest,
558 pull_request_at_ver,
559 pull_request_at_ver,
559 pull_request_display_obj,
560 pull_request_display_obj,
560 at_version) = self._get_pr_version(
561 at_version) = self._get_pr_version(
561 pull_request_id, version=version)
562 pull_request_id, version=version)
562 pr_closed = pull_request_latest.is_closed()
563 pr_closed = pull_request_latest.is_closed()
563
564
564 if pr_closed and (version or from_version):
565 if pr_closed and (version or from_version):
565 # not allow to browse versions
566 # not allow to browse versions
566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
567 return redirect(h.url('pullrequest_show', repo_name=repo_name,
567 pull_request_id=pull_request_id))
568 pull_request_id=pull_request_id))
568
569
569 versions = pull_request_display_obj.versions()
570 versions = pull_request_display_obj.versions()
570
571
571 c.at_version = at_version
572 c.at_version = at_version
572 c.at_version_num = (at_version
573 c.at_version_num = (at_version
573 if at_version and at_version != 'latest'
574 if at_version and at_version != 'latest'
574 else None)
575 else None)
575 c.at_version_pos = ChangesetComment.get_index_from_version(
576 c.at_version_pos = ChangesetComment.get_index_from_version(
576 c.at_version_num, versions)
577 c.at_version_num, versions)
577
578
578 (prev_pull_request_latest,
579 (prev_pull_request_latest,
579 prev_pull_request_at_ver,
580 prev_pull_request_at_ver,
580 prev_pull_request_display_obj,
581 prev_pull_request_display_obj,
581 prev_at_version) = self._get_pr_version(
582 prev_at_version) = self._get_pr_version(
582 pull_request_id, version=from_version)
583 pull_request_id, version=from_version)
583
584
584 c.from_version = prev_at_version
585 c.from_version = prev_at_version
585 c.from_version_num = (prev_at_version
586 c.from_version_num = (prev_at_version
586 if prev_at_version and prev_at_version != 'latest'
587 if prev_at_version and prev_at_version != 'latest'
587 else None)
588 else None)
588 c.from_version_pos = ChangesetComment.get_index_from_version(
589 c.from_version_pos = ChangesetComment.get_index_from_version(
589 c.from_version_num, versions)
590 c.from_version_num, versions)
590
591
591 # define if we're in COMPARE mode or VIEW at version mode
592 # define if we're in COMPARE mode or VIEW at version mode
592 compare = at_version != prev_at_version
593 compare = at_version != prev_at_version
593
594
594 # pull_requests repo_name we opened it against
595 # pull_requests repo_name we opened it against
595 # ie. target_repo must match
596 # ie. target_repo must match
596 if repo_name != pull_request_at_ver.target_repo.repo_name:
597 if repo_name != pull_request_at_ver.target_repo.repo_name:
597 raise HTTPNotFound
598 raise HTTPNotFound
598
599
599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
600 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
600 pull_request_at_ver)
601 pull_request_at_ver)
601
602
602 c.pull_request = pull_request_display_obj
603 c.pull_request = pull_request_display_obj
603 c.pull_request_latest = pull_request_latest
604 c.pull_request_latest = pull_request_latest
604
605
605 if compare or (at_version and not at_version == 'latest'):
606 if compare or (at_version and not at_version == 'latest'):
606 c.allowed_to_change_status = False
607 c.allowed_to_change_status = False
607 c.allowed_to_update = False
608 c.allowed_to_update = False
608 c.allowed_to_merge = False
609 c.allowed_to_merge = False
609 c.allowed_to_delete = False
610 c.allowed_to_delete = False
610 c.allowed_to_comment = False
611 c.allowed_to_comment = False
611 c.allowed_to_close = False
612 c.allowed_to_close = False
612 else:
613 else:
613 can_change_status = PullRequestModel().check_user_change_status(
614 can_change_status = PullRequestModel().check_user_change_status(
614 pull_request_at_ver, c.rhodecode_user)
615 pull_request_at_ver, c.rhodecode_user)
615 c.allowed_to_change_status = can_change_status and not pr_closed
616 c.allowed_to_change_status = can_change_status and not pr_closed
616
617
617 c.allowed_to_update = PullRequestModel().check_user_update(
618 c.allowed_to_update = PullRequestModel().check_user_update(
618 pull_request_latest, c.rhodecode_user) and not pr_closed
619 pull_request_latest, c.rhodecode_user) and not pr_closed
619 c.allowed_to_merge = PullRequestModel().check_user_merge(
620 c.allowed_to_merge = PullRequestModel().check_user_merge(
620 pull_request_latest, c.rhodecode_user) and not pr_closed
621 pull_request_latest, c.rhodecode_user) and not pr_closed
621 c.allowed_to_delete = PullRequestModel().check_user_delete(
622 c.allowed_to_delete = PullRequestModel().check_user_delete(
622 pull_request_latest, c.rhodecode_user) and not pr_closed
623 pull_request_latest, c.rhodecode_user) and not pr_closed
623 c.allowed_to_comment = not pr_closed
624 c.allowed_to_comment = not pr_closed
624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
625 c.allowed_to_close = c.allowed_to_merge and not pr_closed
625
626
626 c.forbid_adding_reviewers = False
627 c.forbid_adding_reviewers = False
627 c.forbid_author_to_review = False
628 c.forbid_author_to_review = False
628 c.forbid_commit_author_to_review = False
629 c.forbid_commit_author_to_review = False
629
630
630 if pull_request_latest.reviewer_data and \
631 if pull_request_latest.reviewer_data and \
631 'rules' in pull_request_latest.reviewer_data:
632 'rules' in pull_request_latest.reviewer_data:
632 rules = pull_request_latest.reviewer_data['rules'] or {}
633 rules = pull_request_latest.reviewer_data['rules'] or {}
633 try:
634 try:
634 c.forbid_adding_reviewers = rules.get(
635 c.forbid_adding_reviewers = rules.get(
635 'forbid_adding_reviewers')
636 'forbid_adding_reviewers')
636 c.forbid_author_to_review = rules.get(
637 c.forbid_author_to_review = rules.get(
637 'forbid_author_to_review')
638 'forbid_author_to_review')
638 c.forbid_commit_author_to_review = rules.get(
639 c.forbid_commit_author_to_review = rules.get(
639 'forbid_commit_author_to_review')
640 'forbid_commit_author_to_review')
640 except Exception:
641 except Exception:
641 pass
642 pass
642
643
643 # check merge capabilities
644 # check merge capabilities
644 _merge_check = MergeCheck.validate(
645 _merge_check = MergeCheck.validate(
645 pull_request_latest, user=c.rhodecode_user)
646 pull_request_latest, user=c.rhodecode_user)
646 c.pr_merge_errors = _merge_check.error_details
647 c.pr_merge_errors = _merge_check.error_details
647 c.pr_merge_possible = not _merge_check.failed
648 c.pr_merge_possible = not _merge_check.failed
648 c.pr_merge_message = _merge_check.merge_msg
649 c.pr_merge_message = _merge_check.merge_msg
649
650
650 c.pull_request_review_status = _merge_check.review_status
651 c.pull_request_review_status = _merge_check.review_status
651 if merge_checks:
652 if merge_checks:
652 return render('/pullrequests/pullrequest_merge_checks.mako')
653 return render('/pullrequests/pullrequest_merge_checks.mako')
653
654
654 comments_model = CommentsModel()
655 comments_model = CommentsModel()
655
656
656 # reviewers and statuses
657 # reviewers and statuses
657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
658 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
659 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
659
660
660 # GENERAL COMMENTS with versions #
661 # GENERAL COMMENTS with versions #
661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
662 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
662 q = q.order_by(ChangesetComment.comment_id.asc())
663 q = q.order_by(ChangesetComment.comment_id.asc())
663 general_comments = q
664 general_comments = q
664
665
665 # pick comments we want to render at current version
666 # pick comments we want to render at current version
666 c.comment_versions = comments_model.aggregate_comments(
667 c.comment_versions = comments_model.aggregate_comments(
667 general_comments, versions, c.at_version_num)
668 general_comments, versions, c.at_version_num)
668 c.comments = c.comment_versions[c.at_version_num]['until']
669 c.comments = c.comment_versions[c.at_version_num]['until']
669
670
670 # INLINE COMMENTS with versions #
671 # INLINE COMMENTS with versions #
671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
672 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
672 q = q.order_by(ChangesetComment.comment_id.asc())
673 q = q.order_by(ChangesetComment.comment_id.asc())
673 inline_comments = q
674 inline_comments = q
674
675
675 c.inline_versions = comments_model.aggregate_comments(
676 c.inline_versions = comments_model.aggregate_comments(
676 inline_comments, versions, c.at_version_num, inline=True)
677 inline_comments, versions, c.at_version_num, inline=True)
677
678
678 # inject latest version
679 # inject latest version
679 latest_ver = PullRequest.get_pr_display_object(
680 latest_ver = PullRequest.get_pr_display_object(
680 pull_request_latest, pull_request_latest)
681 pull_request_latest, pull_request_latest)
681
682
682 c.versions = versions + [latest_ver]
683 c.versions = versions + [latest_ver]
683
684
684 # if we use version, then do not show later comments
685 # if we use version, then do not show later comments
685 # than current version
686 # than current version
686 display_inline_comments = collections.defaultdict(
687 display_inline_comments = collections.defaultdict(
687 lambda: collections.defaultdict(list))
688 lambda: collections.defaultdict(list))
688 for co in inline_comments:
689 for co in inline_comments:
689 if c.at_version_num:
690 if c.at_version_num:
690 # pick comments that are at least UPTO given version, so we
691 # pick comments that are at least UPTO given version, so we
691 # don't render comments for higher version
692 # don't render comments for higher version
692 should_render = co.pull_request_version_id and \
693 should_render = co.pull_request_version_id and \
693 co.pull_request_version_id <= c.at_version_num
694 co.pull_request_version_id <= c.at_version_num
694 else:
695 else:
695 # showing all, for 'latest'
696 # showing all, for 'latest'
696 should_render = True
697 should_render = True
697
698
698 if should_render:
699 if should_render:
699 display_inline_comments[co.f_path][co.line_no].append(co)
700 display_inline_comments[co.f_path][co.line_no].append(co)
700
701
701 # load diff data into template context, if we use compare mode then
702 # load diff data into template context, if we use compare mode then
702 # diff is calculated based on changes between versions of PR
703 # diff is calculated based on changes between versions of PR
703
704
704 source_repo = pull_request_at_ver.source_repo
705 source_repo = pull_request_at_ver.source_repo
705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
706 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
706
707
707 target_repo = pull_request_at_ver.target_repo
708 target_repo = pull_request_at_ver.target_repo
708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
709 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
709
710
710 if compare:
711 if compare:
711 # in compare switch the diff base to latest commit from prev version
712 # in compare switch the diff base to latest commit from prev version
712 target_ref_id = prev_pull_request_display_obj.revisions[0]
713 target_ref_id = prev_pull_request_display_obj.revisions[0]
713
714
714 # despite opening commits for bookmarks/branches/tags, we always
715 # despite opening commits for bookmarks/branches/tags, we always
715 # convert this to rev to prevent changes after bookmark or branch change
716 # convert this to rev to prevent changes after bookmark or branch change
716 c.source_ref_type = 'rev'
717 c.source_ref_type = 'rev'
717 c.source_ref = source_ref_id
718 c.source_ref = source_ref_id
718
719
719 c.target_ref_type = 'rev'
720 c.target_ref_type = 'rev'
720 c.target_ref = target_ref_id
721 c.target_ref = target_ref_id
721
722
722 c.source_repo = source_repo
723 c.source_repo = source_repo
723 c.target_repo = target_repo
724 c.target_repo = target_repo
724
725
725 # diff_limit is the old behavior, will cut off the whole diff
726 # diff_limit is the old behavior, will cut off the whole diff
726 # if the limit is applied otherwise will just hide the
727 # if the limit is applied otherwise will just hide the
727 # big files from the front-end
728 # big files from the front-end
728 diff_limit = self.cut_off_limit_diff
729 diff_limit = self.cut_off_limit_diff
729 file_limit = self.cut_off_limit_file
730 file_limit = self.cut_off_limit_file
730
731
731 c.commit_ranges = []
732 c.commit_ranges = []
732 source_commit = EmptyCommit()
733 source_commit = EmptyCommit()
733 target_commit = EmptyCommit()
734 target_commit = EmptyCommit()
734 c.missing_requirements = False
735 c.missing_requirements = False
735
736
736 source_scm = source_repo.scm_instance()
737 source_scm = source_repo.scm_instance()
737 target_scm = target_repo.scm_instance()
738 target_scm = target_repo.scm_instance()
738
739
739 # try first shadow repo, fallback to regular repo
740 # try first shadow repo, fallback to regular repo
740 try:
741 try:
741 commits_source_repo = pull_request_latest.get_shadow_repo()
742 commits_source_repo = pull_request_latest.get_shadow_repo()
742 except Exception:
743 except Exception:
743 log.debug('Failed to get shadow repo', exc_info=True)
744 log.debug('Failed to get shadow repo', exc_info=True)
744 commits_source_repo = source_scm
745 commits_source_repo = source_scm
745
746
746 c.commits_source_repo = commits_source_repo
747 c.commits_source_repo = commits_source_repo
747 commit_cache = {}
748 commit_cache = {}
748 try:
749 try:
749 pre_load = ["author", "branch", "date", "message"]
750 pre_load = ["author", "branch", "date", "message"]
750 show_revs = pull_request_at_ver.revisions
751 show_revs = pull_request_at_ver.revisions
751 for rev in show_revs:
752 for rev in show_revs:
752 comm = commits_source_repo.get_commit(
753 comm = commits_source_repo.get_commit(
753 commit_id=rev, pre_load=pre_load)
754 commit_id=rev, pre_load=pre_load)
754 c.commit_ranges.append(comm)
755 c.commit_ranges.append(comm)
755 commit_cache[comm.raw_id] = comm
756 commit_cache[comm.raw_id] = comm
756
757
757 # Order here matters, we first need to get target, and then
758 # Order here matters, we first need to get target, and then
758 # the source
759 # the source
759 target_commit = commits_source_repo.get_commit(
760 target_commit = commits_source_repo.get_commit(
760 commit_id=safe_str(target_ref_id))
761 commit_id=safe_str(target_ref_id))
761
762
762 source_commit = commits_source_repo.get_commit(
763 source_commit = commits_source_repo.get_commit(
763 commit_id=safe_str(source_ref_id))
764 commit_id=safe_str(source_ref_id))
764
765
765 except CommitDoesNotExistError:
766 except CommitDoesNotExistError:
766 log.warning(
767 log.warning(
767 'Failed to get commit from `{}` repo'.format(
768 'Failed to get commit from `{}` repo'.format(
768 commits_source_repo), exc_info=True)
769 commits_source_repo), exc_info=True)
769 except RepositoryRequirementError:
770 except RepositoryRequirementError:
770 log.warning(
771 log.warning(
771 'Failed to get all required data from repo', exc_info=True)
772 'Failed to get all required data from repo', exc_info=True)
772 c.missing_requirements = True
773 c.missing_requirements = True
773
774
774 c.ancestor = None # set it to None, to hide it from PR view
775 c.ancestor = None # set it to None, to hide it from PR view
775
776
776 try:
777 try:
777 ancestor_id = source_scm.get_common_ancestor(
778 ancestor_id = source_scm.get_common_ancestor(
778 source_commit.raw_id, target_commit.raw_id, target_scm)
779 source_commit.raw_id, target_commit.raw_id, target_scm)
779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
780 c.ancestor_commit = source_scm.get_commit(ancestor_id)
780 except Exception:
781 except Exception:
781 c.ancestor_commit = None
782 c.ancestor_commit = None
782
783
783 c.statuses = source_repo.statuses(
784 c.statuses = source_repo.statuses(
784 [x.raw_id for x in c.commit_ranges])
785 [x.raw_id for x in c.commit_ranges])
785
786
786 # auto collapse if we have more than limit
787 # auto collapse if we have more than limit
787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
788 collapse_limit = diffs.DiffProcessor._collapse_commits_over
788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
789 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
789 c.compare_mode = compare
790 c.compare_mode = compare
790
791
791 c.missing_commits = False
792 c.missing_commits = False
792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
793 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
793 or source_commit == target_commit):
794 or source_commit == target_commit):
794
795
795 c.missing_commits = True
796 c.missing_commits = True
796 else:
797 else:
797
798
798 c.diffset = self._get_diffset(
799 c.diffset = self._get_diffset(
799 commits_source_repo, source_ref_id, target_ref_id,
800 commits_source_repo, source_ref_id, target_ref_id,
800 target_commit, source_commit,
801 target_commit, source_commit,
801 diff_limit, file_limit, display_inline_comments)
802 diff_limit, file_limit, display_inline_comments)
802
803
803 c.limited_diff = c.diffset.limited_diff
804 c.limited_diff = c.diffset.limited_diff
804
805
805 # calculate removed files that are bound to comments
806 # calculate removed files that are bound to comments
806 comment_deleted_files = [
807 comment_deleted_files = [
807 fname for fname in display_inline_comments
808 fname for fname in display_inline_comments
808 if fname not in c.diffset.file_stats]
809 if fname not in c.diffset.file_stats]
809
810
810 c.deleted_files_comments = collections.defaultdict(dict)
811 c.deleted_files_comments = collections.defaultdict(dict)
811 for fname, per_line_comments in display_inline_comments.items():
812 for fname, per_line_comments in display_inline_comments.items():
812 if fname in comment_deleted_files:
813 if fname in comment_deleted_files:
813 c.deleted_files_comments[fname]['stats'] = 0
814 c.deleted_files_comments[fname]['stats'] = 0
814 c.deleted_files_comments[fname]['comments'] = list()
815 c.deleted_files_comments[fname]['comments'] = list()
815 for lno, comments in per_line_comments.items():
816 for lno, comments in per_line_comments.items():
816 c.deleted_files_comments[fname]['comments'].extend(
817 c.deleted_files_comments[fname]['comments'].extend(
817 comments)
818 comments)
818
819
819 # this is a hack to properly display links, when creating PR, the
820 # this is a hack to properly display links, when creating PR, the
820 # compare view and others uses different notation, and
821 # compare view and others uses different notation, and
821 # compare_commits.mako renders links based on the target_repo.
822 # compare_commits.mako renders links based on the target_repo.
822 # We need to swap that here to generate it properly on the html side
823 # We need to swap that here to generate it properly on the html side
823 c.target_repo = c.source_repo
824 c.target_repo = c.source_repo
824
825
825 c.commit_statuses = ChangesetStatus.STATUSES
826 c.commit_statuses = ChangesetStatus.STATUSES
826
827
827 c.show_version_changes = not pr_closed
828 c.show_version_changes = not pr_closed
828 if c.show_version_changes:
829 if c.show_version_changes:
829 cur_obj = pull_request_at_ver
830 cur_obj = pull_request_at_ver
830 prev_obj = prev_pull_request_at_ver
831 prev_obj = prev_pull_request_at_ver
831
832
832 old_commit_ids = prev_obj.revisions
833 old_commit_ids = prev_obj.revisions
833 new_commit_ids = cur_obj.revisions
834 new_commit_ids = cur_obj.revisions
834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
835 commit_changes = PullRequestModel()._calculate_commit_id_changes(
835 old_commit_ids, new_commit_ids)
836 old_commit_ids, new_commit_ids)
836 c.commit_changes_summary = commit_changes
837 c.commit_changes_summary = commit_changes
837
838
838 # calculate the diff for commits between versions
839 # calculate the diff for commits between versions
839 c.commit_changes = []
840 c.commit_changes = []
840 mark = lambda cs, fw: list(
841 mark = lambda cs, fw: list(
841 h.itertools.izip_longest([], cs, fillvalue=fw))
842 h.itertools.izip_longest([], cs, fillvalue=fw))
842 for c_type, raw_id in mark(commit_changes.added, 'a') \
843 for c_type, raw_id in mark(commit_changes.added, 'a') \
843 + mark(commit_changes.removed, 'r') \
844 + mark(commit_changes.removed, 'r') \
844 + mark(commit_changes.common, 'c'):
845 + mark(commit_changes.common, 'c'):
845
846
846 if raw_id in commit_cache:
847 if raw_id in commit_cache:
847 commit = commit_cache[raw_id]
848 commit = commit_cache[raw_id]
848 else:
849 else:
849 try:
850 try:
850 commit = commits_source_repo.get_commit(raw_id)
851 commit = commits_source_repo.get_commit(raw_id)
851 except CommitDoesNotExistError:
852 except CommitDoesNotExistError:
852 # in case we fail extracting still use "dummy" commit
853 # in case we fail extracting still use "dummy" commit
853 # for display in commit diff
854 # for display in commit diff
854 commit = h.AttributeDict(
855 commit = h.AttributeDict(
855 {'raw_id': raw_id,
856 {'raw_id': raw_id,
856 'message': 'EMPTY or MISSING COMMIT'})
857 'message': 'EMPTY or MISSING COMMIT'})
857 c.commit_changes.append([c_type, commit])
858 c.commit_changes.append([c_type, commit])
858
859
859 # current user review statuses for each version
860 # current user review statuses for each version
860 c.review_versions = {}
861 c.review_versions = {}
861 if c.rhodecode_user.user_id in allowed_reviewers:
862 if c.rhodecode_user.user_id in allowed_reviewers:
862 for co in general_comments:
863 for co in general_comments:
863 if co.author.user_id == c.rhodecode_user.user_id:
864 if co.author.user_id == c.rhodecode_user.user_id:
864 # each comment has a status change
865 # each comment has a status change
865 status = co.status_change
866 status = co.status_change
866 if status:
867 if status:
867 _ver_pr = status[0].comment.pull_request_version_id
868 _ver_pr = status[0].comment.pull_request_version_id
868 c.review_versions[_ver_pr] = status[0]
869 c.review_versions[_ver_pr] = status[0]
869
870
870 return render('/pullrequests/pullrequest_show.mako')
871 return render('/pullrequests/pullrequest_show.mako')
871
872
872 @LoginRequired()
873 @LoginRequired()
873 @NotAnonymous()
874 @NotAnonymous()
874 @HasRepoPermissionAnyDecorator(
875 @HasRepoPermissionAnyDecorator(
875 'repository.read', 'repository.write', 'repository.admin')
876 'repository.read', 'repository.write', 'repository.admin')
876 @auth.CSRFRequired()
877 @auth.CSRFRequired()
877 @jsonify
878 @jsonify
878 def comment(self, repo_name, pull_request_id):
879 def comment(self, repo_name, pull_request_id):
879 pull_request_id = safe_int(pull_request_id)
880 pull_request_id = safe_int(pull_request_id)
880 pull_request = PullRequest.get_or_404(pull_request_id)
881 pull_request = PullRequest.get_or_404(pull_request_id)
881 if pull_request.is_closed():
882 if pull_request.is_closed():
882 log.debug('comment: forbidden because pull request is closed')
883 log.debug('comment: forbidden because pull request is closed')
883 raise HTTPForbidden()
884 raise HTTPForbidden()
884
885
885 status = request.POST.get('changeset_status', None)
886 status = request.POST.get('changeset_status', None)
886 text = request.POST.get('text')
887 text = request.POST.get('text')
887 comment_type = request.POST.get('comment_type')
888 comment_type = request.POST.get('comment_type')
888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
889 resolves_comment_id = request.POST.get('resolves_comment_id', None)
889 close_pull_request = request.POST.get('close_pull_request')
890 close_pull_request = request.POST.get('close_pull_request')
890
891
891 # the logic here should work like following, if we submit close
892 # the logic here should work like following, if we submit close
892 # pr comment, use `close_pull_request_with_comment` function
893 # pr comment, use `close_pull_request_with_comment` function
893 # else handle regular comment logic
894 # else handle regular comment logic
894 user = c.rhodecode_user
895 user = c.rhodecode_user
895 repo = c.rhodecode_db_repo
896 repo = c.rhodecode_db_repo
896
897
897 if close_pull_request:
898 if close_pull_request:
898 # only owner or admin or person with write permissions
899 # only owner or admin or person with write permissions
899 allowed_to_close = PullRequestModel().check_user_update(
900 allowed_to_close = PullRequestModel().check_user_update(
900 pull_request, c.rhodecode_user)
901 pull_request, c.rhodecode_user)
901 if not allowed_to_close:
902 if not allowed_to_close:
902 log.debug('comment: forbidden because not allowed to close '
903 log.debug('comment: forbidden because not allowed to close '
903 'pull request %s', pull_request_id)
904 'pull request %s', pull_request_id)
904 raise HTTPForbidden()
905 raise HTTPForbidden()
905 comment, status = PullRequestModel().close_pull_request_with_comment(
906 comment, status = PullRequestModel().close_pull_request_with_comment(
906 pull_request, user, repo, message=text)
907 pull_request, user, repo, message=text)
907 Session().flush()
908 Session().flush()
908 events.trigger(
909 events.trigger(
909 events.PullRequestCommentEvent(pull_request, comment))
910 events.PullRequestCommentEvent(pull_request, comment))
910
911
911 else:
912 else:
912 # regular comment case, could be inline, or one with status.
913 # regular comment case, could be inline, or one with status.
913 # for that one we check also permissions
914 # for that one we check also permissions
914
915
915 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 pull_request, c.rhodecode_user)
917 pull_request, c.rhodecode_user)
917
918
918 if status and allowed_to_change_status:
919 if status and allowed_to_change_status:
919 message = (_('Status change %(transition_icon)s %(status)s')
920 message = (_('Status change %(transition_icon)s %(status)s')
920 % {'transition_icon': '>',
921 % {'transition_icon': '>',
921 'status': ChangesetStatus.get_status_lbl(status)})
922 'status': ChangesetStatus.get_status_lbl(status)})
922 text = text or message
923 text = text or message
923
924
924 comment = CommentsModel().create(
925 comment = CommentsModel().create(
925 text=text,
926 text=text,
926 repo=c.rhodecode_db_repo.repo_id,
927 repo=c.rhodecode_db_repo.repo_id,
927 user=c.rhodecode_user.user_id,
928 user=c.rhodecode_user.user_id,
928 pull_request=pull_request_id,
929 pull_request=pull_request_id,
929 f_path=request.POST.get('f_path'),
930 f_path=request.POST.get('f_path'),
930 line_no=request.POST.get('line'),
931 line_no=request.POST.get('line'),
931 status_change=(ChangesetStatus.get_status_lbl(status)
932 status_change=(ChangesetStatus.get_status_lbl(status)
932 if status and allowed_to_change_status else None),
933 if status and allowed_to_change_status else None),
933 status_change_type=(status
934 status_change_type=(status
934 if status and allowed_to_change_status else None),
935 if status and allowed_to_change_status else None),
935 comment_type=comment_type,
936 comment_type=comment_type,
936 resolves_comment_id=resolves_comment_id
937 resolves_comment_id=resolves_comment_id
937 )
938 )
938
939
939 if allowed_to_change_status:
940 if allowed_to_change_status:
940 # calculate old status before we change it
941 # calculate old status before we change it
941 old_calculated_status = pull_request.calculated_review_status()
942 old_calculated_status = pull_request.calculated_review_status()
942
943
943 # get status if set !
944 # get status if set !
944 if status:
945 if status:
945 ChangesetStatusModel().set_status(
946 ChangesetStatusModel().set_status(
946 c.rhodecode_db_repo.repo_id,
947 c.rhodecode_db_repo.repo_id,
947 status,
948 status,
948 c.rhodecode_user.user_id,
949 c.rhodecode_user.user_id,
949 comment,
950 comment,
950 pull_request=pull_request_id
951 pull_request=pull_request_id
951 )
952 )
952
953
953 Session().flush()
954 Session().flush()
954 events.trigger(
955 events.trigger(
955 events.PullRequestCommentEvent(pull_request, comment))
956 events.PullRequestCommentEvent(pull_request, comment))
956
957
957 # we now calculate the status of pull request, and based on that
958 # we now calculate the status of pull request, and based on that
958 # calculation we set the commits status
959 # calculation we set the commits status
959 calculated_status = pull_request.calculated_review_status()
960 calculated_status = pull_request.calculated_review_status()
960 if old_calculated_status != calculated_status:
961 if old_calculated_status != calculated_status:
961 PullRequestModel()._trigger_pull_request_hook(
962 PullRequestModel()._trigger_pull_request_hook(
962 pull_request, c.rhodecode_user, 'review_status_change')
963 pull_request, c.rhodecode_user, 'review_status_change')
963
964
964 Session().commit()
965 Session().commit()
965
966
966 if not request.is_xhr:
967 if not request.is_xhr:
967 return redirect(h.url('pullrequest_show', repo_name=repo_name,
968 raise HTTPFound(
968 pull_request_id=pull_request_id))
969 h.route_path('pullrequest_show',
970 repo_name=repo_name,
971 pull_request_id=pull_request_id))
969
972
970 data = {
973 data = {
971 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
974 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
972 }
975 }
973 if comment:
976 if comment:
974 c.co = comment
977 c.co = comment
975 rendered_comment = render('changeset/changeset_comment_block.mako')
978 rendered_comment = render('changeset/changeset_comment_block.mako')
976 data.update(comment.get_dict())
979 data.update(comment.get_dict())
977 data.update({'rendered_text': rendered_comment})
980 data.update({'rendered_text': rendered_comment})
978
981
979 return data
982 return data
980
983
981 @LoginRequired()
984 @LoginRequired()
982 @NotAnonymous()
985 @NotAnonymous()
983 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
986 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
984 'repository.admin')
987 'repository.admin')
985 @auth.CSRFRequired()
988 @auth.CSRFRequired()
986 @jsonify
989 @jsonify
987 def delete_comment(self, repo_name, comment_id):
990 def delete_comment(self, repo_name, comment_id):
988 return self._delete_comment(comment_id)
991 return self._delete_comment(comment_id)
989
992
990 def _delete_comment(self, comment_id):
993 def _delete_comment(self, comment_id):
991 comment_id = safe_int(comment_id)
994 comment_id = safe_int(comment_id)
992 co = ChangesetComment.get_or_404(comment_id)
995 co = ChangesetComment.get_or_404(comment_id)
993 if co.pull_request.is_closed():
996 if co.pull_request.is_closed():
994 # don't allow deleting comments on closed pull request
997 # don't allow deleting comments on closed pull request
995 raise HTTPForbidden()
998 raise HTTPForbidden()
996
999
997 is_owner = co.author.user_id == c.rhodecode_user.user_id
1000 is_owner = co.author.user_id == c.rhodecode_user.user_id
998 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1001 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
999 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1002 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1000 old_calculated_status = co.pull_request.calculated_review_status()
1003 old_calculated_status = co.pull_request.calculated_review_status()
1001 CommentsModel().delete(comment=co, user=c.rhodecode_user)
1004 CommentsModel().delete(comment=co, user=c.rhodecode_user)
1002 Session().commit()
1005 Session().commit()
1003 calculated_status = co.pull_request.calculated_review_status()
1006 calculated_status = co.pull_request.calculated_review_status()
1004 if old_calculated_status != calculated_status:
1007 if old_calculated_status != calculated_status:
1005 PullRequestModel()._trigger_pull_request_hook(
1008 PullRequestModel()._trigger_pull_request_hook(
1006 co.pull_request, c.rhodecode_user, 'review_status_change')
1009 co.pull_request, c.rhodecode_user, 'review_status_change')
1007 return True
1010 return True
1008 else:
1011 else:
1009 raise HTTPForbidden()
1012 raise HTTPForbidden()
@@ -1,315 +1,316 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pylons import url
23 from pylons import url
24 from pylons.i18n.translation import _
24 from pylons.i18n.translation import _
25 from webhelpers.html.builder import literal
25 from webhelpers.html.builder import literal
26 from webhelpers.html.tags import link_to
26 from webhelpers.html.tags import link_to
27
27
28 from rhodecode.lib.utils2 import AttributeDict
28 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.vcs.backends.base import BaseCommit
29 from rhodecode.lib.vcs.backends.base import BaseCommit
30 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
30 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
31
31
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 def action_parser(user_log, feed=False, parse_cs=False):
36 def action_parser(user_log, feed=False, parse_cs=False):
37 """
37 """
38 This helper will action_map the specified string action into translated
38 This helper will action_map the specified string action into translated
39 fancy names with icons and links
39 fancy names with icons and links
40
40
41 :param user_log: user log instance
41 :param user_log: user log instance
42 :param feed: use output for feeds (no html and fancy icons)
42 :param feed: use output for feeds (no html and fancy icons)
43 :param parse_cs: parse Changesets into VCS instances
43 :param parse_cs: parse Changesets into VCS instances
44 """
44 """
45 ap = ActionParser(user_log, feed=False, parse_commits=False)
45 ap = ActionParser(user_log, feed=False, parse_commits=False)
46 return ap.callbacks()
46 return ap.callbacks()
47
47
48
48
49 class ActionParser(object):
49 class ActionParser(object):
50
50
51 commits_limit = 3 # display this amount always
51 commits_limit = 3 # display this amount always
52 commits_top_limit = 50 # show up to this amount of commits hidden
52 commits_top_limit = 50 # show up to this amount of commits hidden
53
53
54 def __init__(self, user_log, feed=False, parse_commits=False):
54 def __init__(self, user_log, feed=False, parse_commits=False):
55 self.user_log = user_log
55 self.user_log = user_log
56 self.feed = feed
56 self.feed = feed
57 self.parse_commits = parse_commits
57 self.parse_commits = parse_commits
58
58
59 self.action = user_log.action
59 self.action = user_log.action
60 self.action_params = ' '
60 self.action_params = ' '
61 x = self.action.split(':', 1)
61 x = self.action.split(':', 1)
62 if len(x) > 1:
62 if len(x) > 1:
63 self.action, self.action_params = x
63 self.action, self.action_params = x
64
64
65 def callbacks(self):
65 def callbacks(self):
66 action_str = self.action_map.get(self.action, self.action)
66 action_str = self.action_map.get(self.action, self.action)
67 if self.feed:
67 if self.feed:
68 action = action_str[0].replace('[', '').replace(']', '')
68 action = action_str[0].replace('[', '').replace(']', '')
69 else:
69 else:
70 action = action_str[0]\
70 action = action_str[0]\
71 .replace('[', '<span class="journal_highlight">')\
71 .replace('[', '<span class="journal_highlight">')\
72 .replace(']', '</span>')
72 .replace(']', '</span>')
73
73
74 action_params_func = _no_params_func
74 action_params_func = _no_params_func
75 if callable(action_str[1]):
75 if callable(action_str[1]):
76 action_params_func = action_str[1]
76 action_params_func = action_str[1]
77
77
78 # returned callbacks we need to call to get
78 # returned callbacks we need to call to get
79 return [
79 return [
80 lambda: literal(action), action_params_func,
80 lambda: literal(action), action_params_func,
81 self.action_parser_icon]
81 self.action_parser_icon]
82
82
83 @property
83 @property
84 def action_map(self):
84 def action_map(self):
85
85
86 # action : translated str, callback(extractor), icon
86 # action : translated str, callback(extractor), icon
87 action_map = {
87 action_map = {
88 'user_deleted_repo': (
88 'user_deleted_repo': (
89 _('[deleted] repository'),
89 _('[deleted] repository'),
90 None, 'icon-trash'),
90 None, 'icon-trash'),
91 'user_created_repo': (
91 'user_created_repo': (
92 _('[created] repository'),
92 _('[created] repository'),
93 None, 'icon-plus icon-plus-colored'),
93 None, 'icon-plus icon-plus-colored'),
94 'user_created_fork': (
94 'user_created_fork': (
95 _('[created] repository as fork'),
95 _('[created] repository as fork'),
96 None, 'icon-code-fork'),
96 None, 'icon-code-fork'),
97 'user_forked_repo': (
97 'user_forked_repo': (
98 _('[forked] repository'),
98 _('[forked] repository'),
99 self.get_fork_name, 'icon-code-fork'),
99 self.get_fork_name, 'icon-code-fork'),
100 'user_updated_repo': (
100 'user_updated_repo': (
101 _('[updated] repository'),
101 _('[updated] repository'),
102 None, 'icon-pencil icon-pencil-colored'),
102 None, 'icon-pencil icon-pencil-colored'),
103 'user_downloaded_archive': (
103 'user_downloaded_archive': (
104 _('[downloaded] archive from repository'),
104 _('[downloaded] archive from repository'),
105 self.get_archive_name, 'icon-download-alt'),
105 self.get_archive_name, 'icon-download-alt'),
106 'admin_deleted_repo': (
106 'admin_deleted_repo': (
107 _('[delete] repository'),
107 _('[delete] repository'),
108 None, 'icon-trash'),
108 None, 'icon-trash'),
109 'admin_created_repo': (
109 'admin_created_repo': (
110 _('[created] repository'),
110 _('[created] repository'),
111 None, 'icon-plus icon-plus-colored'),
111 None, 'icon-plus icon-plus-colored'),
112 'admin_forked_repo': (
112 'admin_forked_repo': (
113 _('[forked] repository'),
113 _('[forked] repository'),
114 None, 'icon-code-fork icon-fork-colored'),
114 None, 'icon-code-fork icon-fork-colored'),
115 'admin_updated_repo': (
115 'admin_updated_repo': (
116 _('[updated] repository'),
116 _('[updated] repository'),
117 None, 'icon-pencil icon-pencil-colored'),
117 None, 'icon-pencil icon-pencil-colored'),
118 'admin_created_user': (
118 'admin_created_user': (
119 _('[created] user'),
119 _('[created] user'),
120 self.get_user_name, 'icon-user icon-user-colored'),
120 self.get_user_name, 'icon-user icon-user-colored'),
121 'admin_updated_user': (
121 'admin_updated_user': (
122 _('[updated] user'),
122 _('[updated] user'),
123 self.get_user_name, 'icon-user icon-user-colored'),
123 self.get_user_name, 'icon-user icon-user-colored'),
124 'admin_created_users_group': (
124 'admin_created_users_group': (
125 _('[created] user group'),
125 _('[created] user group'),
126 self.get_users_group, 'icon-pencil icon-pencil-colored'),
126 self.get_users_group, 'icon-pencil icon-pencil-colored'),
127 'admin_updated_users_group': (
127 'admin_updated_users_group': (
128 _('[updated] user group'),
128 _('[updated] user group'),
129 self.get_users_group, 'icon-pencil icon-pencil-colored'),
129 self.get_users_group, 'icon-pencil icon-pencil-colored'),
130 'user_commented_revision': (
130 'user_commented_revision': (
131 _('[commented] on commit in repository'),
131 _('[commented] on commit in repository'),
132 self.get_cs_links, 'icon-comment icon-comment-colored'),
132 self.get_cs_links, 'icon-comment icon-comment-colored'),
133 'user_commented_pull_request': (
133 'user_commented_pull_request': (
134 _('[commented] on pull request for'),
134 _('[commented] on pull request for'),
135 self.get_pull_request, 'icon-comment icon-comment-colored'),
135 self.get_pull_request, 'icon-comment icon-comment-colored'),
136 'user_closed_pull_request': (
136 'user_closed_pull_request': (
137 _('[closed] pull request for'),
137 _('[closed] pull request for'),
138 self.get_pull_request, 'icon-check'),
138 self.get_pull_request, 'icon-check'),
139 'user_merged_pull_request': (
139 'user_merged_pull_request': (
140 _('[merged] pull request for'),
140 _('[merged] pull request for'),
141 self.get_pull_request, 'icon-check'),
141 self.get_pull_request, 'icon-check'),
142 'push': (
142 'push': (
143 _('[pushed] into'),
143 _('[pushed] into'),
144 self.get_cs_links, 'icon-arrow-up'),
144 self.get_cs_links, 'icon-arrow-up'),
145 'push_local': (
145 'push_local': (
146 _('[committed via RhodeCode] into repository'),
146 _('[committed via RhodeCode] into repository'),
147 self.get_cs_links, 'icon-pencil icon-pencil-colored'),
147 self.get_cs_links, 'icon-pencil icon-pencil-colored'),
148 'push_remote': (
148 'push_remote': (
149 _('[pulled from remote] into repository'),
149 _('[pulled from remote] into repository'),
150 self.get_cs_links, 'icon-arrow-up'),
150 self.get_cs_links, 'icon-arrow-up'),
151 'pull': (
151 'pull': (
152 _('[pulled] from'),
152 _('[pulled] from'),
153 None, 'icon-arrow-down'),
153 None, 'icon-arrow-down'),
154 'started_following_repo': (
154 'started_following_repo': (
155 _('[started following] repository'),
155 _('[started following] repository'),
156 None, 'icon-heart icon-heart-colored'),
156 None, 'icon-heart icon-heart-colored'),
157 'stopped_following_repo': (
157 'stopped_following_repo': (
158 _('[stopped following] repository'),
158 _('[stopped following] repository'),
159 None, 'icon-heart-empty icon-heart-colored'),
159 None, 'icon-heart-empty icon-heart-colored'),
160 }
160 }
161 return action_map
161 return action_map
162
162
163 def get_fork_name(self):
163 def get_fork_name(self):
164 from rhodecode.lib import helpers as h
164 from rhodecode.lib import helpers as h
165 repo_name = self.action_params
165 repo_name = self.action_params
166 _url = h.route_path('repo_summary', repo_name=repo_name)
166 _url = h.route_path('repo_summary', repo_name=repo_name)
167 return _('fork name %s') % link_to(self.action_params, _url)
167 return _('fork name %s') % link_to(self.action_params, _url)
168
168
169 def get_user_name(self):
169 def get_user_name(self):
170 user_name = self.action_params
170 user_name = self.action_params
171 return user_name
171 return user_name
172
172
173 def get_users_group(self):
173 def get_users_group(self):
174 group_name = self.action_params
174 group_name = self.action_params
175 return group_name
175 return group_name
176
176
177 def get_pull_request(self):
177 def get_pull_request(self):
178 from rhodecode.lib import helpers as h
178 pull_request_id = self.action_params
179 pull_request_id = self.action_params
179 if self.is_deleted():
180 if self.is_deleted():
180 repo_name = self.user_log.repository_name
181 repo_name = self.user_log.repository_name
181 else:
182 else:
182 repo_name = self.user_log.repository.repo_name
183 repo_name = self.user_log.repository.repo_name
183 return link_to(
184 return link_to(
184 _('Pull request #%s') % pull_request_id,
185 _('Pull request #%s') % pull_request_id,
185 url('pullrequest_show', repo_name=repo_name,
186 h.route_path('pullrequest_show', repo_name=repo_name,
186 pull_request_id=pull_request_id))
187 pull_request_id=pull_request_id))
187
188
188 def get_archive_name(self):
189 def get_archive_name(self):
189 archive_name = self.action_params
190 archive_name = self.action_params
190 return archive_name
191 return archive_name
191
192
192 def action_parser_icon(self):
193 def action_parser_icon(self):
193 tmpl = """<i class="%s" alt="%s"></i>"""
194 tmpl = """<i class="%s" alt="%s"></i>"""
194 ico = self.action_map.get(self.action, ['', '', ''])[2]
195 ico = self.action_map.get(self.action, ['', '', ''])[2]
195 return literal(tmpl % (ico, self.action))
196 return literal(tmpl % (ico, self.action))
196
197
197 def get_cs_links(self):
198 def get_cs_links(self):
198 if self.is_deleted():
199 if self.is_deleted():
199 return self.action_params
200 return self.action_params
200
201
201 repo_name = self.user_log.repository.repo_name
202 repo_name = self.user_log.repository.repo_name
202 commit_ids = self.action_params.split(',')
203 commit_ids = self.action_params.split(',')
203 commits = self.get_commits(commit_ids)
204 commits = self.get_commits(commit_ids)
204
205
205 link_generator = (
206 link_generator = (
206 self.lnk(commit, repo_name)
207 self.lnk(commit, repo_name)
207 for commit in commits[:self.commits_limit])
208 for commit in commits[:self.commits_limit])
208 commit_links = [" " + ', '.join(link_generator)]
209 commit_links = [" " + ', '.join(link_generator)]
209 _op1, _name1 = _get_op(commit_ids[0])
210 _op1, _name1 = _get_op(commit_ids[0])
210 _op2, _name2 = _get_op(commit_ids[-1])
211 _op2, _name2 = _get_op(commit_ids[-1])
211
212
212 commit_id_range = '%s...%s' % (_name1, _name2)
213 commit_id_range = '%s...%s' % (_name1, _name2)
213
214
214 compare_view = (
215 compare_view = (
215 ' <div class="compare_view tooltip" title="%s">'
216 ' <div class="compare_view tooltip" title="%s">'
216 '<a href="%s">%s</a> </div>' % (
217 '<a href="%s">%s</a> </div>' % (
217 _('Show all combined commits %s->%s') % (
218 _('Show all combined commits %s->%s') % (
218 commit_ids[0][:12], commit_ids[-1][:12]
219 commit_ids[0][:12], commit_ids[-1][:12]
219 ),
220 ),
220 url('changeset_home', repo_name=repo_name,
221 url('changeset_home', repo_name=repo_name,
221 revision=commit_id_range), _('compare view')
222 revision=commit_id_range), _('compare view')
222 )
223 )
223 )
224 )
224
225
225 if len(commit_ids) > self.commits_limit:
226 if len(commit_ids) > self.commits_limit:
226 more_count = len(commit_ids) - self.commits_limit
227 more_count = len(commit_ids) - self.commits_limit
227 commit_links.append(
228 commit_links.append(
228 _(' and %(num)s more commits') % {'num': more_count}
229 _(' and %(num)s more commits') % {'num': more_count}
229 )
230 )
230
231
231 if len(commits) > 1:
232 if len(commits) > 1:
232 commit_links.append(compare_view)
233 commit_links.append(compare_view)
233 return ''.join(commit_links)
234 return ''.join(commit_links)
234
235
235 def get_commits(self, commit_ids):
236 def get_commits(self, commit_ids):
236 commits = []
237 commits = []
237 if not filter(lambda v: v != '', commit_ids):
238 if not filter(lambda v: v != '', commit_ids):
238 return commits
239 return commits
239
240
240 repo = None
241 repo = None
241 if self.parse_commits:
242 if self.parse_commits:
242 repo = self.user_log.repository.scm_instance()
243 repo = self.user_log.repository.scm_instance()
243
244
244 for commit_id in commit_ids[:self.commits_top_limit]:
245 for commit_id in commit_ids[:self.commits_top_limit]:
245 _op, _name = _get_op(commit_id)
246 _op, _name = _get_op(commit_id)
246
247
247 # we want parsed commits, or new log store format is bad
248 # we want parsed commits, or new log store format is bad
248 if self.parse_commits:
249 if self.parse_commits:
249 try:
250 try:
250 commit = repo.get_commit(commit_id=commit_id)
251 commit = repo.get_commit(commit_id=commit_id)
251 commits.append(commit)
252 commits.append(commit)
252 except CommitDoesNotExistError:
253 except CommitDoesNotExistError:
253 log.error(
254 log.error(
254 'cannot find commit id %s in this repository',
255 'cannot find commit id %s in this repository',
255 commit_id)
256 commit_id)
256 commits.append(commit_id)
257 commits.append(commit_id)
257 continue
258 continue
258 else:
259 else:
259 fake_commit = AttributeDict({
260 fake_commit = AttributeDict({
260 'short_id': commit_id[:12],
261 'short_id': commit_id[:12],
261 'raw_id': commit_id,
262 'raw_id': commit_id,
262 'message': '',
263 'message': '',
263 'op': _op,
264 'op': _op,
264 'ref_name': _name
265 'ref_name': _name
265 })
266 })
266 commits.append(fake_commit)
267 commits.append(fake_commit)
267
268
268 return commits
269 return commits
269
270
270 def lnk(self, commit_or_id, repo_name):
271 def lnk(self, commit_or_id, repo_name):
271 from rhodecode.lib.helpers import tooltip
272 from rhodecode.lib.helpers import tooltip
272
273
273 if isinstance(commit_or_id, (BaseCommit, AttributeDict)):
274 if isinstance(commit_or_id, (BaseCommit, AttributeDict)):
274 lazy_cs = True
275 lazy_cs = True
275 if (getattr(commit_or_id, 'op', None) and
276 if (getattr(commit_or_id, 'op', None) and
276 getattr(commit_or_id, 'ref_name', None)):
277 getattr(commit_or_id, 'ref_name', None)):
277 lazy_cs = False
278 lazy_cs = False
278 lbl = '?'
279 lbl = '?'
279 if commit_or_id.op == 'delete_branch':
280 if commit_or_id.op == 'delete_branch':
280 lbl = '%s' % _('Deleted branch: %s') % commit_or_id.ref_name
281 lbl = '%s' % _('Deleted branch: %s') % commit_or_id.ref_name
281 title = ''
282 title = ''
282 elif commit_or_id.op == 'tag':
283 elif commit_or_id.op == 'tag':
283 lbl = '%s' % _('Created tag: %s') % commit_or_id.ref_name
284 lbl = '%s' % _('Created tag: %s') % commit_or_id.ref_name
284 title = ''
285 title = ''
285 _url = '#'
286 _url = '#'
286
287
287 else:
288 else:
288 lbl = '%s' % (commit_or_id.short_id[:8])
289 lbl = '%s' % (commit_or_id.short_id[:8])
289 _url = url('changeset_home', repo_name=repo_name,
290 _url = url('changeset_home', repo_name=repo_name,
290 revision=commit_or_id.raw_id)
291 revision=commit_or_id.raw_id)
291 title = tooltip(commit_or_id.message)
292 title = tooltip(commit_or_id.message)
292 else:
293 else:
293 # commit cannot be found/striped/removed etc.
294 # commit cannot be found/striped/removed etc.
294 lbl = ('%s' % commit_or_id)[:12]
295 lbl = ('%s' % commit_or_id)[:12]
295 _url = '#'
296 _url = '#'
296 title = _('Commit not found')
297 title = _('Commit not found')
297 if self.parse_commits:
298 if self.parse_commits:
298 return link_to(lbl, _url, title=title, class_='tooltip')
299 return link_to(lbl, _url, title=title, class_='tooltip')
299 return link_to(lbl, _url, raw_id=commit_or_id.raw_id, repo_name=repo_name,
300 return link_to(lbl, _url, raw_id=commit_or_id.raw_id, repo_name=repo_name,
300 class_='lazy-cs' if lazy_cs else '')
301 class_='lazy-cs' if lazy_cs else '')
301
302
302 def is_deleted(self):
303 def is_deleted(self):
303 return self.user_log.repository is None
304 return self.user_log.repository is None
304
305
305
306
306 def _no_params_func():
307 def _no_params_func():
307 return ""
308 return ""
308
309
309
310
310 def _get_op(commit_id):
311 def _get_op(commit_id):
311 _op = None
312 _op = None
312 _name = commit_id
313 _name = commit_id
313 if len(commit_id.split('=>')) == 2:
314 if len(commit_id.split('=>')) == 2:
314 _op, _name = commit_id.split('=>')
315 _op, _name = commit_id.split('=>')
315 return _op, _name
316 return _op, _name
@@ -1,596 +1,601 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import ipaddress
30 import ipaddress
31 import pyramid.threadlocal
31 import pyramid.threadlocal
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from pylons import config, tmpl_context as c, request, session, url
36 from pylons import config, tmpl_context as c, request, session, url
37 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
38 from pylons.controllers.util import redirect
38 from pylons.controllers.util import redirect
39 from pylons.i18n import translation
39 from pylons.i18n import translation
40 # marcink: don't remove this import
40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako as render # noqa
42 from pylons.i18n.translation import _
42 from pylons.i18n.translation import _
43 from webob.exc import HTTPFound
43 from webob.exc import HTTPFound
44
44
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import helpers as h
49 from rhodecode.lib import helpers as h
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.utils import (
52 from rhodecode.lib.utils import (
53 get_repo_slug, set_rhodecode_config, password_changed,
53 get_repo_slug, set_rhodecode_config, password_changed,
54 get_enabled_hook_classes)
54 get_enabled_hook_classes)
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.model import meta
58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User, ChangesetComment
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 def _filter_proxy(ip):
68 def _filter_proxy(ip):
69 """
69 """
70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 ips. Those comma separated IPs are passed from various proxies in the
71 ips. Those comma separated IPs are passed from various proxies in the
72 chain of request processing. The left-most being the original client.
72 chain of request processing. The left-most being the original client.
73 We only care about the first IP which came from the org. client.
73 We only care about the first IP which came from the org. client.
74
74
75 :param ip: ip string from headers
75 :param ip: ip string from headers
76 """
76 """
77 if ',' in ip:
77 if ',' in ip:
78 _ips = ip.split(',')
78 _ips = ip.split(',')
79 _first_ip = _ips[0].strip()
79 _first_ip = _ips[0].strip()
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 return _first_ip
81 return _first_ip
82 return ip
82 return ip
83
83
84
84
85 def _filter_port(ip):
85 def _filter_port(ip):
86 """
86 """
87 Removes a port from ip, there are 4 main cases to handle here.
87 Removes a port from ip, there are 4 main cases to handle here.
88 - ipv4 eg. 127.0.0.1
88 - ipv4 eg. 127.0.0.1
89 - ipv6 eg. ::1
89 - ipv6 eg. ::1
90 - ipv4+port eg. 127.0.0.1:8080
90 - ipv4+port eg. 127.0.0.1:8080
91 - ipv6+port eg. [::1]:8080
91 - ipv6+port eg. [::1]:8080
92
92
93 :param ip:
93 :param ip:
94 """
94 """
95 def is_ipv6(ip_addr):
95 def is_ipv6(ip_addr):
96 if hasattr(socket, 'inet_pton'):
96 if hasattr(socket, 'inet_pton'):
97 try:
97 try:
98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 except socket.error:
99 except socket.error:
100 return False
100 return False
101 else:
101 else:
102 # fallback to ipaddress
102 # fallback to ipaddress
103 try:
103 try:
104 ipaddress.IPv6Address(ip_addr)
104 ipaddress.IPv6Address(ip_addr)
105 except Exception:
105 except Exception:
106 return False
106 return False
107 return True
107 return True
108
108
109 if ':' not in ip: # must be ipv4 pure ip
109 if ':' not in ip: # must be ipv4 pure ip
110 return ip
110 return ip
111
111
112 if '[' in ip and ']' in ip: # ipv6 with port
112 if '[' in ip and ']' in ip: # ipv6 with port
113 return ip.split(']')[0][1:].lower()
113 return ip.split(']')[0][1:].lower()
114
114
115 # must be ipv6 or ipv4 with port
115 # must be ipv6 or ipv4 with port
116 if is_ipv6(ip):
116 if is_ipv6(ip):
117 return ip
117 return ip
118 else:
118 else:
119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 return ip
120 return ip
121
121
122
122
123 def get_ip_addr(environ):
123 def get_ip_addr(environ):
124 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key = 'HTTP_X_REAL_IP'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 def_key = 'REMOTE_ADDR'
126 def_key = 'REMOTE_ADDR'
127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 _filters = lambda x: _filter_port(_filter_proxy(x))
128
128
129 ip = environ.get(proxy_key)
129 ip = environ.get(proxy_key)
130 if ip:
130 if ip:
131 return _filters(ip)
131 return _filters(ip)
132
132
133 ip = environ.get(proxy_key2)
133 ip = environ.get(proxy_key2)
134 if ip:
134 if ip:
135 return _filters(ip)
135 return _filters(ip)
136
136
137 ip = environ.get(def_key, '0.0.0.0')
137 ip = environ.get(def_key, '0.0.0.0')
138 return _filters(ip)
138 return _filters(ip)
139
139
140
140
141 def get_server_ip_addr(environ, log_errors=True):
141 def get_server_ip_addr(environ, log_errors=True):
142 hostname = environ.get('SERVER_NAME')
142 hostname = environ.get('SERVER_NAME')
143 try:
143 try:
144 return socket.gethostbyname(hostname)
144 return socket.gethostbyname(hostname)
145 except Exception as e:
145 except Exception as e:
146 if log_errors:
146 if log_errors:
147 # in some cases this lookup is not possible, and we don't want to
147 # in some cases this lookup is not possible, and we don't want to
148 # make it an exception in logs
148 # make it an exception in logs
149 log.exception('Could not retrieve server ip address: %s', e)
149 log.exception('Could not retrieve server ip address: %s', e)
150 return hostname
150 return hostname
151
151
152
152
153 def get_server_port(environ):
153 def get_server_port(environ):
154 return environ.get('SERVER_PORT')
154 return environ.get('SERVER_PORT')
155
155
156
156
157 def get_access_path(environ):
157 def get_access_path(environ):
158 path = environ.get('PATH_INFO')
158 path = environ.get('PATH_INFO')
159 org_req = environ.get('pylons.original_request')
159 org_req = environ.get('pylons.original_request')
160 if org_req:
160 if org_req:
161 path = org_req.environ.get('PATH_INFO')
161 path = org_req.environ.get('PATH_INFO')
162 return path
162 return path
163
163
164
164
165 def get_user_agent(environ):
165 def get_user_agent(environ):
166 return environ.get('HTTP_USER_AGENT')
166 return environ.get('HTTP_USER_AGENT')
167
167
168
168
169 def vcs_operation_context(
169 def vcs_operation_context(
170 environ, repo_name, username, action, scm, check_locking=True,
170 environ, repo_name, username, action, scm, check_locking=True,
171 is_shadow_repo=False):
171 is_shadow_repo=False):
172 """
172 """
173 Generate the context for a vcs operation, e.g. push or pull.
173 Generate the context for a vcs operation, e.g. push or pull.
174
174
175 This context is passed over the layers so that hooks triggered by the
175 This context is passed over the layers so that hooks triggered by the
176 vcs operation know details like the user, the user's IP address etc.
176 vcs operation know details like the user, the user's IP address etc.
177
177
178 :param check_locking: Allows to switch of the computation of the locking
178 :param check_locking: Allows to switch of the computation of the locking
179 data. This serves mainly the need of the simplevcs middleware to be
179 data. This serves mainly the need of the simplevcs middleware to be
180 able to disable this for certain operations.
180 able to disable this for certain operations.
181
181
182 """
182 """
183 # Tri-state value: False: unlock, None: nothing, True: lock
183 # Tri-state value: False: unlock, None: nothing, True: lock
184 make_lock = None
184 make_lock = None
185 locked_by = [None, None, None]
185 locked_by = [None, None, None]
186 is_anonymous = username == User.DEFAULT_USER
186 is_anonymous = username == User.DEFAULT_USER
187 if not is_anonymous and check_locking:
187 if not is_anonymous and check_locking:
188 log.debug('Checking locking on repository "%s"', repo_name)
188 log.debug('Checking locking on repository "%s"', repo_name)
189 user = User.get_by_username(username)
189 user = User.get_by_username(username)
190 repo = Repository.get_by_repo_name(repo_name)
190 repo = Repository.get_by_repo_name(repo_name)
191 make_lock, __, locked_by = repo.get_locking_state(
191 make_lock, __, locked_by = repo.get_locking_state(
192 action, user.user_id)
192 action, user.user_id)
193
193
194 settings_model = VcsSettingsModel(repo=repo_name)
194 settings_model = VcsSettingsModel(repo=repo_name)
195 ui_settings = settings_model.get_ui_settings()
195 ui_settings = settings_model.get_ui_settings()
196
196
197 extras = {
197 extras = {
198 'ip': get_ip_addr(environ),
198 'ip': get_ip_addr(environ),
199 'username': username,
199 'username': username,
200 'action': action,
200 'action': action,
201 'repository': repo_name,
201 'repository': repo_name,
202 'scm': scm,
202 'scm': scm,
203 'config': rhodecode.CONFIG['__file__'],
203 'config': rhodecode.CONFIG['__file__'],
204 'make_lock': make_lock,
204 'make_lock': make_lock,
205 'locked_by': locked_by,
205 'locked_by': locked_by,
206 'server_url': utils2.get_server_url(environ),
206 'server_url': utils2.get_server_url(environ),
207 'user_agent': get_user_agent(environ),
207 'user_agent': get_user_agent(environ),
208 'hooks': get_enabled_hook_classes(ui_settings),
208 'hooks': get_enabled_hook_classes(ui_settings),
209 'is_shadow_repo': is_shadow_repo,
209 'is_shadow_repo': is_shadow_repo,
210 }
210 }
211 return extras
211 return extras
212
212
213
213
214 class BasicAuth(AuthBasicAuthenticator):
214 class BasicAuth(AuthBasicAuthenticator):
215
215
216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 initial_call_detection=False, acl_repo_name=None):
217 initial_call_detection=False, acl_repo_name=None):
218 self.realm = realm
218 self.realm = realm
219 self.initial_call = initial_call_detection
219 self.initial_call = initial_call_detection
220 self.authfunc = authfunc
220 self.authfunc = authfunc
221 self.registry = registry
221 self.registry = registry
222 self.acl_repo_name = acl_repo_name
222 self.acl_repo_name = acl_repo_name
223 self._rc_auth_http_code = auth_http_code
223 self._rc_auth_http_code = auth_http_code
224
224
225 def _get_response_from_code(self, http_code):
225 def _get_response_from_code(self, http_code):
226 try:
226 try:
227 return get_exception(safe_int(http_code))
227 return get_exception(safe_int(http_code))
228 except Exception:
228 except Exception:
229 log.exception('Failed to fetch response for code %s' % http_code)
229 log.exception('Failed to fetch response for code %s' % http_code)
230 return HTTPForbidden
230 return HTTPForbidden
231
231
232 def build_authentication(self):
232 def build_authentication(self):
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 if self._rc_auth_http_code and not self.initial_call:
234 if self._rc_auth_http_code and not self.initial_call:
235 # return alternative HTTP code if alternative http return code
235 # return alternative HTTP code if alternative http return code
236 # is specified in RhodeCode config, but ONLY if it's not the
236 # is specified in RhodeCode config, but ONLY if it's not the
237 # FIRST call
237 # FIRST call
238 custom_response_klass = self._get_response_from_code(
238 custom_response_klass = self._get_response_from_code(
239 self._rc_auth_http_code)
239 self._rc_auth_http_code)
240 return custom_response_klass(headers=head)
240 return custom_response_klass(headers=head)
241 return HTTPUnauthorized(headers=head)
241 return HTTPUnauthorized(headers=head)
242
242
243 def authenticate(self, environ):
243 def authenticate(self, environ):
244 authorization = AUTHORIZATION(environ)
244 authorization = AUTHORIZATION(environ)
245 if not authorization:
245 if not authorization:
246 return self.build_authentication()
246 return self.build_authentication()
247 (authmeth, auth) = authorization.split(' ', 1)
247 (authmeth, auth) = authorization.split(' ', 1)
248 if 'basic' != authmeth.lower():
248 if 'basic' != authmeth.lower():
249 return self.build_authentication()
249 return self.build_authentication()
250 auth = auth.strip().decode('base64')
250 auth = auth.strip().decode('base64')
251 _parts = auth.split(':', 1)
251 _parts = auth.split(':', 1)
252 if len(_parts) == 2:
252 if len(_parts) == 2:
253 username, password = _parts
253 username, password = _parts
254 if self.authfunc(
254 if self.authfunc(
255 username, password, environ, VCS_TYPE,
255 username, password, environ, VCS_TYPE,
256 registry=self.registry, acl_repo_name=self.acl_repo_name):
256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 return username
257 return username
258 if username and password:
258 if username and password:
259 # we mark that we actually executed authentication once, at
259 # we mark that we actually executed authentication once, at
260 # that point we can use the alternative auth code
260 # that point we can use the alternative auth code
261 self.initial_call = False
261 self.initial_call = False
262
262
263 return self.build_authentication()
263 return self.build_authentication()
264
264
265 __call__ = authenticate
265 __call__ = authenticate
266
266
267
267
268 def attach_context_attributes(context, request, user_id, attach_to_request=False):
268 def attach_context_attributes(context, request, user_id, attach_to_request=False):
269 """
269 """
270 Attach variables into template context called `c`, please note that
270 Attach variables into template context called `c`, please note that
271 request could be pylons or pyramid request in here.
271 request could be pylons or pyramid request in here.
272 """
272 """
273 rc_config = SettingsModel().get_all_settings(cache=True)
273 rc_config = SettingsModel().get_all_settings(cache=True)
274
274
275 context.rhodecode_version = rhodecode.__version__
275 context.rhodecode_version = rhodecode.__version__
276 context.rhodecode_edition = config.get('rhodecode.edition')
276 context.rhodecode_edition = config.get('rhodecode.edition')
277 # unique secret + version does not leak the version but keep consistency
277 # unique secret + version does not leak the version but keep consistency
278 context.rhodecode_version_hash = md5(
278 context.rhodecode_version_hash = md5(
279 config.get('beaker.session.secret', '') +
279 config.get('beaker.session.secret', '') +
280 rhodecode.__version__)[:8]
280 rhodecode.__version__)[:8]
281
281
282 # Default language set for the incoming request
282 # Default language set for the incoming request
283 context.language = translation.get_lang()[0]
283 context.language = translation.get_lang()[0]
284
284
285 # Visual options
285 # Visual options
286 context.visual = AttributeDict({})
286 context.visual = AttributeDict({})
287
287
288 # DB stored Visual Items
288 # DB stored Visual Items
289 context.visual.show_public_icon = str2bool(
289 context.visual.show_public_icon = str2bool(
290 rc_config.get('rhodecode_show_public_icon'))
290 rc_config.get('rhodecode_show_public_icon'))
291 context.visual.show_private_icon = str2bool(
291 context.visual.show_private_icon = str2bool(
292 rc_config.get('rhodecode_show_private_icon'))
292 rc_config.get('rhodecode_show_private_icon'))
293 context.visual.stylify_metatags = str2bool(
293 context.visual.stylify_metatags = str2bool(
294 rc_config.get('rhodecode_stylify_metatags'))
294 rc_config.get('rhodecode_stylify_metatags'))
295 context.visual.dashboard_items = safe_int(
295 context.visual.dashboard_items = safe_int(
296 rc_config.get('rhodecode_dashboard_items', 100))
296 rc_config.get('rhodecode_dashboard_items', 100))
297 context.visual.admin_grid_items = safe_int(
297 context.visual.admin_grid_items = safe_int(
298 rc_config.get('rhodecode_admin_grid_items', 100))
298 rc_config.get('rhodecode_admin_grid_items', 100))
299 context.visual.repository_fields = str2bool(
299 context.visual.repository_fields = str2bool(
300 rc_config.get('rhodecode_repository_fields'))
300 rc_config.get('rhodecode_repository_fields'))
301 context.visual.show_version = str2bool(
301 context.visual.show_version = str2bool(
302 rc_config.get('rhodecode_show_version'))
302 rc_config.get('rhodecode_show_version'))
303 context.visual.use_gravatar = str2bool(
303 context.visual.use_gravatar = str2bool(
304 rc_config.get('rhodecode_use_gravatar'))
304 rc_config.get('rhodecode_use_gravatar'))
305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
306 context.visual.default_renderer = rc_config.get(
306 context.visual.default_renderer = rc_config.get(
307 'rhodecode_markup_renderer', 'rst')
307 'rhodecode_markup_renderer', 'rst')
308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
309 context.visual.rhodecode_support_url = \
309 context.visual.rhodecode_support_url = \
310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
311
311
312 context.pre_code = rc_config.get('rhodecode_pre_code')
312 context.pre_code = rc_config.get('rhodecode_pre_code')
313 context.post_code = rc_config.get('rhodecode_post_code')
313 context.post_code = rc_config.get('rhodecode_post_code')
314 context.rhodecode_name = rc_config.get('rhodecode_title')
314 context.rhodecode_name = rc_config.get('rhodecode_title')
315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
316 # if we have specified default_encoding in the request, it has more
316 # if we have specified default_encoding in the request, it has more
317 # priority
317 # priority
318 if request.GET.get('default_encoding'):
318 if request.GET.get('default_encoding'):
319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
321
321
322 # INI stored
322 # INI stored
323 context.labs_active = str2bool(
323 context.labs_active = str2bool(
324 config.get('labs_settings_active', 'false'))
324 config.get('labs_settings_active', 'false'))
325 context.visual.allow_repo_location_change = str2bool(
325 context.visual.allow_repo_location_change = str2bool(
326 config.get('allow_repo_location_change', True))
326 config.get('allow_repo_location_change', True))
327 context.visual.allow_custom_hooks_settings = str2bool(
327 context.visual.allow_custom_hooks_settings = str2bool(
328 config.get('allow_custom_hooks_settings', True))
328 config.get('allow_custom_hooks_settings', True))
329 context.debug_style = str2bool(config.get('debug_style', False))
329 context.debug_style = str2bool(config.get('debug_style', False))
330
330
331 context.rhodecode_instanceid = config.get('instance_id')
331 context.rhodecode_instanceid = config.get('instance_id')
332
332
333 context.visual.cut_off_limit_diff = safe_int(
334 config.get('cut_off_limit_diff'))
335 context.visual.cut_off_limit_file = safe_int(
336 config.get('cut_off_limit_file'))
337
333 # AppEnlight
338 # AppEnlight
334 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
339 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
335 context.appenlight_api_public_key = config.get(
340 context.appenlight_api_public_key = config.get(
336 'appenlight.api_public_key', '')
341 'appenlight.api_public_key', '')
337 context.appenlight_server_url = config.get('appenlight.server_url', '')
342 context.appenlight_server_url = config.get('appenlight.server_url', '')
338
343
339 # JS template context
344 # JS template context
340 context.template_context = {
345 context.template_context = {
341 'repo_name': None,
346 'repo_name': None,
342 'repo_type': None,
347 'repo_type': None,
343 'repo_landing_commit': None,
348 'repo_landing_commit': None,
344 'rhodecode_user': {
349 'rhodecode_user': {
345 'username': None,
350 'username': None,
346 'email': None,
351 'email': None,
347 'notification_status': False
352 'notification_status': False
348 },
353 },
349 'visual': {
354 'visual': {
350 'default_renderer': None
355 'default_renderer': None
351 },
356 },
352 'commit_data': {
357 'commit_data': {
353 'commit_id': None
358 'commit_id': None
354 },
359 },
355 'pull_request_data': {'pull_request_id': None},
360 'pull_request_data': {'pull_request_id': None},
356 'timeago': {
361 'timeago': {
357 'refresh_time': 120 * 1000,
362 'refresh_time': 120 * 1000,
358 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
363 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
359 },
364 },
360 'pylons_dispatch': {
365 'pylons_dispatch': {
361 # 'controller': request.environ['pylons.routes_dict']['controller'],
366 # 'controller': request.environ['pylons.routes_dict']['controller'],
362 # 'action': request.environ['pylons.routes_dict']['action'],
367 # 'action': request.environ['pylons.routes_dict']['action'],
363 },
368 },
364 'pyramid_dispatch': {
369 'pyramid_dispatch': {
365
370
366 },
371 },
367 'extra': {'plugins': {}}
372 'extra': {'plugins': {}}
368 }
373 }
369 # END CONFIG VARS
374 # END CONFIG VARS
370
375
371 # TODO: This dosn't work when called from pylons compatibility tween.
376 # TODO: This dosn't work when called from pylons compatibility tween.
372 # Fix this and remove it from base controller.
377 # Fix this and remove it from base controller.
373 # context.repo_name = get_repo_slug(request) # can be empty
378 # context.repo_name = get_repo_slug(request) # can be empty
374
379
375 diffmode = 'sideside'
380 diffmode = 'sideside'
376 if request.GET.get('diffmode'):
381 if request.GET.get('diffmode'):
377 if request.GET['diffmode'] == 'unified':
382 if request.GET['diffmode'] == 'unified':
378 diffmode = 'unified'
383 diffmode = 'unified'
379 elif request.session.get('diffmode'):
384 elif request.session.get('diffmode'):
380 diffmode = request.session['diffmode']
385 diffmode = request.session['diffmode']
381
386
382 context.diffmode = diffmode
387 context.diffmode = diffmode
383
388
384 if request.session.get('diffmode') != diffmode:
389 if request.session.get('diffmode') != diffmode:
385 request.session['diffmode'] = diffmode
390 request.session['diffmode'] = diffmode
386
391
387 context.csrf_token = auth.get_csrf_token()
392 context.csrf_token = auth.get_csrf_token()
388 context.backends = rhodecode.BACKENDS.keys()
393 context.backends = rhodecode.BACKENDS.keys()
389 context.backends.sort()
394 context.backends.sort()
390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
395 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
391 if attach_to_request:
396 if attach_to_request:
392 request.call_context = context
397 request.call_context = context
393 else:
398 else:
394 context.pyramid_request = pyramid.threadlocal.get_current_request()
399 context.pyramid_request = pyramid.threadlocal.get_current_request()
395
400
396
401
397
402
398 def get_auth_user(environ):
403 def get_auth_user(environ):
399 ip_addr = get_ip_addr(environ)
404 ip_addr = get_ip_addr(environ)
400 # make sure that we update permissions each time we call controller
405 # make sure that we update permissions each time we call controller
401 _auth_token = (request.GET.get('auth_token', '') or
406 _auth_token = (request.GET.get('auth_token', '') or
402 request.GET.get('api_key', ''))
407 request.GET.get('api_key', ''))
403
408
404 if _auth_token:
409 if _auth_token:
405 # when using API_KEY we assume user exists, and
410 # when using API_KEY we assume user exists, and
406 # doesn't need auth based on cookies.
411 # doesn't need auth based on cookies.
407 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
412 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
408 authenticated = False
413 authenticated = False
409 else:
414 else:
410 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
415 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
411 try:
416 try:
412 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
417 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
413 ip_addr=ip_addr)
418 ip_addr=ip_addr)
414 except UserCreationError as e:
419 except UserCreationError as e:
415 h.flash(e, 'error')
420 h.flash(e, 'error')
416 # container auth or other auth functions that create users
421 # container auth or other auth functions that create users
417 # on the fly can throw this exception signaling that there's
422 # on the fly can throw this exception signaling that there's
418 # issue with user creation, explanation should be provided
423 # issue with user creation, explanation should be provided
419 # in Exception itself. We then create a simple blank
424 # in Exception itself. We then create a simple blank
420 # AuthUser
425 # AuthUser
421 auth_user = AuthUser(ip_addr=ip_addr)
426 auth_user = AuthUser(ip_addr=ip_addr)
422
427
423 if password_changed(auth_user, session):
428 if password_changed(auth_user, session):
424 session.invalidate()
429 session.invalidate()
425 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
430 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
426 auth_user = AuthUser(ip_addr=ip_addr)
431 auth_user = AuthUser(ip_addr=ip_addr)
427
432
428 authenticated = cookie_store.get('is_authenticated')
433 authenticated = cookie_store.get('is_authenticated')
429
434
430 if not auth_user.is_authenticated and auth_user.is_user_object:
435 if not auth_user.is_authenticated and auth_user.is_user_object:
431 # user is not authenticated and not empty
436 # user is not authenticated and not empty
432 auth_user.set_authenticated(authenticated)
437 auth_user.set_authenticated(authenticated)
433
438
434 return auth_user
439 return auth_user
435
440
436
441
437 class BaseController(WSGIController):
442 class BaseController(WSGIController):
438
443
439 def __before__(self):
444 def __before__(self):
440 """
445 """
441 __before__ is called before controller methods and after __call__
446 __before__ is called before controller methods and after __call__
442 """
447 """
443 # on each call propagate settings calls into global settings.
448 # on each call propagate settings calls into global settings.
444 set_rhodecode_config(config)
449 set_rhodecode_config(config)
445 attach_context_attributes(c, request, c.rhodecode_user.user_id)
450 attach_context_attributes(c, request, c.rhodecode_user.user_id)
446
451
447 # TODO: Remove this when fixed in attach_context_attributes()
452 # TODO: Remove this when fixed in attach_context_attributes()
448 c.repo_name = get_repo_slug(request) # can be empty
453 c.repo_name = get_repo_slug(request) # can be empty
449
454
450 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
455 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
451 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
456 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
452 self.sa = meta.Session
457 self.sa = meta.Session
453 self.scm_model = ScmModel(self.sa)
458 self.scm_model = ScmModel(self.sa)
454
459
455 # set user language
460 # set user language
456 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
461 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
457 if user_lang:
462 if user_lang:
458 translation.set_lang(user_lang)
463 translation.set_lang(user_lang)
459 log.debug('set language to %s for user %s',
464 log.debug('set language to %s for user %s',
460 user_lang, self._rhodecode_user)
465 user_lang, self._rhodecode_user)
461
466
462 def _dispatch_redirect(self, with_url, environ, start_response):
467 def _dispatch_redirect(self, with_url, environ, start_response):
463 resp = HTTPFound(with_url)
468 resp = HTTPFound(with_url)
464 environ['SCRIPT_NAME'] = '' # handle prefix middleware
469 environ['SCRIPT_NAME'] = '' # handle prefix middleware
465 environ['PATH_INFO'] = with_url
470 environ['PATH_INFO'] = with_url
466 return resp(environ, start_response)
471 return resp(environ, start_response)
467
472
468 def __call__(self, environ, start_response):
473 def __call__(self, environ, start_response):
469 """Invoke the Controller"""
474 """Invoke the Controller"""
470 # WSGIController.__call__ dispatches to the Controller method
475 # WSGIController.__call__ dispatches to the Controller method
471 # the request is routed to. This routing information is
476 # the request is routed to. This routing information is
472 # available in environ['pylons.routes_dict']
477 # available in environ['pylons.routes_dict']
473 from rhodecode.lib import helpers as h
478 from rhodecode.lib import helpers as h
474
479
475 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
480 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
476 if environ.get('debugtoolbar.wants_pylons_context', False):
481 if environ.get('debugtoolbar.wants_pylons_context', False):
477 environ['debugtoolbar.pylons_context'] = c._current_obj()
482 environ['debugtoolbar.pylons_context'] = c._current_obj()
478
483
479 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
484 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
480 environ['pylons.routes_dict']['action']])
485 environ['pylons.routes_dict']['action']])
481
486
482 self.rc_config = SettingsModel().get_all_settings(cache=True)
487 self.rc_config = SettingsModel().get_all_settings(cache=True)
483 self.ip_addr = get_ip_addr(environ)
488 self.ip_addr = get_ip_addr(environ)
484
489
485 # The rhodecode auth user is looked up and passed through the
490 # The rhodecode auth user is looked up and passed through the
486 # environ by the pylons compatibility tween in pyramid.
491 # environ by the pylons compatibility tween in pyramid.
487 # So we can just grab it from there.
492 # So we can just grab it from there.
488 auth_user = environ['rc_auth_user']
493 auth_user = environ['rc_auth_user']
489
494
490 # set globals for auth user
495 # set globals for auth user
491 request.user = auth_user
496 request.user = auth_user
492 c.rhodecode_user = self._rhodecode_user = auth_user
497 c.rhodecode_user = self._rhodecode_user = auth_user
493
498
494 log.info('IP: %s User: %s accessed %s [%s]' % (
499 log.info('IP: %s User: %s accessed %s [%s]' % (
495 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
500 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
496 _route_name)
501 _route_name)
497 )
502 )
498
503
499 user_obj = auth_user.get_instance()
504 user_obj = auth_user.get_instance()
500 if user_obj and user_obj.user_data.get('force_password_change'):
505 if user_obj and user_obj.user_data.get('force_password_change'):
501 h.flash('You are required to change your password', 'warning',
506 h.flash('You are required to change your password', 'warning',
502 ignore_duplicate=True)
507 ignore_duplicate=True)
503 return self._dispatch_redirect(
508 return self._dispatch_redirect(
504 url('my_account_password'), environ, start_response)
509 url('my_account_password'), environ, start_response)
505
510
506 return WSGIController.__call__(self, environ, start_response)
511 return WSGIController.__call__(self, environ, start_response)
507
512
508
513
509 class BaseRepoController(BaseController):
514 class BaseRepoController(BaseController):
510 """
515 """
511 Base class for controllers responsible for loading all needed data for
516 Base class for controllers responsible for loading all needed data for
512 repository loaded items are
517 repository loaded items are
513
518
514 c.rhodecode_repo: instance of scm repository
519 c.rhodecode_repo: instance of scm repository
515 c.rhodecode_db_repo: instance of db
520 c.rhodecode_db_repo: instance of db
516 c.repository_requirements_missing: shows that repository specific data
521 c.repository_requirements_missing: shows that repository specific data
517 could not be displayed due to the missing requirements
522 could not be displayed due to the missing requirements
518 c.repository_pull_requests: show number of open pull requests
523 c.repository_pull_requests: show number of open pull requests
519 """
524 """
520
525
521 def __before__(self):
526 def __before__(self):
522 super(BaseRepoController, self).__before__()
527 super(BaseRepoController, self).__before__()
523 if c.repo_name: # extracted from routes
528 if c.repo_name: # extracted from routes
524 db_repo = Repository.get_by_repo_name(c.repo_name)
529 db_repo = Repository.get_by_repo_name(c.repo_name)
525 if not db_repo:
530 if not db_repo:
526 return
531 return
527
532
528 log.debug(
533 log.debug(
529 'Found repository in database %s with state `%s`',
534 'Found repository in database %s with state `%s`',
530 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
535 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
531 route = getattr(request.environ.get('routes.route'), 'name', '')
536 route = getattr(request.environ.get('routes.route'), 'name', '')
532
537
533 # allow to delete repos that are somehow damages in filesystem
538 # allow to delete repos that are somehow damages in filesystem
534 if route in ['delete_repo']:
539 if route in ['delete_repo']:
535 return
540 return
536
541
537 if db_repo.repo_state in [Repository.STATE_PENDING]:
542 if db_repo.repo_state in [Repository.STATE_PENDING]:
538 if route in ['repo_creating_home']:
543 if route in ['repo_creating_home']:
539 return
544 return
540 check_url = url('repo_creating_home', repo_name=c.repo_name)
545 check_url = url('repo_creating_home', repo_name=c.repo_name)
541 return redirect(check_url)
546 return redirect(check_url)
542
547
543 self.rhodecode_db_repo = db_repo
548 self.rhodecode_db_repo = db_repo
544
549
545 missing_requirements = False
550 missing_requirements = False
546 try:
551 try:
547 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
552 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
548 except RepositoryRequirementError as e:
553 except RepositoryRequirementError as e:
549 missing_requirements = True
554 missing_requirements = True
550 self._handle_missing_requirements(e)
555 self._handle_missing_requirements(e)
551
556
552 if self.rhodecode_repo is None and not missing_requirements:
557 if self.rhodecode_repo is None and not missing_requirements:
553 log.error('%s this repository is present in database but it '
558 log.error('%s this repository is present in database but it '
554 'cannot be created as an scm instance', c.repo_name)
559 'cannot be created as an scm instance', c.repo_name)
555
560
556 h.flash(_(
561 h.flash(_(
557 "The repository at %(repo_name)s cannot be located.") %
562 "The repository at %(repo_name)s cannot be located.") %
558 {'repo_name': c.repo_name},
563 {'repo_name': c.repo_name},
559 category='error', ignore_duplicate=True)
564 category='error', ignore_duplicate=True)
560 redirect(h.route_path('home'))
565 redirect(h.route_path('home'))
561
566
562 # update last change according to VCS data
567 # update last change according to VCS data
563 if not missing_requirements:
568 if not missing_requirements:
564 commit = db_repo.get_commit(
569 commit = db_repo.get_commit(
565 pre_load=["author", "date", "message", "parents"])
570 pre_load=["author", "date", "message", "parents"])
566 db_repo.update_commit_cache(commit)
571 db_repo.update_commit_cache(commit)
567
572
568 # Prepare context
573 # Prepare context
569 c.rhodecode_db_repo = db_repo
574 c.rhodecode_db_repo = db_repo
570 c.rhodecode_repo = self.rhodecode_repo
575 c.rhodecode_repo = self.rhodecode_repo
571 c.repository_requirements_missing = missing_requirements
576 c.repository_requirements_missing = missing_requirements
572
577
573 self._update_global_counters(self.scm_model, db_repo)
578 self._update_global_counters(self.scm_model, db_repo)
574
579
575 def _update_global_counters(self, scm_model, db_repo):
580 def _update_global_counters(self, scm_model, db_repo):
576 """
581 """
577 Base variables that are exposed to every page of repository
582 Base variables that are exposed to every page of repository
578 """
583 """
579 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
584 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
580
585
581 def _handle_missing_requirements(self, error):
586 def _handle_missing_requirements(self, error):
582 self.rhodecode_repo = None
587 self.rhodecode_repo = None
583 log.error(
588 log.error(
584 'Requirements are missing for repository %s: %s',
589 'Requirements are missing for repository %s: %s',
585 c.repo_name, error.message)
590 c.repo_name, error.message)
586
591
587 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
592 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
588 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
593 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
589 settings_update_url = url('repo', repo_name=c.repo_name)
594 settings_update_url = url('repo', repo_name=c.repo_name)
590 path = request.path
595 path = request.path
591 should_redirect = (
596 should_redirect = (
592 path not in (summary_url, settings_update_url)
597 path not in (summary_url, settings_update_url)
593 and '/settings' not in path or path == statistics_url
598 and '/settings' not in path or path == statistics_url
594 )
599 )
595 if should_redirect:
600 if should_redirect:
596 redirect(summary_url)
601 redirect(summary_url)
@@ -1,666 +1,665 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry, get_current_request
32 from pyramid.threadlocal import get_current_registry, get_current_request
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.channelstream import channelstream_request
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
87 # group by versions, and count until, and display objects
88
88
89 comment_groups = collections.defaultdict(list)
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
92
93 def yield_comments(pos):
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
94 for co in comment_groups[pos]:
95 yield co
95 yield co
96
96
97 comment_versions = collections.defaultdict(
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
104 if prev_prvid == -1:
105 prev_prvid = prvid
105 prev_prvid = prvid
106
106
107 for co in yield_comments(prvid):
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
108 comment_versions[prvid]['at'].append(co)
109
109
110 # save until
110 # save until
111 current = comment_versions[prvid]['at']
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
114 comment_versions[prvid]['until'].extend(cur_until)
115
115
116 # save outdated
116 # save outdated
117 if inline:
117 if inline:
118 outdated = [x for x in cur_until
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
119 if x.outdated_at_version(show_version)]
120 else:
120 else:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
123 display = [x for x in cur_until if x not in outdated]
124
124
125 comment_versions[prvid]['outdated'] = outdated
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
126 comment_versions[prvid]['display'] = display
127
127
128 prev_prvid = prvid
128 prev_prvid = prvid
129
129
130 return comment_versions
130 return comment_versions
131
131
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
133
133
134 todos = Session().query(ChangesetComment) \
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO)
138 == ChangesetComment.COMMENT_TYPE_TODO)
139
139
140 if not show_outdated:
140 if not show_outdated:
141 todos = todos.filter(
141 todos = todos.filter(
142 coalesce(ChangesetComment.display_state, '') !=
142 coalesce(ChangesetComment.display_state, '') !=
143 ChangesetComment.COMMENT_OUTDATED)
143 ChangesetComment.COMMENT_OUTDATED)
144
144
145 todos = todos.all()
145 todos = todos.all()
146
146
147 return todos
147 return todos
148
148
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
150
150
151 todos = Session().query(ChangesetComment) \
151 todos = Session().query(ChangesetComment) \
152 .filter(ChangesetComment.revision == commit_id) \
152 .filter(ChangesetComment.revision == commit_id) \
153 .filter(ChangesetComment.resolved_by == None) \
153 .filter(ChangesetComment.resolved_by == None) \
154 .filter(ChangesetComment.comment_type
154 .filter(ChangesetComment.comment_type
155 == ChangesetComment.COMMENT_TYPE_TODO)
155 == ChangesetComment.COMMENT_TYPE_TODO)
156
156
157 if not show_outdated:
157 if not show_outdated:
158 todos = todos.filter(
158 todos = todos.filter(
159 coalesce(ChangesetComment.display_state, '') !=
159 coalesce(ChangesetComment.display_state, '') !=
160 ChangesetComment.COMMENT_OUTDATED)
160 ChangesetComment.COMMENT_OUTDATED)
161
161
162 todos = todos.all()
162 todos = todos.all()
163
163
164 return todos
164 return todos
165
165
166 def _log_audit_action(self, action, action_data, user, comment):
166 def _log_audit_action(self, action, action_data, user, comment):
167 audit_logger.store(
167 audit_logger.store(
168 action=action,
168 action=action,
169 action_data=action_data,
169 action_data=action_data,
170 user=user,
170 user=user,
171 repo=comment.repo)
171 repo=comment.repo)
172
172
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
174 f_path=None, line_no=None, status_change=None,
174 f_path=None, line_no=None, status_change=None,
175 status_change_type=None, comment_type=None,
175 status_change_type=None, comment_type=None,
176 resolves_comment_id=None, closing_pr=False, send_email=True,
176 resolves_comment_id=None, closing_pr=False, send_email=True,
177 renderer=None):
177 renderer=None):
178 """
178 """
179 Creates new comment for commit or pull request.
179 Creates new comment for commit or pull request.
180 IF status_change is not none this comment is associated with a
180 IF status_change is not none this comment is associated with a
181 status change of commit or commit associated with pull request
181 status change of commit or commit associated with pull request
182
182
183 :param text:
183 :param text:
184 :param repo:
184 :param repo:
185 :param user:
185 :param user:
186 :param commit_id:
186 :param commit_id:
187 :param pull_request:
187 :param pull_request:
188 :param f_path:
188 :param f_path:
189 :param line_no:
189 :param line_no:
190 :param status_change: Label for status change
190 :param status_change: Label for status change
191 :param comment_type: Type of comment
191 :param comment_type: Type of comment
192 :param status_change_type: type of status change
192 :param status_change_type: type of status change
193 :param closing_pr:
193 :param closing_pr:
194 :param send_email:
194 :param send_email:
195 :param renderer: pick renderer for this comment
195 :param renderer: pick renderer for this comment
196 """
196 """
197 if not text:
197 if not text:
198 log.warning('Missing text for comment, skipping...')
198 log.warning('Missing text for comment, skipping...')
199 return
199 return
200
200
201 if not renderer:
201 if not renderer:
202 renderer = self._get_renderer()
202 renderer = self._get_renderer()
203
203
204 repo = self._get_repo(repo)
204 repo = self._get_repo(repo)
205 user = self._get_user(user)
205 user = self._get_user(user)
206
206
207 schema = comment_schema.CommentSchema()
207 schema = comment_schema.CommentSchema()
208 validated_kwargs = schema.deserialize(dict(
208 validated_kwargs = schema.deserialize(dict(
209 comment_body=text,
209 comment_body=text,
210 comment_type=comment_type,
210 comment_type=comment_type,
211 comment_file=f_path,
211 comment_file=f_path,
212 comment_line=line_no,
212 comment_line=line_no,
213 renderer_type=renderer,
213 renderer_type=renderer,
214 status_change=status_change_type,
214 status_change=status_change_type,
215 resolves_comment_id=resolves_comment_id,
215 resolves_comment_id=resolves_comment_id,
216 repo=repo.repo_id,
216 repo=repo.repo_id,
217 user=user.user_id,
217 user=user.user_id,
218 ))
218 ))
219
219
220 comment = ChangesetComment()
220 comment = ChangesetComment()
221 comment.renderer = validated_kwargs['renderer_type']
221 comment.renderer = validated_kwargs['renderer_type']
222 comment.text = validated_kwargs['comment_body']
222 comment.text = validated_kwargs['comment_body']
223 comment.f_path = validated_kwargs['comment_file']
223 comment.f_path = validated_kwargs['comment_file']
224 comment.line_no = validated_kwargs['comment_line']
224 comment.line_no = validated_kwargs['comment_line']
225 comment.comment_type = validated_kwargs['comment_type']
225 comment.comment_type = validated_kwargs['comment_type']
226
226
227 comment.repo = repo
227 comment.repo = repo
228 comment.author = user
228 comment.author = user
229 comment.resolved_comment = self.__get_commit_comment(
229 comment.resolved_comment = self.__get_commit_comment(
230 validated_kwargs['resolves_comment_id'])
230 validated_kwargs['resolves_comment_id'])
231
231
232 pull_request_id = pull_request
232 pull_request_id = pull_request
233
233
234 commit_obj = None
234 commit_obj = None
235 pull_request_obj = None
235 pull_request_obj = None
236
236
237 if commit_id:
237 if commit_id:
238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
239 # do a lookup, so we don't pass something bad here
239 # do a lookup, so we don't pass something bad here
240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
241 comment.revision = commit_obj.raw_id
241 comment.revision = commit_obj.raw_id
242
242
243 elif pull_request_id:
243 elif pull_request_id:
244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
245 pull_request_obj = self.__get_pull_request(pull_request_id)
245 pull_request_obj = self.__get_pull_request(pull_request_id)
246 comment.pull_request = pull_request_obj
246 comment.pull_request = pull_request_obj
247 else:
247 else:
248 raise Exception('Please specify commit or pull_request_id')
248 raise Exception('Please specify commit or pull_request_id')
249
249
250 Session().add(comment)
250 Session().add(comment)
251 Session().flush()
251 Session().flush()
252 kwargs = {
252 kwargs = {
253 'user': user,
253 'user': user,
254 'renderer_type': renderer,
254 'renderer_type': renderer,
255 'repo_name': repo.repo_name,
255 'repo_name': repo.repo_name,
256 'status_change': status_change,
256 'status_change': status_change,
257 'status_change_type': status_change_type,
257 'status_change_type': status_change_type,
258 'comment_body': text,
258 'comment_body': text,
259 'comment_file': f_path,
259 'comment_file': f_path,
260 'comment_line': line_no,
260 'comment_line': line_no,
261 'comment_type': comment_type or 'note'
261 'comment_type': comment_type or 'note'
262 }
262 }
263
263
264 if commit_obj:
264 if commit_obj:
265 recipients = ChangesetComment.get_users(
265 recipients = ChangesetComment.get_users(
266 revision=commit_obj.raw_id)
266 revision=commit_obj.raw_id)
267 # add commit author if it's in RhodeCode system
267 # add commit author if it's in RhodeCode system
268 cs_author = User.get_from_cs_author(commit_obj.author)
268 cs_author = User.get_from_cs_author(commit_obj.author)
269 if not cs_author:
269 if not cs_author:
270 # use repo owner if we cannot extract the author correctly
270 # use repo owner if we cannot extract the author correctly
271 cs_author = repo.user
271 cs_author = repo.user
272 recipients += [cs_author]
272 recipients += [cs_author]
273
273
274 commit_comment_url = self.get_url(comment)
274 commit_comment_url = self.get_url(comment)
275
275
276 target_repo_url = h.link_to(
276 target_repo_url = h.link_to(
277 repo.repo_name,
277 repo.repo_name,
278 h.route_url('repo_summary', repo_name=repo.repo_name))
278 h.route_url('repo_summary', repo_name=repo.repo_name))
279
279
280 # commit specifics
280 # commit specifics
281 kwargs.update({
281 kwargs.update({
282 'commit': commit_obj,
282 'commit': commit_obj,
283 'commit_message': commit_obj.message,
283 'commit_message': commit_obj.message,
284 'commit_target_repo': target_repo_url,
284 'commit_target_repo': target_repo_url,
285 'commit_comment_url': commit_comment_url,
285 'commit_comment_url': commit_comment_url,
286 })
286 })
287
287
288 elif pull_request_obj:
288 elif pull_request_obj:
289 # get the current participants of this pull request
289 # get the current participants of this pull request
290 recipients = ChangesetComment.get_users(
290 recipients = ChangesetComment.get_users(
291 pull_request_id=pull_request_obj.pull_request_id)
291 pull_request_id=pull_request_obj.pull_request_id)
292 # add pull request author
292 # add pull request author
293 recipients += [pull_request_obj.author]
293 recipients += [pull_request_obj.author]
294
294
295 # add the reviewers to notification
295 # add the reviewers to notification
296 recipients += [x.user for x in pull_request_obj.reviewers]
296 recipients += [x.user for x in pull_request_obj.reviewers]
297
297
298 pr_target_repo = pull_request_obj.target_repo
298 pr_target_repo = pull_request_obj.target_repo
299 pr_source_repo = pull_request_obj.source_repo
299 pr_source_repo = pull_request_obj.source_repo
300
300
301 pr_comment_url = h.url(
301 pr_comment_url = h.url(
302 'pullrequest_show',
302 'pullrequest_show',
303 repo_name=pr_target_repo.repo_name,
303 repo_name=pr_target_repo.repo_name,
304 pull_request_id=pull_request_obj.pull_request_id,
304 pull_request_id=pull_request_obj.pull_request_id,
305 anchor='comment-%s' % comment.comment_id,
305 anchor='comment-%s' % comment.comment_id,
306 qualified=True,)
306 qualified=True,)
307
307
308 # set some variables for email notification
308 # set some variables for email notification
309 pr_target_repo_url = h.route_url(
309 pr_target_repo_url = h.route_url(
310 'repo_summary', repo_name=pr_target_repo.repo_name)
310 'repo_summary', repo_name=pr_target_repo.repo_name)
311
311
312 pr_source_repo_url = h.route_url(
312 pr_source_repo_url = h.route_url(
313 'repo_summary', repo_name=pr_source_repo.repo_name)
313 'repo_summary', repo_name=pr_source_repo.repo_name)
314
314
315 # pull request specifics
315 # pull request specifics
316 kwargs.update({
316 kwargs.update({
317 'pull_request': pull_request_obj,
317 'pull_request': pull_request_obj,
318 'pr_id': pull_request_obj.pull_request_id,
318 'pr_id': pull_request_obj.pull_request_id,
319 'pr_target_repo': pr_target_repo,
319 'pr_target_repo': pr_target_repo,
320 'pr_target_repo_url': pr_target_repo_url,
320 'pr_target_repo_url': pr_target_repo_url,
321 'pr_source_repo': pr_source_repo,
321 'pr_source_repo': pr_source_repo,
322 'pr_source_repo_url': pr_source_repo_url,
322 'pr_source_repo_url': pr_source_repo_url,
323 'pr_comment_url': pr_comment_url,
323 'pr_comment_url': pr_comment_url,
324 'pr_closing': closing_pr,
324 'pr_closing': closing_pr,
325 })
325 })
326 if send_email:
326 if send_email:
327 # pre-generate the subject for notification itself
327 # pre-generate the subject for notification itself
328 (subject,
328 (subject,
329 _h, _e, # we don't care about those
329 _h, _e, # we don't care about those
330 body_plaintext) = EmailNotificationModel().render_email(
330 body_plaintext) = EmailNotificationModel().render_email(
331 notification_type, **kwargs)
331 notification_type, **kwargs)
332
332
333 mention_recipients = set(
333 mention_recipients = set(
334 self._extract_mentions(text)).difference(recipients)
334 self._extract_mentions(text)).difference(recipients)
335
335
336 # create notification objects, and emails
336 # create notification objects, and emails
337 NotificationModel().create(
337 NotificationModel().create(
338 created_by=user,
338 created_by=user,
339 notification_subject=subject,
339 notification_subject=subject,
340 notification_body=body_plaintext,
340 notification_body=body_plaintext,
341 notification_type=notification_type,
341 notification_type=notification_type,
342 recipients=recipients,
342 recipients=recipients,
343 mention_recipients=mention_recipients,
343 mention_recipients=mention_recipients,
344 email_kwargs=kwargs,
344 email_kwargs=kwargs,
345 )
345 )
346
346
347 Session().flush()
347 Session().flush()
348 if comment.pull_request:
348 if comment.pull_request:
349 action = 'repo.pull_request.comment.create'
349 action = 'repo.pull_request.comment.create'
350 else:
350 else:
351 action = 'repo.commit.comment.create'
351 action = 'repo.commit.comment.create'
352
352
353 comment_data = comment.get_api_data()
353 comment_data = comment.get_api_data()
354 self._log_audit_action(
354 self._log_audit_action(
355 action, {'data': comment_data}, user, comment)
355 action, {'data': comment_data}, user, comment)
356
356
357 registry = get_current_registry()
357 registry = get_current_registry()
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 msg_url = ''
360 msg_url = ''
361 if commit_obj:
361 if commit_obj:
362 msg_url = commit_comment_url
362 msg_url = commit_comment_url
363 repo_name = repo.repo_name
363 repo_name = repo.repo_name
364 elif pull_request_obj:
364 elif pull_request_obj:
365 msg_url = pr_comment_url
365 msg_url = pr_comment_url
366 repo_name = pr_target_repo.repo_name
366 repo_name = pr_target_repo.repo_name
367
367
368 if channelstream_config.get('enabled'):
368 if channelstream_config.get('enabled'):
369 message = '<strong>{}</strong> {} - ' \
369 message = '<strong>{}</strong> {} - ' \
370 '<a onclick="window.location=\'{}\';' \
370 '<a onclick="window.location=\'{}\';' \
371 'window.location.reload()">' \
371 'window.location.reload()">' \
372 '<strong>{}</strong></a>'
372 '<strong>{}</strong></a>'
373 message = message.format(
373 message = message.format(
374 user.username, _('made a comment'), msg_url,
374 user.username, _('made a comment'), msg_url,
375 _('Show it now'))
375 _('Show it now'))
376 channel = '/repo${}$/pr/{}'.format(
376 channel = '/repo${}$/pr/{}'.format(
377 repo_name,
377 repo_name,
378 pull_request_id
378 pull_request_id
379 )
379 )
380 payload = {
380 payload = {
381 'type': 'message',
381 'type': 'message',
382 'timestamp': datetime.utcnow(),
382 'timestamp': datetime.utcnow(),
383 'user': 'system',
383 'user': 'system',
384 'exclude_users': [user.username],
384 'exclude_users': [user.username],
385 'channel': channel,
385 'channel': channel,
386 'message': {
386 'message': {
387 'message': message,
387 'message': message,
388 'level': 'info',
388 'level': 'info',
389 'topic': '/notifications'
389 'topic': '/notifications'
390 }
390 }
391 }
391 }
392 channelstream_request(channelstream_config, [payload],
392 channelstream_request(channelstream_config, [payload],
393 '/message', raise_exc=False)
393 '/message', raise_exc=False)
394
394
395 return comment
395 return comment
396
396
397 def delete(self, comment, user):
397 def delete(self, comment, user):
398 """
398 """
399 Deletes given comment
399 Deletes given comment
400 """
400 """
401 comment = self.__get_commit_comment(comment)
401 comment = self.__get_commit_comment(comment)
402 old_data = comment.get_api_data()
402 old_data = comment.get_api_data()
403 Session().delete(comment)
403 Session().delete(comment)
404
404
405 if comment.pull_request:
405 if comment.pull_request:
406 action = 'repo.pull_request.comment.delete'
406 action = 'repo.pull_request.comment.delete'
407 else:
407 else:
408 action = 'repo.commit.comment.delete'
408 action = 'repo.commit.comment.delete'
409
409
410 self._log_audit_action(
410 self._log_audit_action(
411 action, {'old_data': old_data}, user, comment)
411 action, {'old_data': old_data}, user, comment)
412
412
413 return comment
413 return comment
414
414
415 def get_all_comments(self, repo_id, revision=None, pull_request=None):
415 def get_all_comments(self, repo_id, revision=None, pull_request=None):
416 q = ChangesetComment.query()\
416 q = ChangesetComment.query()\
417 .filter(ChangesetComment.repo_id == repo_id)
417 .filter(ChangesetComment.repo_id == repo_id)
418 if revision:
418 if revision:
419 q = q.filter(ChangesetComment.revision == revision)
419 q = q.filter(ChangesetComment.revision == revision)
420 elif pull_request:
420 elif pull_request:
421 pull_request = self.__get_pull_request(pull_request)
421 pull_request = self.__get_pull_request(pull_request)
422 q = q.filter(ChangesetComment.pull_request == pull_request)
422 q = q.filter(ChangesetComment.pull_request == pull_request)
423 else:
423 else:
424 raise Exception('Please specify commit or pull_request')
424 raise Exception('Please specify commit or pull_request')
425 q = q.order_by(ChangesetComment.created_on)
425 q = q.order_by(ChangesetComment.created_on)
426 return q.all()
426 return q.all()
427
427
428 def get_url(self, comment, request=None, permalink=False):
428 def get_url(self, comment, request=None, permalink=False):
429 if not request:
429 if not request:
430 request = get_current_request()
430 request = get_current_request()
431
431
432 comment = self.__get_commit_comment(comment)
432 comment = self.__get_commit_comment(comment)
433 if comment.pull_request:
433 if comment.pull_request:
434 pull_request = comment.pull_request
434 pull_request = comment.pull_request
435 if permalink:
435 if permalink:
436 return request.route_url(
436 return request.route_url(
437 'pull_requests_global',
437 'pull_requests_global',
438 pull_request_id=pull_request.pull_request_id,
438 pull_request_id=pull_request.pull_request_id,
439 _anchor='comment-%s' % comment.comment_id)
439 _anchor='comment-%s' % comment.comment_id)
440 else:
440 else:
441 return request.route_url(
441 return request.route_url('pullrequest_show',
442 'pullrequest_show',
443 repo_name=safe_str(pull_request.target_repo.repo_name),
442 repo_name=safe_str(pull_request.target_repo.repo_name),
444 pull_request_id=pull_request.pull_request_id,
443 pull_request_id=pull_request.pull_request_id,
445 _anchor='comment-%s' % comment.comment_id)
444 _anchor='comment-%s' % comment.comment_id)
446
445
447 else:
446 else:
448 repo = comment.repo
447 repo = comment.repo
449 commit_id = comment.revision
448 commit_id = comment.revision
450
449
451 if permalink:
450 if permalink:
452 return request.route_url(
451 return request.route_url(
453 'repo_commit', repo_name=safe_str(repo.repo_id),
452 'repo_commit', repo_name=safe_str(repo.repo_id),
454 commit_id=commit_id,
453 commit_id=commit_id,
455 _anchor='comment-%s' % comment.comment_id)
454 _anchor='comment-%s' % comment.comment_id)
456
455
457 else:
456 else:
458 return request.route_url(
457 return request.route_url(
459 'repo_commit', repo_name=safe_str(repo.repo_name),
458 'repo_commit', repo_name=safe_str(repo.repo_name),
460 commit_id=commit_id,
459 commit_id=commit_id,
461 _anchor='comment-%s' % comment.comment_id)
460 _anchor='comment-%s' % comment.comment_id)
462
461
463 def get_comments(self, repo_id, revision=None, pull_request=None):
462 def get_comments(self, repo_id, revision=None, pull_request=None):
464 """
463 """
465 Gets main comments based on revision or pull_request_id
464 Gets main comments based on revision or pull_request_id
466
465
467 :param repo_id:
466 :param repo_id:
468 :param revision:
467 :param revision:
469 :param pull_request:
468 :param pull_request:
470 """
469 """
471
470
472 q = ChangesetComment.query()\
471 q = ChangesetComment.query()\
473 .filter(ChangesetComment.repo_id == repo_id)\
472 .filter(ChangesetComment.repo_id == repo_id)\
474 .filter(ChangesetComment.line_no == None)\
473 .filter(ChangesetComment.line_no == None)\
475 .filter(ChangesetComment.f_path == None)
474 .filter(ChangesetComment.f_path == None)
476 if revision:
475 if revision:
477 q = q.filter(ChangesetComment.revision == revision)
476 q = q.filter(ChangesetComment.revision == revision)
478 elif pull_request:
477 elif pull_request:
479 pull_request = self.__get_pull_request(pull_request)
478 pull_request = self.__get_pull_request(pull_request)
480 q = q.filter(ChangesetComment.pull_request == pull_request)
479 q = q.filter(ChangesetComment.pull_request == pull_request)
481 else:
480 else:
482 raise Exception('Please specify commit or pull_request')
481 raise Exception('Please specify commit or pull_request')
483 q = q.order_by(ChangesetComment.created_on)
482 q = q.order_by(ChangesetComment.created_on)
484 return q.all()
483 return q.all()
485
484
486 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
485 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
487 q = self._get_inline_comments_query(repo_id, revision, pull_request)
486 q = self._get_inline_comments_query(repo_id, revision, pull_request)
488 return self._group_comments_by_path_and_line_number(q)
487 return self._group_comments_by_path_and_line_number(q)
489
488
490 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
489 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
491 version=None):
490 version=None):
492 inline_cnt = 0
491 inline_cnt = 0
493 for fname, per_line_comments in inline_comments.iteritems():
492 for fname, per_line_comments in inline_comments.iteritems():
494 for lno, comments in per_line_comments.iteritems():
493 for lno, comments in per_line_comments.iteritems():
495 for comm in comments:
494 for comm in comments:
496 if not comm.outdated_at_version(version) and skip_outdated:
495 if not comm.outdated_at_version(version) and skip_outdated:
497 inline_cnt += 1
496 inline_cnt += 1
498
497
499 return inline_cnt
498 return inline_cnt
500
499
501 def get_outdated_comments(self, repo_id, pull_request):
500 def get_outdated_comments(self, repo_id, pull_request):
502 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
501 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
503 # of a pull request.
502 # of a pull request.
504 q = self._all_inline_comments_of_pull_request(pull_request)
503 q = self._all_inline_comments_of_pull_request(pull_request)
505 q = q.filter(
504 q = q.filter(
506 ChangesetComment.display_state ==
505 ChangesetComment.display_state ==
507 ChangesetComment.COMMENT_OUTDATED
506 ChangesetComment.COMMENT_OUTDATED
508 ).order_by(ChangesetComment.comment_id.asc())
507 ).order_by(ChangesetComment.comment_id.asc())
509
508
510 return self._group_comments_by_path_and_line_number(q)
509 return self._group_comments_by_path_and_line_number(q)
511
510
512 def _get_inline_comments_query(self, repo_id, revision, pull_request):
511 def _get_inline_comments_query(self, repo_id, revision, pull_request):
513 # TODO: johbo: Split this into two methods: One for PR and one for
512 # TODO: johbo: Split this into two methods: One for PR and one for
514 # commit.
513 # commit.
515 if revision:
514 if revision:
516 q = Session().query(ChangesetComment).filter(
515 q = Session().query(ChangesetComment).filter(
517 ChangesetComment.repo_id == repo_id,
516 ChangesetComment.repo_id == repo_id,
518 ChangesetComment.line_no != null(),
517 ChangesetComment.line_no != null(),
519 ChangesetComment.f_path != null(),
518 ChangesetComment.f_path != null(),
520 ChangesetComment.revision == revision)
519 ChangesetComment.revision == revision)
521
520
522 elif pull_request:
521 elif pull_request:
523 pull_request = self.__get_pull_request(pull_request)
522 pull_request = self.__get_pull_request(pull_request)
524 if not CommentsModel.use_outdated_comments(pull_request):
523 if not CommentsModel.use_outdated_comments(pull_request):
525 q = self._visible_inline_comments_of_pull_request(pull_request)
524 q = self._visible_inline_comments_of_pull_request(pull_request)
526 else:
525 else:
527 q = self._all_inline_comments_of_pull_request(pull_request)
526 q = self._all_inline_comments_of_pull_request(pull_request)
528
527
529 else:
528 else:
530 raise Exception('Please specify commit or pull_request_id')
529 raise Exception('Please specify commit or pull_request_id')
531 q = q.order_by(ChangesetComment.comment_id.asc())
530 q = q.order_by(ChangesetComment.comment_id.asc())
532 return q
531 return q
533
532
534 def _group_comments_by_path_and_line_number(self, q):
533 def _group_comments_by_path_and_line_number(self, q):
535 comments = q.all()
534 comments = q.all()
536 paths = collections.defaultdict(lambda: collections.defaultdict(list))
535 paths = collections.defaultdict(lambda: collections.defaultdict(list))
537 for co in comments:
536 for co in comments:
538 paths[co.f_path][co.line_no].append(co)
537 paths[co.f_path][co.line_no].append(co)
539 return paths
538 return paths
540
539
541 @classmethod
540 @classmethod
542 def needed_extra_diff_context(cls):
541 def needed_extra_diff_context(cls):
543 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
542 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
544
543
545 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
544 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
546 if not CommentsModel.use_outdated_comments(pull_request):
545 if not CommentsModel.use_outdated_comments(pull_request):
547 return
546 return
548
547
549 comments = self._visible_inline_comments_of_pull_request(pull_request)
548 comments = self._visible_inline_comments_of_pull_request(pull_request)
550 comments_to_outdate = comments.all()
549 comments_to_outdate = comments.all()
551
550
552 for comment in comments_to_outdate:
551 for comment in comments_to_outdate:
553 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
552 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
554
553
555 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
554 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
556 diff_line = _parse_comment_line_number(comment.line_no)
555 diff_line = _parse_comment_line_number(comment.line_no)
557
556
558 try:
557 try:
559 old_context = old_diff_proc.get_context_of_line(
558 old_context = old_diff_proc.get_context_of_line(
560 path=comment.f_path, diff_line=diff_line)
559 path=comment.f_path, diff_line=diff_line)
561 new_context = new_diff_proc.get_context_of_line(
560 new_context = new_diff_proc.get_context_of_line(
562 path=comment.f_path, diff_line=diff_line)
561 path=comment.f_path, diff_line=diff_line)
563 except (diffs.LineNotInDiffException,
562 except (diffs.LineNotInDiffException,
564 diffs.FileNotInDiffException):
563 diffs.FileNotInDiffException):
565 comment.display_state = ChangesetComment.COMMENT_OUTDATED
564 comment.display_state = ChangesetComment.COMMENT_OUTDATED
566 return
565 return
567
566
568 if old_context == new_context:
567 if old_context == new_context:
569 return
568 return
570
569
571 if self._should_relocate_diff_line(diff_line):
570 if self._should_relocate_diff_line(diff_line):
572 new_diff_lines = new_diff_proc.find_context(
571 new_diff_lines = new_diff_proc.find_context(
573 path=comment.f_path, context=old_context,
572 path=comment.f_path, context=old_context,
574 offset=self.DIFF_CONTEXT_BEFORE)
573 offset=self.DIFF_CONTEXT_BEFORE)
575 if not new_diff_lines:
574 if not new_diff_lines:
576 comment.display_state = ChangesetComment.COMMENT_OUTDATED
575 comment.display_state = ChangesetComment.COMMENT_OUTDATED
577 else:
576 else:
578 new_diff_line = self._choose_closest_diff_line(
577 new_diff_line = self._choose_closest_diff_line(
579 diff_line, new_diff_lines)
578 diff_line, new_diff_lines)
580 comment.line_no = _diff_to_comment_line_number(new_diff_line)
579 comment.line_no = _diff_to_comment_line_number(new_diff_line)
581 else:
580 else:
582 comment.display_state = ChangesetComment.COMMENT_OUTDATED
581 comment.display_state = ChangesetComment.COMMENT_OUTDATED
583
582
584 def _should_relocate_diff_line(self, diff_line):
583 def _should_relocate_diff_line(self, diff_line):
585 """
584 """
586 Checks if relocation shall be tried for the given `diff_line`.
585 Checks if relocation shall be tried for the given `diff_line`.
587
586
588 If a comment points into the first lines, then we can have a situation
587 If a comment points into the first lines, then we can have a situation
589 that after an update another line has been added on top. In this case
588 that after an update another line has been added on top. In this case
590 we would find the context still and move the comment around. This
589 we would find the context still and move the comment around. This
591 would be wrong.
590 would be wrong.
592 """
591 """
593 should_relocate = (
592 should_relocate = (
594 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
593 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
595 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
594 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
596 return should_relocate
595 return should_relocate
597
596
598 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
597 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
599 candidate = new_diff_lines[0]
598 candidate = new_diff_lines[0]
600 best_delta = _diff_line_delta(diff_line, candidate)
599 best_delta = _diff_line_delta(diff_line, candidate)
601 for new_diff_line in new_diff_lines[1:]:
600 for new_diff_line in new_diff_lines[1:]:
602 delta = _diff_line_delta(diff_line, new_diff_line)
601 delta = _diff_line_delta(diff_line, new_diff_line)
603 if delta < best_delta:
602 if delta < best_delta:
604 candidate = new_diff_line
603 candidate = new_diff_line
605 best_delta = delta
604 best_delta = delta
606 return candidate
605 return candidate
607
606
608 def _visible_inline_comments_of_pull_request(self, pull_request):
607 def _visible_inline_comments_of_pull_request(self, pull_request):
609 comments = self._all_inline_comments_of_pull_request(pull_request)
608 comments = self._all_inline_comments_of_pull_request(pull_request)
610 comments = comments.filter(
609 comments = comments.filter(
611 coalesce(ChangesetComment.display_state, '') !=
610 coalesce(ChangesetComment.display_state, '') !=
612 ChangesetComment.COMMENT_OUTDATED)
611 ChangesetComment.COMMENT_OUTDATED)
613 return comments
612 return comments
614
613
615 def _all_inline_comments_of_pull_request(self, pull_request):
614 def _all_inline_comments_of_pull_request(self, pull_request):
616 comments = Session().query(ChangesetComment)\
615 comments = Session().query(ChangesetComment)\
617 .filter(ChangesetComment.line_no != None)\
616 .filter(ChangesetComment.line_no != None)\
618 .filter(ChangesetComment.f_path != None)\
617 .filter(ChangesetComment.f_path != None)\
619 .filter(ChangesetComment.pull_request == pull_request)
618 .filter(ChangesetComment.pull_request == pull_request)
620 return comments
619 return comments
621
620
622 def _all_general_comments_of_pull_request(self, pull_request):
621 def _all_general_comments_of_pull_request(self, pull_request):
623 comments = Session().query(ChangesetComment)\
622 comments = Session().query(ChangesetComment)\
624 .filter(ChangesetComment.line_no == None)\
623 .filter(ChangesetComment.line_no == None)\
625 .filter(ChangesetComment.f_path == None)\
624 .filter(ChangesetComment.f_path == None)\
626 .filter(ChangesetComment.pull_request == pull_request)
625 .filter(ChangesetComment.pull_request == pull_request)
627 return comments
626 return comments
628
627
629 @staticmethod
628 @staticmethod
630 def use_outdated_comments(pull_request):
629 def use_outdated_comments(pull_request):
631 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
630 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
632 settings = settings_model.get_general_settings()
631 settings = settings_model.get_general_settings()
633 return settings.get('rhodecode_use_outdated_comments', False)
632 return settings.get('rhodecode_use_outdated_comments', False)
634
633
635
634
636 def _parse_comment_line_number(line_no):
635 def _parse_comment_line_number(line_no):
637 """
636 """
638 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
637 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
639 """
638 """
640 old_line = None
639 old_line = None
641 new_line = None
640 new_line = None
642 if line_no.startswith('o'):
641 if line_no.startswith('o'):
643 old_line = int(line_no[1:])
642 old_line = int(line_no[1:])
644 elif line_no.startswith('n'):
643 elif line_no.startswith('n'):
645 new_line = int(line_no[1:])
644 new_line = int(line_no[1:])
646 else:
645 else:
647 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
646 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
648 return diffs.DiffLineNumber(old_line, new_line)
647 return diffs.DiffLineNumber(old_line, new_line)
649
648
650
649
651 def _diff_to_comment_line_number(diff_line):
650 def _diff_to_comment_line_number(diff_line):
652 if diff_line.new is not None:
651 if diff_line.new is not None:
653 return u'n{}'.format(diff_line.new)
652 return u'n{}'.format(diff_line.new)
654 elif diff_line.old is not None:
653 elif diff_line.old is not None:
655 return u'o{}'.format(diff_line.old)
654 return u'o{}'.format(diff_line.old)
656 return u''
655 return u''
657
656
658
657
659 def _diff_line_delta(a, b):
658 def _diff_line_delta(a, b):
660 if None not in (a.new, b.new):
659 if None not in (a.new, b.new):
661 return abs(a.new - b.new)
660 return abs(a.new - b.new)
662 elif None not in (a.old, b.old):
661 elif None not in (a.old, b.old):
663 return abs(a.old - b.old)
662 return abs(a.old - b.old)
664 else:
663 else:
665 raise ValueError(
664 raise ValueError(
666 "Cannot compute delta between {} and {}".format(a, b))
665 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1554 +1,1551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
78
78
79 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
84 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
89 ' failed.'),
89 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
92 ' head.'),
92 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
95 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
98 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
101 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
104 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
110 'reference is missing.'),
110 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
113 'to sub repositories.'),
114 }
114 }
115
115
116 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
118 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
128 'reference is missing.'),
128 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
131 'reference is missing.'),
131 'reference is missing.'),
132 }
132 }
133
133
134 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
135 return self._get_instance((
135 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
137
137
138 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
139 if not api:
140 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
142 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
145
145
146 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
149
149
150 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
153
153
154 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
157
157
158 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
162
162
163 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
165 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
167
167
168 def get(self, pull_request):
168 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
170
170
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
173 order_dir='desc'):
173 order_dir='desc'):
174 repo = None
174 repo = None
175 if repo_name:
175 if repo_name:
176 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
177
177
178 q = PullRequest.query()
178 q = PullRequest.query()
179
179
180 # source or target
180 # source or target
181 if repo and source:
181 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
183 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
185
185
186 # closed,opened
186 # closed,opened
187 if statuses:
187 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
189
189
190 # opened by filter
190 # opened by filter
191 if opened_by:
191 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
193
193
194 if order_by:
194 if order_by:
195 order_map = {
195 order_map = {
196 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
197 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
200 }
200 }
201 if order_dir == 'asc':
201 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
203 else:
203 else:
204 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
205
205
206 return q
206 return q
207
207
208 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
209 opened_by=None):
210 """
210 """
211 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
212
212
213 :param repo_name: target or source repo
213 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
217 :returns: int number of pull requests
218 """
218 """
219 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
221
222 return q.count()
222 return q.count()
223
223
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
226 """
227 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :param offset: pagination offset
233 :param offset: pagination offset
234 :param length: length of returned list
234 :param length: length of returned list
235 :param order_by: order of the returned list
235 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
237 :returns: list of pull requests
238 """
238 """
239 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242
242
243 if length:
243 if length:
244 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
245 else:
245 else:
246 pull_requests = q.all()
246 pull_requests = q.all()
247
247
248 return pull_requests
248 return pull_requests
249
249
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
251 opened_by=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review.
254 awaiting review.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
260 :returns: int number of pull requests
261 """
261 """
262 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
264
265 return len(pull_requests)
265 return len(pull_requests)
266
266
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
270 """
270 """
271 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
272 review.
272 review.
273
273
274 :param repo_name: target or source repo
274 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
278 :param offset: pagination offset
278 :param offset: pagination offset
279 :param length: length of returned list
279 :param length: length of returned list
280 :param order_by: order of the returned list
280 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
282 :returns: list of pull requests
283 """
283 """
284 pull_requests = self.get_all(
284 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
287
287
288 _filtered_pull_requests = []
288 _filtered_pull_requests = []
289 for pr in pull_requests:
289 for pr in pull_requests:
290 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
294 if length:
294 if length:
295 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
296 else:
296 else:
297 return _filtered_pull_requests
297 return _filtered_pull_requests
298
298
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
301 """
301 """
302 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
303 awaiting review from a specific user.
304
304
305 :param repo_name: target or source repo
305 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
314 user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
330 :param offset: pagination offset
331 :param length: length of returned list
331 :param length: length of returned list
332 :param order_by: order of the returned list
332 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
334 :returns: list of pull requests
335 """
335 """
336 pull_requests = self.get_all(
336 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
339
339
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
341 my_participation = []
342 for pr in pull_requests:
342 for pr in pull_requests:
343 if pr in _my:
343 if pr in _my:
344 my_participation.append(pr)
344 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
346 if length:
346 if length:
347 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
348 else:
348 else:
349 return _filtered_pull_requests
349 return _filtered_pull_requests
350
350
351 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
352 return [
352 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
355 ]
355 ]
356
356
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
359 q = PullRequest.query()
360 if user_id:
360 if user_id:
361 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
364 user_filter= or_(
365 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
367 )
368 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
369
369
370 # closed,opened
370 # closed,opened
371 if statuses:
371 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
373
373
374 if order_by:
374 if order_by:
375 order_map = {
375 order_map = {
376 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
377 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
380 }
380 }
381 if order_dir == 'asc':
381 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
383 else:
383 else:
384 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
385
385
386 return q
386 return q
387
387
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
390 return q.count()
391
391
392 def get_im_participating_in(
392 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
395 """
395 """
396 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
397 """
397 """
398
398
399 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
401 order_dir=order_dir)
402
402
403 if length:
403 if length:
404 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
405 else:
405 else:
406 pull_requests = q.all()
406 pull_requests = q.all()
407
407
408 return pull_requests
408 return pull_requests
409
409
410 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
411 """
411 """
412 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
413 """
413 """
414 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
417 .all()
418
418
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
421 reviewer_data=None):
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 self.notify_reviewers(pull_request, reviewer_ids)
469 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
476 created_by_user, pull_request)
477
477
478 return pull_request
478 return pull_request
479
479
480 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 pull_request = self.__get_pull_request(pull_request)
481 pull_request = self.__get_pull_request(pull_request)
482 target_scm = pull_request.target_repo.scm_instance()
482 target_scm = pull_request.target_repo.scm_instance()
483 if action == 'create':
483 if action == 'create':
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 elif action == 'merge':
485 elif action == 'merge':
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 elif action == 'close':
487 elif action == 'close':
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 elif action == 'review_status_change':
489 elif action == 'review_status_change':
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 elif action == 'update':
491 elif action == 'update':
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 else:
493 else:
494 return
494 return
495
495
496 trigger_hook(
496 trigger_hook(
497 username=user.username,
497 username=user.username,
498 repo_name=pull_request.target_repo.repo_name,
498 repo_name=pull_request.target_repo.repo_name,
499 repo_alias=target_scm.alias,
499 repo_alias=target_scm.alias,
500 pull_request=pull_request)
500 pull_request=pull_request)
501
501
502 def _get_commit_ids(self, pull_request):
502 def _get_commit_ids(self, pull_request):
503 """
503 """
504 Return the commit ids of the merged pull request.
504 Return the commit ids of the merged pull request.
505
505
506 This method is not dealing correctly yet with the lack of autoupdates
506 This method is not dealing correctly yet with the lack of autoupdates
507 nor with the implicit target updates.
507 nor with the implicit target updates.
508 For example: if a commit in the source repo is already in the target it
508 For example: if a commit in the source repo is already in the target it
509 will be reported anyways.
509 will be reported anyways.
510 """
510 """
511 merge_rev = pull_request.merge_rev
511 merge_rev = pull_request.merge_rev
512 if merge_rev is None:
512 if merge_rev is None:
513 raise ValueError('This pull request was not merged yet')
513 raise ValueError('This pull request was not merged yet')
514
514
515 commit_ids = list(pull_request.revisions)
515 commit_ids = list(pull_request.revisions)
516 if merge_rev not in commit_ids:
516 if merge_rev not in commit_ids:
517 commit_ids.append(merge_rev)
517 commit_ids.append(merge_rev)
518
518
519 return commit_ids
519 return commit_ids
520
520
521 def merge(self, pull_request, user, extras):
521 def merge(self, pull_request, user, extras):
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 if merge_state.executed:
524 if merge_state.executed:
525 log.debug(
525 log.debug(
526 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
527 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
528
528
529 self._log_audit_action(
529 self._log_audit_action(
530 'repo.pull_request.merge',
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
532 user, pull_request)
533
533
534 else:
534 else:
535 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
536 return merge_state
537
537
538 def _merge_pull_request(self, pull_request, user, extras):
538 def _merge_pull_request(self, pull_request, user, extras):
539 target_vcs = pull_request.target_repo.scm_instance()
539 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
541 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
542 pull_request.target_ref_parts, target_vcs)
543
543
544 message = _(
544 message = _(
545 'Merge pull request #%(pr_id)s from '
545 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
547 'pr_id': pull_request.pull_request_id,
548 'source_repo': source_vcs.name,
548 'source_repo': source_vcs.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
550 'pr_title': pull_request.title
550 'pr_title': pull_request.title
551 }
551 }
552
552
553 workspace_id = self._workspace_id(pull_request)
553 workspace_id = self._workspace_id(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
555
555
556 callback_daemon, extras = prepare_callback_daemon(
556 callback_daemon, extras = prepare_callback_daemon(
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559
559
560 with callback_daemon:
560 with callback_daemon:
561 # TODO: johbo: Implement a clean way to run a config_override
561 # TODO: johbo: Implement a clean way to run a config_override
562 # for a single call.
562 # for a single call.
563 target_vcs.config.set(
563 target_vcs.config.set(
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 merge_state = target_vcs.merge(
565 merge_state = target_vcs.merge(
566 target_ref, source_vcs, pull_request.source_ref_parts,
566 target_ref, source_vcs, pull_request.source_ref_parts,
567 workspace_id, user_name=user.username,
567 workspace_id, user_name=user.username,
568 user_email=user.email, message=message, use_rebase=use_rebase)
568 user_email=user.email, message=message, use_rebase=use_rebase)
569 return merge_state
569 return merge_state
570
570
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
573 pull_request.updated_on = datetime.datetime.now()
573 pull_request.updated_on = datetime.datetime.now()
574
574
575 CommentsModel().create(
575 CommentsModel().create(
576 text=unicode(_('Pull request merged and closed')),
576 text=unicode(_('Pull request merged and closed')),
577 repo=pull_request.target_repo.repo_id,
577 repo=pull_request.target_repo.repo_id,
578 user=user.user_id,
578 user=user.user_id,
579 pull_request=pull_request.pull_request_id,
579 pull_request=pull_request.pull_request_id,
580 f_path=None,
580 f_path=None,
581 line_no=None,
581 line_no=None,
582 closing_pr=True
582 closing_pr=True
583 )
583 )
584
584
585 Session().add(pull_request)
585 Session().add(pull_request)
586 Session().flush()
586 Session().flush()
587 # TODO: paris: replace invalidation with less radical solution
587 # TODO: paris: replace invalidation with less radical solution
588 ScmModel().mark_for_invalidation(
588 ScmModel().mark_for_invalidation(
589 pull_request.target_repo.repo_name)
589 pull_request.target_repo.repo_name)
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
591
591
592 def has_valid_update_type(self, pull_request):
592 def has_valid_update_type(self, pull_request):
593 source_ref_type = pull_request.source_ref_parts.type
593 source_ref_type = pull_request.source_ref_parts.type
594 return source_ref_type in ['book', 'branch', 'tag']
594 return source_ref_type in ['book', 'branch', 'tag']
595
595
596 def update_commits(self, pull_request):
596 def update_commits(self, pull_request):
597 """
597 """
598 Get the updated list of commits for the pull request
598 Get the updated list of commits for the pull request
599 and return the new pull request version and the list
599 and return the new pull request version and the list
600 of commits processed by this update action
600 of commits processed by this update action
601 """
601 """
602 pull_request = self.__get_pull_request(pull_request)
602 pull_request = self.__get_pull_request(pull_request)
603 source_ref_type = pull_request.source_ref_parts.type
603 source_ref_type = pull_request.source_ref_parts.type
604 source_ref_name = pull_request.source_ref_parts.name
604 source_ref_name = pull_request.source_ref_parts.name
605 source_ref_id = pull_request.source_ref_parts.commit_id
605 source_ref_id = pull_request.source_ref_parts.commit_id
606
606
607 target_ref_type = pull_request.target_ref_parts.type
607 target_ref_type = pull_request.target_ref_parts.type
608 target_ref_name = pull_request.target_ref_parts.name
608 target_ref_name = pull_request.target_ref_parts.name
609 target_ref_id = pull_request.target_ref_parts.commit_id
609 target_ref_id = pull_request.target_ref_parts.commit_id
610
610
611 if not self.has_valid_update_type(pull_request):
611 if not self.has_valid_update_type(pull_request):
612 log.debug(
612 log.debug(
613 "Skipping update of pull request %s due to ref type: %s",
613 "Skipping update of pull request %s due to ref type: %s",
614 pull_request, source_ref_type)
614 pull_request, source_ref_type)
615 return UpdateResponse(
615 return UpdateResponse(
616 executed=False,
616 executed=False,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
618 old=pull_request, new=None, changes=None,
618 old=pull_request, new=None, changes=None,
619 source_changed=False, target_changed=False)
619 source_changed=False, target_changed=False)
620
620
621 # source repo
621 # source repo
622 source_repo = pull_request.source_repo.scm_instance()
622 source_repo = pull_request.source_repo.scm_instance()
623 try:
623 try:
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
625 except CommitDoesNotExistError:
625 except CommitDoesNotExistError:
626 return UpdateResponse(
626 return UpdateResponse(
627 executed=False,
627 executed=False,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
629 old=pull_request, new=None, changes=None,
629 old=pull_request, new=None, changes=None,
630 source_changed=False, target_changed=False)
630 source_changed=False, target_changed=False)
631
631
632 source_changed = source_ref_id != source_commit.raw_id
632 source_changed = source_ref_id != source_commit.raw_id
633
633
634 # target repo
634 # target repo
635 target_repo = pull_request.target_repo.scm_instance()
635 target_repo = pull_request.target_repo.scm_instance()
636 try:
636 try:
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
638 except CommitDoesNotExistError:
638 except CommitDoesNotExistError:
639 return UpdateResponse(
639 return UpdateResponse(
640 executed=False,
640 executed=False,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
642 old=pull_request, new=None, changes=None,
642 old=pull_request, new=None, changes=None,
643 source_changed=False, target_changed=False)
643 source_changed=False, target_changed=False)
644 target_changed = target_ref_id != target_commit.raw_id
644 target_changed = target_ref_id != target_commit.raw_id
645
645
646 if not (source_changed or target_changed):
646 if not (source_changed or target_changed):
647 log.debug("Nothing changed in pull request %s", pull_request)
647 log.debug("Nothing changed in pull request %s", pull_request)
648 return UpdateResponse(
648 return UpdateResponse(
649 executed=False,
649 executed=False,
650 reason=UpdateFailureReason.NO_CHANGE,
650 reason=UpdateFailureReason.NO_CHANGE,
651 old=pull_request, new=None, changes=None,
651 old=pull_request, new=None, changes=None,
652 source_changed=target_changed, target_changed=source_changed)
652 source_changed=target_changed, target_changed=source_changed)
653
653
654 change_in_found = 'target repo' if target_changed else 'source repo'
654 change_in_found = 'target repo' if target_changed else 'source repo'
655 log.debug('Updating pull request because of change in %s detected',
655 log.debug('Updating pull request because of change in %s detected',
656 change_in_found)
656 change_in_found)
657
657
658 # Finally there is a need for an update, in case of source change
658 # Finally there is a need for an update, in case of source change
659 # we create a new version, else just an update
659 # we create a new version, else just an update
660 if source_changed:
660 if source_changed:
661 pull_request_version = self._create_version_from_snapshot(pull_request)
661 pull_request_version = self._create_version_from_snapshot(pull_request)
662 self._link_comments_to_version(pull_request_version)
662 self._link_comments_to_version(pull_request_version)
663 else:
663 else:
664 try:
664 try:
665 ver = pull_request.versions[-1]
665 ver = pull_request.versions[-1]
666 except IndexError:
666 except IndexError:
667 ver = None
667 ver = None
668
668
669 pull_request.pull_request_version_id = \
669 pull_request.pull_request_version_id = \
670 ver.pull_request_version_id if ver else None
670 ver.pull_request_version_id if ver else None
671 pull_request_version = pull_request
671 pull_request_version = pull_request
672
672
673 try:
673 try:
674 if target_ref_type in ('tag', 'branch', 'book'):
674 if target_ref_type in ('tag', 'branch', 'book'):
675 target_commit = target_repo.get_commit(target_ref_name)
675 target_commit = target_repo.get_commit(target_ref_name)
676 else:
676 else:
677 target_commit = target_repo.get_commit(target_ref_id)
677 target_commit = target_repo.get_commit(target_ref_id)
678 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
679 return UpdateResponse(
679 return UpdateResponse(
680 executed=False,
680 executed=False,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
682 old=pull_request, new=None, changes=None,
682 old=pull_request, new=None, changes=None,
683 source_changed=source_changed, target_changed=target_changed)
683 source_changed=source_changed, target_changed=target_changed)
684
684
685 # re-compute commit ids
685 # re-compute commit ids
686 old_commit_ids = pull_request.revisions
686 old_commit_ids = pull_request.revisions
687 pre_load = ["author", "branch", "date", "message"]
687 pre_load = ["author", "branch", "date", "message"]
688 commit_ranges = target_repo.compare(
688 commit_ranges = target_repo.compare(
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
690 pre_load=pre_load)
690 pre_load=pre_load)
691
691
692 ancestor = target_repo.get_common_ancestor(
692 ancestor = target_repo.get_common_ancestor(
693 target_commit.raw_id, source_commit.raw_id, source_repo)
693 target_commit.raw_id, source_commit.raw_id, source_repo)
694
694
695 pull_request.source_ref = '%s:%s:%s' % (
695 pull_request.source_ref = '%s:%s:%s' % (
696 source_ref_type, source_ref_name, source_commit.raw_id)
696 source_ref_type, source_ref_name, source_commit.raw_id)
697 pull_request.target_ref = '%s:%s:%s' % (
697 pull_request.target_ref = '%s:%s:%s' % (
698 target_ref_type, target_ref_name, ancestor)
698 target_ref_type, target_ref_name, ancestor)
699
699
700 pull_request.revisions = [
700 pull_request.revisions = [
701 commit.raw_id for commit in reversed(commit_ranges)]
701 commit.raw_id for commit in reversed(commit_ranges)]
702 pull_request.updated_on = datetime.datetime.now()
702 pull_request.updated_on = datetime.datetime.now()
703 Session().add(pull_request)
703 Session().add(pull_request)
704 new_commit_ids = pull_request.revisions
704 new_commit_ids = pull_request.revisions
705
705
706 old_diff_data, new_diff_data = self._generate_update_diffs(
706 old_diff_data, new_diff_data = self._generate_update_diffs(
707 pull_request, pull_request_version)
707 pull_request, pull_request_version)
708
708
709 # calculate commit and file changes
709 # calculate commit and file changes
710 changes = self._calculate_commit_id_changes(
710 changes = self._calculate_commit_id_changes(
711 old_commit_ids, new_commit_ids)
711 old_commit_ids, new_commit_ids)
712 file_changes = self._calculate_file_changes(
712 file_changes = self._calculate_file_changes(
713 old_diff_data, new_diff_data)
713 old_diff_data, new_diff_data)
714
714
715 # set comments as outdated if DIFFS changed
715 # set comments as outdated if DIFFS changed
716 CommentsModel().outdate_comments(
716 CommentsModel().outdate_comments(
717 pull_request, old_diff_data=old_diff_data,
717 pull_request, old_diff_data=old_diff_data,
718 new_diff_data=new_diff_data)
718 new_diff_data=new_diff_data)
719
719
720 commit_changes = (changes.added or changes.removed)
720 commit_changes = (changes.added or changes.removed)
721 file_node_changes = (
721 file_node_changes = (
722 file_changes.added or file_changes.modified or file_changes.removed)
722 file_changes.added or file_changes.modified or file_changes.removed)
723 pr_has_changes = commit_changes or file_node_changes
723 pr_has_changes = commit_changes or file_node_changes
724
724
725 # Add an automatic comment to the pull request, in case
725 # Add an automatic comment to the pull request, in case
726 # anything has changed
726 # anything has changed
727 if pr_has_changes:
727 if pr_has_changes:
728 update_comment = CommentsModel().create(
728 update_comment = CommentsModel().create(
729 text=self._render_update_message(changes, file_changes),
729 text=self._render_update_message(changes, file_changes),
730 repo=pull_request.target_repo,
730 repo=pull_request.target_repo,
731 user=pull_request.author,
731 user=pull_request.author,
732 pull_request=pull_request,
732 pull_request=pull_request,
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
734
734
735 # Update status to "Under Review" for added commits
735 # Update status to "Under Review" for added commits
736 for commit_id in changes.added:
736 for commit_id in changes.added:
737 ChangesetStatusModel().set_status(
737 ChangesetStatusModel().set_status(
738 repo=pull_request.source_repo,
738 repo=pull_request.source_repo,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
740 comment=update_comment,
740 comment=update_comment,
741 user=pull_request.author,
741 user=pull_request.author,
742 pull_request=pull_request,
742 pull_request=pull_request,
743 revision=commit_id)
743 revision=commit_id)
744
744
745 log.debug(
745 log.debug(
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
747 'removed_ids: %s', pull_request.pull_request_id,
747 'removed_ids: %s', pull_request.pull_request_id,
748 changes.added, changes.common, changes.removed)
748 changes.added, changes.common, changes.removed)
749 log.debug(
749 log.debug(
750 'Updated pull request with the following file changes: %s',
750 'Updated pull request with the following file changes: %s',
751 file_changes)
751 file_changes)
752
752
753 log.info(
753 log.info(
754 "Updated pull request %s from commit %s to commit %s, "
754 "Updated pull request %s from commit %s to commit %s, "
755 "stored new version %s of this pull request.",
755 "stored new version %s of this pull request.",
756 pull_request.pull_request_id, source_ref_id,
756 pull_request.pull_request_id, source_ref_id,
757 pull_request.source_ref_parts.commit_id,
757 pull_request.source_ref_parts.commit_id,
758 pull_request_version.pull_request_version_id)
758 pull_request_version.pull_request_version_id)
759 Session().commit()
759 Session().commit()
760 self._trigger_pull_request_hook(
760 self._trigger_pull_request_hook(
761 pull_request, pull_request.author, 'update')
761 pull_request, pull_request.author, 'update')
762
762
763 return UpdateResponse(
763 return UpdateResponse(
764 executed=True, reason=UpdateFailureReason.NONE,
764 executed=True, reason=UpdateFailureReason.NONE,
765 old=pull_request, new=pull_request_version, changes=changes,
765 old=pull_request, new=pull_request_version, changes=changes,
766 source_changed=source_changed, target_changed=target_changed)
766 source_changed=source_changed, target_changed=target_changed)
767
767
768 def _create_version_from_snapshot(self, pull_request):
768 def _create_version_from_snapshot(self, pull_request):
769 version = PullRequestVersion()
769 version = PullRequestVersion()
770 version.title = pull_request.title
770 version.title = pull_request.title
771 version.description = pull_request.description
771 version.description = pull_request.description
772 version.status = pull_request.status
772 version.status = pull_request.status
773 version.created_on = datetime.datetime.now()
773 version.created_on = datetime.datetime.now()
774 version.updated_on = pull_request.updated_on
774 version.updated_on = pull_request.updated_on
775 version.user_id = pull_request.user_id
775 version.user_id = pull_request.user_id
776 version.source_repo = pull_request.source_repo
776 version.source_repo = pull_request.source_repo
777 version.source_ref = pull_request.source_ref
777 version.source_ref = pull_request.source_ref
778 version.target_repo = pull_request.target_repo
778 version.target_repo = pull_request.target_repo
779 version.target_ref = pull_request.target_ref
779 version.target_ref = pull_request.target_ref
780
780
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
783 version._last_merge_status = pull_request._last_merge_status
783 version._last_merge_status = pull_request._last_merge_status
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
785 version.merge_rev = pull_request.merge_rev
785 version.merge_rev = pull_request.merge_rev
786 version.reviewer_data = pull_request.reviewer_data
786 version.reviewer_data = pull_request.reviewer_data
787
787
788 version.revisions = pull_request.revisions
788 version.revisions = pull_request.revisions
789 version.pull_request = pull_request
789 version.pull_request = pull_request
790 Session().add(version)
790 Session().add(version)
791 Session().flush()
791 Session().flush()
792
792
793 return version
793 return version
794
794
795 def _generate_update_diffs(self, pull_request, pull_request_version):
795 def _generate_update_diffs(self, pull_request, pull_request_version):
796
796
797 diff_context = (
797 diff_context = (
798 self.DIFF_CONTEXT +
798 self.DIFF_CONTEXT +
799 CommentsModel.needed_extra_diff_context())
799 CommentsModel.needed_extra_diff_context())
800
800
801 source_repo = pull_request_version.source_repo
801 source_repo = pull_request_version.source_repo
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
804 old_diff = self._get_diff_from_pr_or_version(
804 old_diff = self._get_diff_from_pr_or_version(
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
806
806
807 source_repo = pull_request.source_repo
807 source_repo = pull_request.source_repo
808 source_ref_id = pull_request.source_ref_parts.commit_id
808 source_ref_id = pull_request.source_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
810
810
811 new_diff = self._get_diff_from_pr_or_version(
811 new_diff = self._get_diff_from_pr_or_version(
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
813
813
814 old_diff_data = diffs.DiffProcessor(old_diff)
814 old_diff_data = diffs.DiffProcessor(old_diff)
815 old_diff_data.prepare()
815 old_diff_data.prepare()
816 new_diff_data = diffs.DiffProcessor(new_diff)
816 new_diff_data = diffs.DiffProcessor(new_diff)
817 new_diff_data.prepare()
817 new_diff_data.prepare()
818
818
819 return old_diff_data, new_diff_data
819 return old_diff_data, new_diff_data
820
820
821 def _link_comments_to_version(self, pull_request_version):
821 def _link_comments_to_version(self, pull_request_version):
822 """
822 """
823 Link all unlinked comments of this pull request to the given version.
823 Link all unlinked comments of this pull request to the given version.
824
824
825 :param pull_request_version: The `PullRequestVersion` to which
825 :param pull_request_version: The `PullRequestVersion` to which
826 the comments shall be linked.
826 the comments shall be linked.
827
827
828 """
828 """
829 pull_request = pull_request_version.pull_request
829 pull_request = pull_request_version.pull_request
830 comments = ChangesetComment.query()\
830 comments = ChangesetComment.query()\
831 .filter(
831 .filter(
832 # TODO: johbo: Should we query for the repo at all here?
832 # TODO: johbo: Should we query for the repo at all here?
833 # Pending decision on how comments of PRs are to be related
833 # Pending decision on how comments of PRs are to be related
834 # to either the source repo, the target repo or no repo at all.
834 # to either the source repo, the target repo or no repo at all.
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
836 ChangesetComment.pull_request == pull_request,
836 ChangesetComment.pull_request == pull_request,
837 ChangesetComment.pull_request_version == None)\
837 ChangesetComment.pull_request_version == None)\
838 .order_by(ChangesetComment.comment_id.asc())
838 .order_by(ChangesetComment.comment_id.asc())
839
839
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
841 # operation.
841 # operation.
842 for comment in comments:
842 for comment in comments:
843 comment.pull_request_version_id = (
843 comment.pull_request_version_id = (
844 pull_request_version.pull_request_version_id)
844 pull_request_version.pull_request_version_id)
845 Session().add(comment)
845 Session().add(comment)
846
846
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
848 added = [x for x in new_ids if x not in old_ids]
848 added = [x for x in new_ids if x not in old_ids]
849 common = [x for x in new_ids if x in old_ids]
849 common = [x for x in new_ids if x in old_ids]
850 removed = [x for x in old_ids if x not in new_ids]
850 removed = [x for x in old_ids if x not in new_ids]
851 total = new_ids
851 total = new_ids
852 return ChangeTuple(added, common, removed, total)
852 return ChangeTuple(added, common, removed, total)
853
853
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
855
855
856 old_files = OrderedDict()
856 old_files = OrderedDict()
857 for diff_data in old_diff_data.parsed_diff:
857 for diff_data in old_diff_data.parsed_diff:
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
859
859
860 added_files = []
860 added_files = []
861 modified_files = []
861 modified_files = []
862 removed_files = []
862 removed_files = []
863 for diff_data in new_diff_data.parsed_diff:
863 for diff_data in new_diff_data.parsed_diff:
864 new_filename = diff_data['filename']
864 new_filename = diff_data['filename']
865 new_hash = md5_safe(diff_data['raw_diff'])
865 new_hash = md5_safe(diff_data['raw_diff'])
866
866
867 old_hash = old_files.get(new_filename)
867 old_hash = old_files.get(new_filename)
868 if not old_hash:
868 if not old_hash:
869 # file is not present in old diff, means it's added
869 # file is not present in old diff, means it's added
870 added_files.append(new_filename)
870 added_files.append(new_filename)
871 else:
871 else:
872 if new_hash != old_hash:
872 if new_hash != old_hash:
873 modified_files.append(new_filename)
873 modified_files.append(new_filename)
874 # now remove a file from old, since we have seen it already
874 # now remove a file from old, since we have seen it already
875 del old_files[new_filename]
875 del old_files[new_filename]
876
876
877 # removed files is when there are present in old, but not in NEW,
877 # removed files is when there are present in old, but not in NEW,
878 # since we remove old files that are present in new diff, left-overs
878 # since we remove old files that are present in new diff, left-overs
879 # if any should be the removed files
879 # if any should be the removed files
880 removed_files.extend(old_files.keys())
880 removed_files.extend(old_files.keys())
881
881
882 return FileChangeTuple(added_files, modified_files, removed_files)
882 return FileChangeTuple(added_files, modified_files, removed_files)
883
883
884 def _render_update_message(self, changes, file_changes):
884 def _render_update_message(self, changes, file_changes):
885 """
885 """
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
887 so it's always looking the same disregarding on which default
887 so it's always looking the same disregarding on which default
888 renderer system is using.
888 renderer system is using.
889
889
890 :param changes: changes named tuple
890 :param changes: changes named tuple
891 :param file_changes: file changes named tuple
891 :param file_changes: file changes named tuple
892
892
893 """
893 """
894 new_status = ChangesetStatus.get_status_lbl(
894 new_status = ChangesetStatus.get_status_lbl(
895 ChangesetStatus.STATUS_UNDER_REVIEW)
895 ChangesetStatus.STATUS_UNDER_REVIEW)
896
896
897 changed_files = (
897 changed_files = (
898 file_changes.added + file_changes.modified + file_changes.removed)
898 file_changes.added + file_changes.modified + file_changes.removed)
899
899
900 params = {
900 params = {
901 'under_review_label': new_status,
901 'under_review_label': new_status,
902 'added_commits': changes.added,
902 'added_commits': changes.added,
903 'removed_commits': changes.removed,
903 'removed_commits': changes.removed,
904 'changed_files': changed_files,
904 'changed_files': changed_files,
905 'added_files': file_changes.added,
905 'added_files': file_changes.added,
906 'modified_files': file_changes.modified,
906 'modified_files': file_changes.modified,
907 'removed_files': file_changes.removed,
907 'removed_files': file_changes.removed,
908 }
908 }
909 renderer = RstTemplateRenderer()
909 renderer = RstTemplateRenderer()
910 return renderer.render('pull_request_update.mako', **params)
910 return renderer.render('pull_request_update.mako', **params)
911
911
912 def edit(self, pull_request, title, description, user):
912 def edit(self, pull_request, title, description, user):
913 pull_request = self.__get_pull_request(pull_request)
913 pull_request = self.__get_pull_request(pull_request)
914 old_data = pull_request.get_api_data(with_merge_state=False)
914 old_data = pull_request.get_api_data(with_merge_state=False)
915 if pull_request.is_closed():
915 if pull_request.is_closed():
916 raise ValueError('This pull request is closed')
916 raise ValueError('This pull request is closed')
917 if title:
917 if title:
918 pull_request.title = title
918 pull_request.title = title
919 pull_request.description = description
919 pull_request.description = description
920 pull_request.updated_on = datetime.datetime.now()
920 pull_request.updated_on = datetime.datetime.now()
921 Session().add(pull_request)
921 Session().add(pull_request)
922 self._log_audit_action(
922 self._log_audit_action(
923 'repo.pull_request.edit', {'old_data': old_data},
923 'repo.pull_request.edit', {'old_data': old_data},
924 user, pull_request)
924 user, pull_request)
925
925
926 def update_reviewers(self, pull_request, reviewer_data, user):
926 def update_reviewers(self, pull_request, reviewer_data, user):
927 """
927 """
928 Update the reviewers in the pull request
928 Update the reviewers in the pull request
929
929
930 :param pull_request: the pr to update
930 :param pull_request: the pr to update
931 :param reviewer_data: list of tuples
931 :param reviewer_data: list of tuples
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
933 """
933 """
934
934
935 reviewers = {}
935 reviewers = {}
936 for user_id, reasons, mandatory in reviewer_data:
936 for user_id, reasons, mandatory in reviewer_data:
937 if isinstance(user_id, (int, basestring)):
937 if isinstance(user_id, (int, basestring)):
938 user_id = self._get_user(user_id).user_id
938 user_id = self._get_user(user_id).user_id
939 reviewers[user_id] = {
939 reviewers[user_id] = {
940 'reasons': reasons, 'mandatory': mandatory}
940 'reasons': reasons, 'mandatory': mandatory}
941
941
942 reviewers_ids = set(reviewers.keys())
942 reviewers_ids = set(reviewers.keys())
943 pull_request = self.__get_pull_request(pull_request)
943 pull_request = self.__get_pull_request(pull_request)
944 current_reviewers = PullRequestReviewers.query()\
944 current_reviewers = PullRequestReviewers.query()\
945 .filter(PullRequestReviewers.pull_request ==
945 .filter(PullRequestReviewers.pull_request ==
946 pull_request).all()
946 pull_request).all()
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
948
948
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
951
951
952 log.debug("Adding %s reviewers", ids_to_add)
952 log.debug("Adding %s reviewers", ids_to_add)
953 log.debug("Removing %s reviewers", ids_to_remove)
953 log.debug("Removing %s reviewers", ids_to_remove)
954 changed = False
954 changed = False
955 for uid in ids_to_add:
955 for uid in ids_to_add:
956 changed = True
956 changed = True
957 _usr = self._get_user(uid)
957 _usr = self._get_user(uid)
958 reviewer = PullRequestReviewers()
958 reviewer = PullRequestReviewers()
959 reviewer.user = _usr
959 reviewer.user = _usr
960 reviewer.pull_request = pull_request
960 reviewer.pull_request = pull_request
961 reviewer.reasons = reviewers[uid]['reasons']
961 reviewer.reasons = reviewers[uid]['reasons']
962 # NOTE(marcink): mandatory shouldn't be changed now
962 # NOTE(marcink): mandatory shouldn't be changed now
963 # reviewer.mandatory = reviewers[uid]['reasons']
963 # reviewer.mandatory = reviewers[uid]['reasons']
964 Session().add(reviewer)
964 Session().add(reviewer)
965 self._log_audit_action(
965 self._log_audit_action(
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 user, pull_request)
967 user, pull_request)
968
968
969 for uid in ids_to_remove:
969 for uid in ids_to_remove:
970 changed = True
970 changed = True
971 reviewers = PullRequestReviewers.query()\
971 reviewers = PullRequestReviewers.query()\
972 .filter(PullRequestReviewers.user_id == uid,
972 .filter(PullRequestReviewers.user_id == uid,
973 PullRequestReviewers.pull_request == pull_request)\
973 PullRequestReviewers.pull_request == pull_request)\
974 .all()
974 .all()
975 # use .all() in case we accidentally added the same person twice
975 # use .all() in case we accidentally added the same person twice
976 # this CAN happen due to the lack of DB checks
976 # this CAN happen due to the lack of DB checks
977 for obj in reviewers:
977 for obj in reviewers:
978 old_data = obj.get_dict()
978 old_data = obj.get_dict()
979 Session().delete(obj)
979 Session().delete(obj)
980 self._log_audit_action(
980 self._log_audit_action(
981 'repo.pull_request.reviewer.delete',
981 'repo.pull_request.reviewer.delete',
982 {'old_data': old_data}, user, pull_request)
982 {'old_data': old_data}, user, pull_request)
983
983
984 if changed:
984 if changed:
985 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
986 Session().add(pull_request)
986 Session().add(pull_request)
987
987
988 self.notify_reviewers(pull_request, ids_to_add)
988 self.notify_reviewers(pull_request, ids_to_add)
989 return ids_to_add, ids_to_remove
989 return ids_to_add, ids_to_remove
990
990
991 def get_url(self, pull_request, request=None, permalink=False):
991 def get_url(self, pull_request, request=None, permalink=False):
992 if not request:
992 if not request:
993 request = get_current_request()
993 request = get_current_request()
994
994
995 if permalink:
995 if permalink:
996 return request.route_url(
996 return request.route_url(
997 'pull_requests_global',
997 'pull_requests_global',
998 pull_request_id=pull_request.pull_request_id,)
998 pull_request_id=pull_request.pull_request_id,)
999 else:
999 else:
1000 return request.route_url(
1000 return request.route_url('pullrequest_show',
1001 'pullrequest_show',
1002 repo_name=safe_str(pull_request.target_repo.repo_name),
1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1003 pull_request_id=pull_request.pull_request_id,)
1002 pull_request_id=pull_request.pull_request_id,)
1004
1003
1005 def get_shadow_clone_url(self, pull_request):
1004 def get_shadow_clone_url(self, pull_request):
1006 """
1005 """
1007 Returns qualified url pointing to the shadow repository. If this pull
1006 Returns qualified url pointing to the shadow repository. If this pull
1008 request is closed there is no shadow repository and ``None`` will be
1007 request is closed there is no shadow repository and ``None`` will be
1009 returned.
1008 returned.
1010 """
1009 """
1011 if pull_request.is_closed():
1010 if pull_request.is_closed():
1012 return None
1011 return None
1013 else:
1012 else:
1014 pr_url = urllib.unquote(self.get_url(pull_request))
1013 pr_url = urllib.unquote(self.get_url(pull_request))
1015 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1016
1015
1017 def notify_reviewers(self, pull_request, reviewers_ids):
1016 def notify_reviewers(self, pull_request, reviewers_ids):
1018 # notification to reviewers
1017 # notification to reviewers
1019 if not reviewers_ids:
1018 if not reviewers_ids:
1020 return
1019 return
1021
1020
1022 pull_request_obj = pull_request
1021 pull_request_obj = pull_request
1023 # get the current participants of this pull request
1022 # get the current participants of this pull request
1024 recipients = reviewers_ids
1023 recipients = reviewers_ids
1025 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1026
1025
1027 pr_source_repo = pull_request_obj.source_repo
1026 pr_source_repo = pull_request_obj.source_repo
1028 pr_target_repo = pull_request_obj.target_repo
1027 pr_target_repo = pull_request_obj.target_repo
1029
1028
1030 pr_url = h.url(
1029 pr_url = h.route_url('pullrequest_show',
1031 'pullrequest_show',
1032 repo_name=pr_target_repo.repo_name,
1030 repo_name=pr_target_repo.repo_name,
1033 pull_request_id=pull_request_obj.pull_request_id,
1031 pull_request_id=pull_request_obj.pull_request_id,)
1034 qualified=True,)
1035
1032
1036 # set some variables for email notification
1033 # set some variables for email notification
1037 pr_target_repo_url = h.route_url(
1034 pr_target_repo_url = h.route_url(
1038 'repo_summary', repo_name=pr_target_repo.repo_name)
1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1039
1036
1040 pr_source_repo_url = h.route_url(
1037 pr_source_repo_url = h.route_url(
1041 'repo_summary', repo_name=pr_source_repo.repo_name)
1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1042
1039
1043 # pull request specifics
1040 # pull request specifics
1044 pull_request_commits = [
1041 pull_request_commits = [
1045 (x.raw_id, x.message)
1042 (x.raw_id, x.message)
1046 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1047
1044
1048 kwargs = {
1045 kwargs = {
1049 'user': pull_request.author,
1046 'user': pull_request.author,
1050 'pull_request': pull_request_obj,
1047 'pull_request': pull_request_obj,
1051 'pull_request_commits': pull_request_commits,
1048 'pull_request_commits': pull_request_commits,
1052
1049
1053 'pull_request_target_repo': pr_target_repo,
1050 'pull_request_target_repo': pr_target_repo,
1054 'pull_request_target_repo_url': pr_target_repo_url,
1051 'pull_request_target_repo_url': pr_target_repo_url,
1055
1052
1056 'pull_request_source_repo': pr_source_repo,
1053 'pull_request_source_repo': pr_source_repo,
1057 'pull_request_source_repo_url': pr_source_repo_url,
1054 'pull_request_source_repo_url': pr_source_repo_url,
1058
1055
1059 'pull_request_url': pr_url,
1056 'pull_request_url': pr_url,
1060 }
1057 }
1061
1058
1062 # pre-generate the subject for notification itself
1059 # pre-generate the subject for notification itself
1063 (subject,
1060 (subject,
1064 _h, _e, # we don't care about those
1061 _h, _e, # we don't care about those
1065 body_plaintext) = EmailNotificationModel().render_email(
1062 body_plaintext) = EmailNotificationModel().render_email(
1066 notification_type, **kwargs)
1063 notification_type, **kwargs)
1067
1064
1068 # create notification objects, and emails
1065 # create notification objects, and emails
1069 NotificationModel().create(
1066 NotificationModel().create(
1070 created_by=pull_request.author,
1067 created_by=pull_request.author,
1071 notification_subject=subject,
1068 notification_subject=subject,
1072 notification_body=body_plaintext,
1069 notification_body=body_plaintext,
1073 notification_type=notification_type,
1070 notification_type=notification_type,
1074 recipients=recipients,
1071 recipients=recipients,
1075 email_kwargs=kwargs,
1072 email_kwargs=kwargs,
1076 )
1073 )
1077
1074
1078 def delete(self, pull_request, user):
1075 def delete(self, pull_request, user):
1079 pull_request = self.__get_pull_request(pull_request)
1076 pull_request = self.__get_pull_request(pull_request)
1080 old_data = pull_request.get_api_data(with_merge_state=False)
1077 old_data = pull_request.get_api_data(with_merge_state=False)
1081 self._cleanup_merge_workspace(pull_request)
1078 self._cleanup_merge_workspace(pull_request)
1082 self._log_audit_action(
1079 self._log_audit_action(
1083 'repo.pull_request.delete', {'old_data': old_data},
1080 'repo.pull_request.delete', {'old_data': old_data},
1084 user, pull_request)
1081 user, pull_request)
1085 Session().delete(pull_request)
1082 Session().delete(pull_request)
1086
1083
1087 def close_pull_request(self, pull_request, user):
1084 def close_pull_request(self, pull_request, user):
1088 pull_request = self.__get_pull_request(pull_request)
1085 pull_request = self.__get_pull_request(pull_request)
1089 self._cleanup_merge_workspace(pull_request)
1086 self._cleanup_merge_workspace(pull_request)
1090 pull_request.status = PullRequest.STATUS_CLOSED
1087 pull_request.status = PullRequest.STATUS_CLOSED
1091 pull_request.updated_on = datetime.datetime.now()
1088 pull_request.updated_on = datetime.datetime.now()
1092 Session().add(pull_request)
1089 Session().add(pull_request)
1093 self._trigger_pull_request_hook(
1090 self._trigger_pull_request_hook(
1094 pull_request, pull_request.author, 'close')
1091 pull_request, pull_request.author, 'close')
1095 self._log_audit_action(
1092 self._log_audit_action(
1096 'repo.pull_request.close', {}, user, pull_request)
1093 'repo.pull_request.close', {}, user, pull_request)
1097
1094
1098 def close_pull_request_with_comment(
1095 def close_pull_request_with_comment(
1099 self, pull_request, user, repo, message=None):
1096 self, pull_request, user, repo, message=None):
1100
1097
1101 pull_request_review_status = pull_request.calculated_review_status()
1098 pull_request_review_status = pull_request.calculated_review_status()
1102
1099
1103 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1104 # approved only if we have voting consent
1101 # approved only if we have voting consent
1105 status = ChangesetStatus.STATUS_APPROVED
1102 status = ChangesetStatus.STATUS_APPROVED
1106 else:
1103 else:
1107 status = ChangesetStatus.STATUS_REJECTED
1104 status = ChangesetStatus.STATUS_REJECTED
1108 status_lbl = ChangesetStatus.get_status_lbl(status)
1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1109
1106
1110 default_message = (
1107 default_message = (
1111 _('Closing with status change {transition_icon} {status}.')
1108 _('Closing with status change {transition_icon} {status}.')
1112 ).format(transition_icon='>', status=status_lbl)
1109 ).format(transition_icon='>', status=status_lbl)
1113 text = message or default_message
1110 text = message or default_message
1114
1111
1115 # create a comment, and link it to new status
1112 # create a comment, and link it to new status
1116 comment = CommentsModel().create(
1113 comment = CommentsModel().create(
1117 text=text,
1114 text=text,
1118 repo=repo.repo_id,
1115 repo=repo.repo_id,
1119 user=user.user_id,
1116 user=user.user_id,
1120 pull_request=pull_request.pull_request_id,
1117 pull_request=pull_request.pull_request_id,
1121 status_change=status_lbl,
1118 status_change=status_lbl,
1122 status_change_type=status,
1119 status_change_type=status,
1123 closing_pr=True
1120 closing_pr=True
1124 )
1121 )
1125
1122
1126 # calculate old status before we change it
1123 # calculate old status before we change it
1127 old_calculated_status = pull_request.calculated_review_status()
1124 old_calculated_status = pull_request.calculated_review_status()
1128 ChangesetStatusModel().set_status(
1125 ChangesetStatusModel().set_status(
1129 repo.repo_id,
1126 repo.repo_id,
1130 status,
1127 status,
1131 user.user_id,
1128 user.user_id,
1132 comment=comment,
1129 comment=comment,
1133 pull_request=pull_request.pull_request_id
1130 pull_request=pull_request.pull_request_id
1134 )
1131 )
1135
1132
1136 Session().flush()
1133 Session().flush()
1137 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1138 # we now calculate the status of pull request again, and based on that
1135 # we now calculate the status of pull request again, and based on that
1139 # calculation trigger status change. This might happen in cases
1136 # calculation trigger status change. This might happen in cases
1140 # that non-reviewer admin closes a pr, which means his vote doesn't
1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1141 # change the status, while if he's a reviewer this might change it.
1138 # change the status, while if he's a reviewer this might change it.
1142 calculated_status = pull_request.calculated_review_status()
1139 calculated_status = pull_request.calculated_review_status()
1143 if old_calculated_status != calculated_status:
1140 if old_calculated_status != calculated_status:
1144 self._trigger_pull_request_hook(
1141 self._trigger_pull_request_hook(
1145 pull_request, user, 'review_status_change')
1142 pull_request, user, 'review_status_change')
1146
1143
1147 # finally close the PR
1144 # finally close the PR
1148 PullRequestModel().close_pull_request(
1145 PullRequestModel().close_pull_request(
1149 pull_request.pull_request_id, user)
1146 pull_request.pull_request_id, user)
1150
1147
1151 return comment, status
1148 return comment, status
1152
1149
1153 def merge_status(self, pull_request):
1150 def merge_status(self, pull_request):
1154 if not self._is_merge_enabled(pull_request):
1151 if not self._is_merge_enabled(pull_request):
1155 return False, _('Server-side pull request merging is disabled.')
1152 return False, _('Server-side pull request merging is disabled.')
1156 if pull_request.is_closed():
1153 if pull_request.is_closed():
1157 return False, _('This pull request is closed.')
1154 return False, _('This pull request is closed.')
1158 merge_possible, msg = self._check_repo_requirements(
1155 merge_possible, msg = self._check_repo_requirements(
1159 target=pull_request.target_repo, source=pull_request.source_repo)
1156 target=pull_request.target_repo, source=pull_request.source_repo)
1160 if not merge_possible:
1157 if not merge_possible:
1161 return merge_possible, msg
1158 return merge_possible, msg
1162
1159
1163 try:
1160 try:
1164 resp = self._try_merge(pull_request)
1161 resp = self._try_merge(pull_request)
1165 log.debug("Merge response: %s", resp)
1162 log.debug("Merge response: %s", resp)
1166 status = resp.possible, self.merge_status_message(
1163 status = resp.possible, self.merge_status_message(
1167 resp.failure_reason)
1164 resp.failure_reason)
1168 except NotImplementedError:
1165 except NotImplementedError:
1169 status = False, _('Pull request merging is not supported.')
1166 status = False, _('Pull request merging is not supported.')
1170
1167
1171 return status
1168 return status
1172
1169
1173 def _check_repo_requirements(self, target, source):
1170 def _check_repo_requirements(self, target, source):
1174 """
1171 """
1175 Check if `target` and `source` have compatible requirements.
1172 Check if `target` and `source` have compatible requirements.
1176
1173
1177 Currently this is just checking for largefiles.
1174 Currently this is just checking for largefiles.
1178 """
1175 """
1179 target_has_largefiles = self._has_largefiles(target)
1176 target_has_largefiles = self._has_largefiles(target)
1180 source_has_largefiles = self._has_largefiles(source)
1177 source_has_largefiles = self._has_largefiles(source)
1181 merge_possible = True
1178 merge_possible = True
1182 message = u''
1179 message = u''
1183
1180
1184 if target_has_largefiles != source_has_largefiles:
1181 if target_has_largefiles != source_has_largefiles:
1185 merge_possible = False
1182 merge_possible = False
1186 if source_has_largefiles:
1183 if source_has_largefiles:
1187 message = _(
1184 message = _(
1188 'Target repository large files support is disabled.')
1185 'Target repository large files support is disabled.')
1189 else:
1186 else:
1190 message = _(
1187 message = _(
1191 'Source repository large files support is disabled.')
1188 'Source repository large files support is disabled.')
1192
1189
1193 return merge_possible, message
1190 return merge_possible, message
1194
1191
1195 def _has_largefiles(self, repo):
1192 def _has_largefiles(self, repo):
1196 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1197 'extensions', 'largefiles')
1194 'extensions', 'largefiles')
1198 return largefiles_ui and largefiles_ui[0].active
1195 return largefiles_ui and largefiles_ui[0].active
1199
1196
1200 def _try_merge(self, pull_request):
1197 def _try_merge(self, pull_request):
1201 """
1198 """
1202 Try to merge the pull request and return the merge status.
1199 Try to merge the pull request and return the merge status.
1203 """
1200 """
1204 log.debug(
1201 log.debug(
1205 "Trying out if the pull request %s can be merged.",
1202 "Trying out if the pull request %s can be merged.",
1206 pull_request.pull_request_id)
1203 pull_request.pull_request_id)
1207 target_vcs = pull_request.target_repo.scm_instance()
1204 target_vcs = pull_request.target_repo.scm_instance()
1208
1205
1209 # Refresh the target reference.
1206 # Refresh the target reference.
1210 try:
1207 try:
1211 target_ref = self._refresh_reference(
1208 target_ref = self._refresh_reference(
1212 pull_request.target_ref_parts, target_vcs)
1209 pull_request.target_ref_parts, target_vcs)
1213 except CommitDoesNotExistError:
1210 except CommitDoesNotExistError:
1214 merge_state = MergeResponse(
1211 merge_state = MergeResponse(
1215 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1216 return merge_state
1213 return merge_state
1217
1214
1218 target_locked = pull_request.target_repo.locked
1215 target_locked = pull_request.target_repo.locked
1219 if target_locked and target_locked[0]:
1216 if target_locked and target_locked[0]:
1220 log.debug("The target repository is locked.")
1217 log.debug("The target repository is locked.")
1221 merge_state = MergeResponse(
1218 merge_state = MergeResponse(
1222 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1223 elif self._needs_merge_state_refresh(pull_request, target_ref):
1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1224 log.debug("Refreshing the merge status of the repository.")
1221 log.debug("Refreshing the merge status of the repository.")
1225 merge_state = self._refresh_merge_state(
1222 merge_state = self._refresh_merge_state(
1226 pull_request, target_vcs, target_ref)
1223 pull_request, target_vcs, target_ref)
1227 else:
1224 else:
1228 possible = pull_request.\
1225 possible = pull_request.\
1229 _last_merge_status == MergeFailureReason.NONE
1226 _last_merge_status == MergeFailureReason.NONE
1230 merge_state = MergeResponse(
1227 merge_state = MergeResponse(
1231 possible, False, None, pull_request._last_merge_status)
1228 possible, False, None, pull_request._last_merge_status)
1232
1229
1233 return merge_state
1230 return merge_state
1234
1231
1235 def _refresh_reference(self, reference, vcs_repository):
1232 def _refresh_reference(self, reference, vcs_repository):
1236 if reference.type in ('branch', 'book'):
1233 if reference.type in ('branch', 'book'):
1237 name_or_id = reference.name
1234 name_or_id = reference.name
1238 else:
1235 else:
1239 name_or_id = reference.commit_id
1236 name_or_id = reference.commit_id
1240 refreshed_commit = vcs_repository.get_commit(name_or_id)
1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1241 refreshed_reference = Reference(
1238 refreshed_reference = Reference(
1242 reference.type, reference.name, refreshed_commit.raw_id)
1239 reference.type, reference.name, refreshed_commit.raw_id)
1243 return refreshed_reference
1240 return refreshed_reference
1244
1241
1245 def _needs_merge_state_refresh(self, pull_request, target_reference):
1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1246 return not(
1243 return not(
1247 pull_request.revisions and
1244 pull_request.revisions and
1248 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1249 target_reference.commit_id == pull_request._last_merge_target_rev)
1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1250
1247
1251 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1252 workspace_id = self._workspace_id(pull_request)
1249 workspace_id = self._workspace_id(pull_request)
1253 source_vcs = pull_request.source_repo.scm_instance()
1250 source_vcs = pull_request.source_repo.scm_instance()
1254 use_rebase = self._use_rebase_for_merging(pull_request)
1251 use_rebase = self._use_rebase_for_merging(pull_request)
1255 merge_state = target_vcs.merge(
1252 merge_state = target_vcs.merge(
1256 target_reference, source_vcs, pull_request.source_ref_parts,
1253 target_reference, source_vcs, pull_request.source_ref_parts,
1257 workspace_id, dry_run=True, use_rebase=use_rebase)
1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1258
1255
1259 # Do not store the response if there was an unknown error.
1256 # Do not store the response if there was an unknown error.
1260 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1261 pull_request._last_merge_source_rev = \
1258 pull_request._last_merge_source_rev = \
1262 pull_request.source_ref_parts.commit_id
1259 pull_request.source_ref_parts.commit_id
1263 pull_request._last_merge_target_rev = target_reference.commit_id
1260 pull_request._last_merge_target_rev = target_reference.commit_id
1264 pull_request._last_merge_status = merge_state.failure_reason
1261 pull_request._last_merge_status = merge_state.failure_reason
1265 pull_request.shadow_merge_ref = merge_state.merge_ref
1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1266 Session().add(pull_request)
1263 Session().add(pull_request)
1267 Session().commit()
1264 Session().commit()
1268
1265
1269 return merge_state
1266 return merge_state
1270
1267
1271 def _workspace_id(self, pull_request):
1268 def _workspace_id(self, pull_request):
1272 workspace_id = 'pr-%s' % pull_request.pull_request_id
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1273 return workspace_id
1270 return workspace_id
1274
1271
1275 def merge_status_message(self, status_code):
1272 def merge_status_message(self, status_code):
1276 """
1273 """
1277 Return a human friendly error message for the given merge status code.
1274 Return a human friendly error message for the given merge status code.
1278 """
1275 """
1279 return self.MERGE_STATUS_MESSAGES[status_code]
1276 return self.MERGE_STATUS_MESSAGES[status_code]
1280
1277
1281 def generate_repo_data(self, repo, commit_id=None, branch=None,
1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1282 bookmark=None):
1279 bookmark=None):
1283 all_refs, selected_ref = \
1280 all_refs, selected_ref = \
1284 self._get_repo_pullrequest_sources(
1281 self._get_repo_pullrequest_sources(
1285 repo.scm_instance(), commit_id=commit_id,
1282 repo.scm_instance(), commit_id=commit_id,
1286 branch=branch, bookmark=bookmark)
1283 branch=branch, bookmark=bookmark)
1287
1284
1288 refs_select2 = []
1285 refs_select2 = []
1289 for element in all_refs:
1286 for element in all_refs:
1290 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1291 refs_select2.append({'text': element[1], 'children': children})
1288 refs_select2.append({'text': element[1], 'children': children})
1292
1289
1293 return {
1290 return {
1294 'user': {
1291 'user': {
1295 'user_id': repo.user.user_id,
1292 'user_id': repo.user.user_id,
1296 'username': repo.user.username,
1293 'username': repo.user.username,
1297 'firstname': repo.user.firstname,
1294 'firstname': repo.user.firstname,
1298 'lastname': repo.user.lastname,
1295 'lastname': repo.user.lastname,
1299 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1300 },
1297 },
1301 'description': h.chop_at_smart(repo.description, '\n'),
1298 'description': h.chop_at_smart(repo.description, '\n'),
1302 'refs': {
1299 'refs': {
1303 'all_refs': all_refs,
1300 'all_refs': all_refs,
1304 'selected_ref': selected_ref,
1301 'selected_ref': selected_ref,
1305 'select2_refs': refs_select2
1302 'select2_refs': refs_select2
1306 }
1303 }
1307 }
1304 }
1308
1305
1309 def generate_pullrequest_title(self, source, source_ref, target):
1306 def generate_pullrequest_title(self, source, source_ref, target):
1310 return u'{source}#{at_ref} to {target}'.format(
1307 return u'{source}#{at_ref} to {target}'.format(
1311 source=source,
1308 source=source,
1312 at_ref=source_ref,
1309 at_ref=source_ref,
1313 target=target,
1310 target=target,
1314 )
1311 )
1315
1312
1316 def _cleanup_merge_workspace(self, pull_request):
1313 def _cleanup_merge_workspace(self, pull_request):
1317 # Merging related cleanup
1314 # Merging related cleanup
1318 target_scm = pull_request.target_repo.scm_instance()
1315 target_scm = pull_request.target_repo.scm_instance()
1319 workspace_id = 'pr-%s' % pull_request.pull_request_id
1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1320
1317
1321 try:
1318 try:
1322 target_scm.cleanup_merge_workspace(workspace_id)
1319 target_scm.cleanup_merge_workspace(workspace_id)
1323 except NotImplementedError:
1320 except NotImplementedError:
1324 pass
1321 pass
1325
1322
1326 def _get_repo_pullrequest_sources(
1323 def _get_repo_pullrequest_sources(
1327 self, repo, commit_id=None, branch=None, bookmark=None):
1324 self, repo, commit_id=None, branch=None, bookmark=None):
1328 """
1325 """
1329 Return a structure with repo's interesting commits, suitable for
1326 Return a structure with repo's interesting commits, suitable for
1330 the selectors in pullrequest controller
1327 the selectors in pullrequest controller
1331
1328
1332 :param commit_id: a commit that must be in the list somehow
1329 :param commit_id: a commit that must be in the list somehow
1333 and selected by default
1330 and selected by default
1334 :param branch: a branch that must be in the list and selected
1331 :param branch: a branch that must be in the list and selected
1335 by default - even if closed
1332 by default - even if closed
1336 :param bookmark: a bookmark that must be in the list and selected
1333 :param bookmark: a bookmark that must be in the list and selected
1337 """
1334 """
1338
1335
1339 commit_id = safe_str(commit_id) if commit_id else None
1336 commit_id = safe_str(commit_id) if commit_id else None
1340 branch = safe_str(branch) if branch else None
1337 branch = safe_str(branch) if branch else None
1341 bookmark = safe_str(bookmark) if bookmark else None
1338 bookmark = safe_str(bookmark) if bookmark else None
1342
1339
1343 selected = None
1340 selected = None
1344
1341
1345 # order matters: first source that has commit_id in it will be selected
1342 # order matters: first source that has commit_id in it will be selected
1346 sources = []
1343 sources = []
1347 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1348 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1349
1346
1350 if commit_id:
1347 if commit_id:
1351 ref_commit = (h.short_id(commit_id), commit_id)
1348 ref_commit = (h.short_id(commit_id), commit_id)
1352 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1353
1350
1354 sources.append(
1351 sources.append(
1355 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1356 )
1353 )
1357
1354
1358 groups = []
1355 groups = []
1359 for group_key, ref_list, group_name, match in sources:
1356 for group_key, ref_list, group_name, match in sources:
1360 group_refs = []
1357 group_refs = []
1361 for ref_name, ref_id in ref_list:
1358 for ref_name, ref_id in ref_list:
1362 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1363 group_refs.append((ref_key, ref_name))
1360 group_refs.append((ref_key, ref_name))
1364
1361
1365 if not selected:
1362 if not selected:
1366 if set([commit_id, match]) & set([ref_id, ref_name]):
1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1367 selected = ref_key
1364 selected = ref_key
1368
1365
1369 if group_refs:
1366 if group_refs:
1370 groups.append((group_refs, group_name))
1367 groups.append((group_refs, group_name))
1371
1368
1372 if not selected:
1369 if not selected:
1373 ref = commit_id or branch or bookmark
1370 ref = commit_id or branch or bookmark
1374 if ref:
1371 if ref:
1375 raise CommitDoesNotExistError(
1372 raise CommitDoesNotExistError(
1376 'No commit refs could be found matching: %s' % ref)
1373 'No commit refs could be found matching: %s' % ref)
1377 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1378 selected = 'branch:%s:%s' % (
1375 selected = 'branch:%s:%s' % (
1379 repo.DEFAULT_BRANCH_NAME,
1376 repo.DEFAULT_BRANCH_NAME,
1380 repo.branches[repo.DEFAULT_BRANCH_NAME]
1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1381 )
1378 )
1382 elif repo.commit_ids:
1379 elif repo.commit_ids:
1383 rev = repo.commit_ids[0]
1380 rev = repo.commit_ids[0]
1384 selected = 'rev:%s:%s' % (rev, rev)
1381 selected = 'rev:%s:%s' % (rev, rev)
1385 else:
1382 else:
1386 raise EmptyRepositoryError()
1383 raise EmptyRepositoryError()
1387 return groups, selected
1384 return groups, selected
1388
1385
1389 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1390 return self._get_diff_from_pr_or_version(
1387 return self._get_diff_from_pr_or_version(
1391 source_repo, source_ref_id, target_ref_id, context=context)
1388 source_repo, source_ref_id, target_ref_id, context=context)
1392
1389
1393 def _get_diff_from_pr_or_version(
1390 def _get_diff_from_pr_or_version(
1394 self, source_repo, source_ref_id, target_ref_id, context):
1391 self, source_repo, source_ref_id, target_ref_id, context):
1395 target_commit = source_repo.get_commit(
1392 target_commit = source_repo.get_commit(
1396 commit_id=safe_str(target_ref_id))
1393 commit_id=safe_str(target_ref_id))
1397 source_commit = source_repo.get_commit(
1394 source_commit = source_repo.get_commit(
1398 commit_id=safe_str(source_ref_id))
1395 commit_id=safe_str(source_ref_id))
1399 if isinstance(source_repo, Repository):
1396 if isinstance(source_repo, Repository):
1400 vcs_repo = source_repo.scm_instance()
1397 vcs_repo = source_repo.scm_instance()
1401 else:
1398 else:
1402 vcs_repo = source_repo
1399 vcs_repo = source_repo
1403
1400
1404 # TODO: johbo: In the context of an update, we cannot reach
1401 # TODO: johbo: In the context of an update, we cannot reach
1405 # the old commit anymore with our normal mechanisms. It needs
1402 # the old commit anymore with our normal mechanisms. It needs
1406 # some sort of special support in the vcs layer to avoid this
1403 # some sort of special support in the vcs layer to avoid this
1407 # workaround.
1404 # workaround.
1408 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1409 vcs_repo.alias == 'git'):
1406 vcs_repo.alias == 'git'):
1410 source_commit.raw_id = safe_str(source_ref_id)
1407 source_commit.raw_id = safe_str(source_ref_id)
1411
1408
1412 log.debug('calculating diff between '
1409 log.debug('calculating diff between '
1413 'source_ref:%s and target_ref:%s for repo `%s`',
1410 'source_ref:%s and target_ref:%s for repo `%s`',
1414 target_ref_id, source_ref_id,
1411 target_ref_id, source_ref_id,
1415 safe_unicode(vcs_repo.path))
1412 safe_unicode(vcs_repo.path))
1416
1413
1417 vcs_diff = vcs_repo.get_diff(
1414 vcs_diff = vcs_repo.get_diff(
1418 commit1=target_commit, commit2=source_commit, context=context)
1415 commit1=target_commit, commit2=source_commit, context=context)
1419 return vcs_diff
1416 return vcs_diff
1420
1417
1421 def _is_merge_enabled(self, pull_request):
1418 def _is_merge_enabled(self, pull_request):
1422 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1423 settings = settings_model.get_general_settings()
1420 settings = settings_model.get_general_settings()
1424 return settings.get('rhodecode_pr_merge_enabled', False)
1421 return settings.get('rhodecode_pr_merge_enabled', False)
1425
1422
1426 def _use_rebase_for_merging(self, pull_request):
1423 def _use_rebase_for_merging(self, pull_request):
1427 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1428 settings = settings_model.get_general_settings()
1425 settings = settings_model.get_general_settings()
1429 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1430
1427
1431 def _log_audit_action(self, action, action_data, user, pull_request):
1428 def _log_audit_action(self, action, action_data, user, pull_request):
1432 audit_logger.store(
1429 audit_logger.store(
1433 action=action,
1430 action=action,
1434 action_data=action_data,
1431 action_data=action_data,
1435 user=user,
1432 user=user,
1436 repo=pull_request.target_repo)
1433 repo=pull_request.target_repo)
1437
1434
1438 def get_reviewer_functions(self):
1435 def get_reviewer_functions(self):
1439 """
1436 """
1440 Fetches functions for validation and fetching default reviewers.
1437 Fetches functions for validation and fetching default reviewers.
1441 If available we use the EE package, else we fallback to CE
1438 If available we use the EE package, else we fallback to CE
1442 package functions
1439 package functions
1443 """
1440 """
1444 try:
1441 try:
1445 from rc_reviewers.utils import get_default_reviewers_data
1442 from rc_reviewers.utils import get_default_reviewers_data
1446 from rc_reviewers.utils import validate_default_reviewers
1443 from rc_reviewers.utils import validate_default_reviewers
1447 except ImportError:
1444 except ImportError:
1448 from rhodecode.apps.repository.utils import \
1445 from rhodecode.apps.repository.utils import \
1449 get_default_reviewers_data
1446 get_default_reviewers_data
1450 from rhodecode.apps.repository.utils import \
1447 from rhodecode.apps.repository.utils import \
1451 validate_default_reviewers
1448 validate_default_reviewers
1452
1449
1453 return get_default_reviewers_data, validate_default_reviewers
1450 return get_default_reviewers_data, validate_default_reviewers
1454
1451
1455
1452
1456 class MergeCheck(object):
1453 class MergeCheck(object):
1457 """
1454 """
1458 Perform Merge Checks and returns a check object which stores information
1455 Perform Merge Checks and returns a check object which stores information
1459 about merge errors, and merge conditions
1456 about merge errors, and merge conditions
1460 """
1457 """
1461 TODO_CHECK = 'todo'
1458 TODO_CHECK = 'todo'
1462 PERM_CHECK = 'perm'
1459 PERM_CHECK = 'perm'
1463 REVIEW_CHECK = 'review'
1460 REVIEW_CHECK = 'review'
1464 MERGE_CHECK = 'merge'
1461 MERGE_CHECK = 'merge'
1465
1462
1466 def __init__(self):
1463 def __init__(self):
1467 self.review_status = None
1464 self.review_status = None
1468 self.merge_possible = None
1465 self.merge_possible = None
1469 self.merge_msg = ''
1466 self.merge_msg = ''
1470 self.failed = None
1467 self.failed = None
1471 self.errors = []
1468 self.errors = []
1472 self.error_details = OrderedDict()
1469 self.error_details = OrderedDict()
1473
1470
1474 def push_error(self, error_type, message, error_key, details):
1471 def push_error(self, error_type, message, error_key, details):
1475 self.failed = True
1472 self.failed = True
1476 self.errors.append([error_type, message])
1473 self.errors.append([error_type, message])
1477 self.error_details[error_key] = dict(
1474 self.error_details[error_key] = dict(
1478 details=details,
1475 details=details,
1479 error_type=error_type,
1476 error_type=error_type,
1480 message=message
1477 message=message
1481 )
1478 )
1482
1479
1483 @classmethod
1480 @classmethod
1484 def validate(cls, pull_request, user, fail_early=False, translator=None):
1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1485 # if migrated to pyramid...
1482 # if migrated to pyramid...
1486 # _ = lambda: translator or _ # use passed in translator if any
1483 # _ = lambda: translator or _ # use passed in translator if any
1487
1484
1488 merge_check = cls()
1485 merge_check = cls()
1489
1486
1490 # permissions to merge
1487 # permissions to merge
1491 user_allowed_to_merge = PullRequestModel().check_user_merge(
1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1492 pull_request, user)
1489 pull_request, user)
1493 if not user_allowed_to_merge:
1490 if not user_allowed_to_merge:
1494 log.debug("MergeCheck: cannot merge, approval is pending.")
1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1495
1492
1496 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1497 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1498 if fail_early:
1495 if fail_early:
1499 return merge_check
1496 return merge_check
1500
1497
1501 # review status, must be always present
1498 # review status, must be always present
1502 review_status = pull_request.calculated_review_status()
1499 review_status = pull_request.calculated_review_status()
1503 merge_check.review_status = review_status
1500 merge_check.review_status = review_status
1504
1501
1505 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1506 if not status_approved:
1503 if not status_approved:
1507 log.debug("MergeCheck: cannot merge, approval is pending.")
1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1508
1505
1509 msg = _('Pull request reviewer approval is pending.')
1506 msg = _('Pull request reviewer approval is pending.')
1510
1507
1511 merge_check.push_error(
1508 merge_check.push_error(
1512 'warning', msg, cls.REVIEW_CHECK, review_status)
1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1513
1510
1514 if fail_early:
1511 if fail_early:
1515 return merge_check
1512 return merge_check
1516
1513
1517 # left over TODOs
1514 # left over TODOs
1518 todos = CommentsModel().get_unresolved_todos(pull_request)
1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1519 if todos:
1516 if todos:
1520 log.debug("MergeCheck: cannot merge, {} "
1517 log.debug("MergeCheck: cannot merge, {} "
1521 "unresolved todos left.".format(len(todos)))
1518 "unresolved todos left.".format(len(todos)))
1522
1519
1523 if len(todos) == 1:
1520 if len(todos) == 1:
1524 msg = _('Cannot merge, {} TODO still not resolved.').format(
1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1525 len(todos))
1522 len(todos))
1526 else:
1523 else:
1527 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1528 len(todos))
1525 len(todos))
1529
1526
1530 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1531
1528
1532 if fail_early:
1529 if fail_early:
1533 return merge_check
1530 return merge_check
1534
1531
1535 # merge possible
1532 # merge possible
1536 merge_status, msg = PullRequestModel().merge_status(pull_request)
1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1537 merge_check.merge_possible = merge_status
1534 merge_check.merge_possible = merge_status
1538 merge_check.merge_msg = msg
1535 merge_check.merge_msg = msg
1539 if not merge_status:
1536 if not merge_status:
1540 log.debug(
1537 log.debug(
1541 "MergeCheck: cannot merge, pull request merge not possible.")
1538 "MergeCheck: cannot merge, pull request merge not possible.")
1542 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1543
1540
1544 if fail_early:
1541 if fail_early:
1545 return merge_check
1542 return merge_check
1546
1543
1547 return merge_check
1544 return merge_check
1548
1545
1549
1546
1550 ChangeTuple = namedtuple('ChangeTuple',
1547 ChangeTuple = namedtuple('ChangeTuple',
1551 ['added', 'common', 'removed', 'total'])
1548 ['added', 'common', 'removed', 'total'])
1552
1549
1553 FileChangeTuple = namedtuple('FileChangeTuple',
1550 FileChangeTuple = namedtuple('FileChangeTuple',
1554 ['added', 'modified', 'removed'])
1551 ['added', 'modified', 'removed'])
@@ -1,142 +1,142 b''
1 ## small box that displays changed/added/removed details fetched by AJAX
1 ## small box that displays changed/added/removed details fetched by AJAX
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4
4
5 % if c.prev_page:
5 % if c.prev_page:
6 <tr>
6 <tr>
7 <td colspan="9" class="load-more-commits">
7 <td colspan="9" class="load-more-commits">
8 <a class="prev-commits" href="#loadPrevCommits" onclick="commitsController.loadPrev(this, ${c.prev_page}, '${c.branch_name}');return false">
8 <a class="prev-commits" href="#loadPrevCommits" onclick="commitsController.loadPrev(this, ${c.prev_page}, '${c.branch_name}');return false">
9 ${_('load previous')}
9 ${_('load previous')}
10 </a>
10 </a>
11 </td>
11 </td>
12 </tr>
12 </tr>
13 % endif
13 % endif
14
14
15 % for cnt,commit in enumerate(c.pagination):
15 % for cnt,commit in enumerate(c.pagination):
16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
17
17
18 <td class="td-checkbox">
18 <td class="td-checkbox">
19 ${h.checkbox(commit.raw_id,class_="commit-range")}
19 ${h.checkbox(commit.raw_id,class_="commit-range")}
20 </td>
20 </td>
21 <td class="td-status">
21 <td class="td-status">
22
22
23 %if c.statuses.get(commit.raw_id):
23 %if c.statuses.get(commit.raw_id):
24 <div class="changeset-status-ico">
24 <div class="changeset-status-ico">
25 %if c.statuses.get(commit.raw_id)[2]:
25 %if c.statuses.get(commit.raw_id)[2]:
26 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
26 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
27 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
27 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
28 </a>
28 </a>
29 %else:
29 %else:
30 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(commit.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
30 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(commit.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
31 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
31 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
32 </a>
32 </a>
33 %endif
33 %endif
34 </div>
34 </div>
35 %else:
35 %else:
36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
37 %endif
37 %endif
38 </td>
38 </td>
39 <td class="td-comments comments-col">
39 <td class="td-comments comments-col">
40 %if c.comments.get(commit.raw_id):
40 %if c.comments.get(commit.raw_id):
41 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
41 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
43 </a>
43 </a>
44 %endif
44 %endif
45 </td>
45 </td>
46 <td class="td-hash">
46 <td class="td-hash">
47 <code>
47 <code>
48 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">
48 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">
49 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
49 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
50 </a>
50 </a>
51 % if hasattr(commit, 'phase'):
51 % if hasattr(commit, 'phase'):
52 % if commit.phase != 'public':
52 % if commit.phase != 'public':
53 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
53 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
54 % endif
54 % endif
55 % endif
55 % endif
56
56
57 ## obsolete commits
57 ## obsolete commits
58 % if hasattr(commit, 'obsolete'):
58 % if hasattr(commit, 'obsolete'):
59 % if commit.obsolete:
59 % if commit.obsolete:
60 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
60 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
61 % endif
61 % endif
62 % endif
62 % endif
63
63
64 ## hidden commits
64 ## hidden commits
65 % if hasattr(commit, 'hidden'):
65 % if hasattr(commit, 'hidden'):
66 % if commit.hidden:
66 % if commit.hidden:
67 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
67 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
68 % endif
68 % endif
69 % endif
69 % endif
70
70
71 </code>
71 </code>
72 </td>
72 </td>
73 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
73 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
74 <div class="show_more_col">
74 <div class="show_more_col">
75 <i class="show_more"></i>&nbsp;
75 <i class="show_more"></i>&nbsp;
76 </div>
76 </div>
77 </td>
77 </td>
78 <td class="td-description mid">
78 <td class="td-description mid">
79 <div class="log-container truncate-wrap">
79 <div class="log-container truncate-wrap">
80 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
80 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
81 </div>
81 </div>
82 </td>
82 </td>
83
83
84 <td class="td-time">
84 <td class="td-time">
85 ${h.age_component(commit.date)}
85 ${h.age_component(commit.date)}
86 </td>
86 </td>
87 <td class="td-user">
87 <td class="td-user">
88 ${base.gravatar_with_user(commit.author)}
88 ${base.gravatar_with_user(commit.author)}
89 </td>
89 </td>
90
90
91 <td class="td-tags tags-col">
91 <td class="td-tags tags-col">
92 <div id="t-${commit.raw_id}">
92 <div id="t-${commit.raw_id}">
93
93
94 ## merge
94 ## merge
95 %if commit.merge:
95 %if commit.merge:
96 <span class="tag mergetag">
96 <span class="tag mergetag">
97 <i class="icon-merge"></i>${_('merge')}
97 <i class="icon-merge"></i>${_('merge')}
98 </span>
98 </span>
99 %endif
99 %endif
100
100
101 ## branch
101 ## branch
102 %if commit.branch:
102 %if commit.branch:
103 <span class="tag branchtag" title="${_('Branch %s') % commit.branch}">
103 <span class="tag branchtag" title="${_('Branch %s') % commit.branch}">
104 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=commit.branch)}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
104 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=commit.branch)}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
105 </span>
105 </span>
106 %endif
106 %endif
107
107
108 ## bookmarks
108 ## bookmarks
109 %if h.is_hg(c.rhodecode_repo):
109 %if h.is_hg(c.rhodecode_repo):
110 %for book in commit.bookmarks:
110 %for book in commit.bookmarks:
111 <span class="tag booktag" title="${_('Bookmark %s') % book}">
111 <span class="tag booktag" title="${_('Bookmark %s') % book}">
112 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
112 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
113 </span>
113 </span>
114 %endfor
114 %endfor
115 %endif
115 %endif
116
116
117 ## tags
117 ## tags
118 %for tag in commit.tags:
118 %for tag in commit.tags:
119 <span class="tag tagtag" title="${_('Tag %s') % tag}">
119 <span class="tag tagtag" title="${_('Tag %s') % tag}">
120 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
120 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
121 </span>
121 </span>
122 %endfor
122 %endfor
123
123
124 </div>
124 </div>
125 </td>
125 </td>
126 </tr>
126 </tr>
127 % endfor
127 % endfor
128
128
129 % if c.next_page:
129 % if c.next_page:
130 <tr>
130 <tr>
131 <td colspan="9" class="load-more-commits">
131 <td colspan="9" class="load-more-commits">
132 <a class="next-commits" href="#loadNextCommits" onclick="commitsController.loadNext(this, ${c.next_page}, '${c.branch_name}');return false">
132 <a class="next-commits" href="#loadNextCommits" onclick="commitsController.loadNext(this, ${c.next_page}, '${c.branch_name}');return false">
133 ${_('load next')}
133 ${_('load next')}
134 </a>
134 </a>
135 </td>
135 </td>
136 </tr>
136 </tr>
137 % endif
137 % endif
138 <tr class="chunk-graph-data" style="display:none"
138 <tr class="chunk-graph-data" style="display:none"
139 data-graph='${c.graph_data|n}'
139 data-graph='${c.graph_data|n}'
140 data-node='${c.prev_page}:${c.next_page}'
140 data-node='${c.prev_page}:${c.next_page}'
141 data-commits='${c.graph_commits|n}'>
141 data-commits='${c.graph_commits|n}'>
142 </tr> No newline at end of file
142 </tr>
@@ -1,405 +1,405 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 % if inline:
10 % if inline:
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 % else:
12 % else:
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 % endif
14 % endif
15
15
16
16
17 <div class="comment
17 <div class="comment
18 ${'comment-inline' if inline else 'comment-general'}
18 ${'comment-inline' if inline else 'comment-general'}
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 id="comment-${comment.comment_id}"
20 id="comment-${comment.comment_id}"
21 line="${comment.line_no}"
21 line="${comment.line_no}"
22 data-comment-id="${comment.comment_id}"
22 data-comment-id="${comment.comment_id}"
23 data-comment-type="${comment.comment_type}"
23 data-comment-type="${comment.comment_type}"
24 data-comment-inline=${h.json.dumps(inline)}
24 data-comment-inline=${h.json.dumps(inline)}
25 style="${'display: none;' if outdated_at_ver else ''}">
25 style="${'display: none;' if outdated_at_ver else ''}">
26
26
27 <div class="meta">
27 <div class="meta">
28 <div class="comment-type-label">
28 <div class="comment-type-label">
29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
30 % if comment.comment_type == 'todo':
30 % if comment.comment_type == 'todo':
31 % if comment.resolved:
31 % if comment.resolved:
32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
34 </div>
34 </div>
35 % else:
35 % else:
36 <div class="resolved tooltip" style="display: none">
36 <div class="resolved tooltip" style="display: none">
37 <span>${comment.comment_type}</span>
37 <span>${comment.comment_type}</span>
38 </div>
38 </div>
39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
40 ${comment.comment_type}
40 ${comment.comment_type}
41 </div>
41 </div>
42 % endif
42 % endif
43 % else:
43 % else:
44 % if comment.resolved_comment:
44 % if comment.resolved_comment:
45 fix
45 fix
46 % else:
46 % else:
47 ${comment.comment_type or 'note'}
47 ${comment.comment_type or 'note'}
48 % endif
48 % endif
49 % endif
49 % endif
50 </div>
50 </div>
51 </div>
51 </div>
52
52
53 <div class="author ${'author-inline' if inline else 'author-general'}">
53 <div class="author ${'author-inline' if inline else 'author-general'}">
54 ${base.gravatar_with_user(comment.author.email, 16)}
54 ${base.gravatar_with_user(comment.author.email, 16)}
55 </div>
55 </div>
56 <div class="date">
56 <div class="date">
57 ${h.age_component(comment.modified_at, time_is_local=True)}
57 ${h.age_component(comment.modified_at, time_is_local=True)}
58 </div>
58 </div>
59 % if inline:
59 % if inline:
60 <span></span>
60 <span></span>
61 % else:
61 % else:
62 <div class="status-change">
62 <div class="status-change">
63 % if comment.pull_request:
63 % if comment.pull_request:
64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
64 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
65 % if comment.status_change:
65 % if comment.status_change:
66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
67 % else:
67 % else:
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
69 % endif
69 % endif
70 </a>
70 </a>
71 % else:
71 % else:
72 % if comment.status_change:
72 % if comment.status_change:
73 ${_('Status change on commit')}:
73 ${_('Status change on commit')}:
74 % endif
74 % endif
75 % endif
75 % endif
76 </div>
76 </div>
77 % endif
77 % endif
78
78
79 % if comment.status_change:
79 % if comment.status_change:
80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
81 <div title="${_('Commit status')}" class="changeset-status-lbl">
81 <div title="${_('Commit status')}" class="changeset-status-lbl">
82 ${comment.status_change[0].status_lbl}
82 ${comment.status_change[0].status_lbl}
83 </div>
83 </div>
84 % endif
84 % endif
85
85
86 % if comment.resolved_comment:
86 % if comment.resolved_comment:
87 <a class="has-spacer-before" href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
87 <a class="has-spacer-before" href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
88 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
88 ${_('resolves comment #{}').format(comment.resolved_comment.comment_id)}
89 </a>
89 </a>
90 % endif
90 % endif
91
91
92 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
92 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
93
93
94 <div class="comment-links-block">
94 <div class="comment-links-block">
95 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
95 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
96 <span class="tag authortag tooltip" title="${_('Pull request author')}">
96 <span class="tag authortag tooltip" title="${_('Pull request author')}">
97 ${_('author')}
97 ${_('author')}
98 </span>
98 </span>
99 |
99 |
100 % endif
100 % endif
101 % if inline:
101 % if inline:
102 <div class="pr-version-inline">
102 <div class="pr-version-inline">
103 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
103 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
104 % if outdated_at_ver:
104 % if outdated_at_ver:
105 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
105 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
106 outdated ${'v{}'.format(pr_index_ver)} |
106 outdated ${'v{}'.format(pr_index_ver)} |
107 </code>
107 </code>
108 % elif pr_index_ver:
108 % elif pr_index_ver:
109 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
109 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
110 ${'v{}'.format(pr_index_ver)} |
110 ${'v{}'.format(pr_index_ver)} |
111 </code>
111 </code>
112 % endif
112 % endif
113 </a>
113 </a>
114 </div>
114 </div>
115 % else:
115 % else:
116 % if comment.pull_request_version_id and pr_index_ver:
116 % if comment.pull_request_version_id and pr_index_ver:
117 |
117 |
118 <div class="pr-version">
118 <div class="pr-version">
119 % if comment.outdated:
119 % if comment.outdated:
120 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
120 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
121 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
121 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
122 </a>
122 </a>
123 % else:
123 % else:
124 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
124 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
125 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
125 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
126 <code class="pr-version-num">
126 <code class="pr-version-num">
127 ${'v{}'.format(pr_index_ver)}
127 ${'v{}'.format(pr_index_ver)}
128 </code>
128 </code>
129 </a>
129 </a>
130 </div>
130 </div>
131 % endif
131 % endif
132 </div>
132 </div>
133 % endif
133 % endif
134 % endif
134 % endif
135
135
136 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
136 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
137 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
137 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
138 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
138 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
139 ## permissions to delete
139 ## permissions to delete
140 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
140 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
141 ## TODO: dan: add edit comment here
141 ## TODO: dan: add edit comment here
142 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
142 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
143 %else:
143 %else:
144 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
144 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
145 %endif
145 %endif
146 %else:
146 %else:
147 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
147 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
148 %endif
148 %endif
149
149
150 % if outdated_at_ver:
150 % if outdated_at_ver:
151 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
151 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
152 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
152 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
153 % else:
153 % else:
154 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
154 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
155 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
155 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
156 % endif
156 % endif
157
157
158 </div>
158 </div>
159 </div>
159 </div>
160 <div class="text">
160 <div class="text">
161 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
161 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
162 </div>
162 </div>
163
163
164 </div>
164 </div>
165 </%def>
165 </%def>
166
166
167 ## generate main comments
167 ## generate main comments
168 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
168 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
169 <div class="general-comments" id="comments">
169 <div class="general-comments" id="comments">
170 %for comment in comments:
170 %for comment in comments:
171 <div id="comment-tr-${comment.comment_id}">
171 <div id="comment-tr-${comment.comment_id}">
172 ## only render comments that are not from pull request, or from
172 ## only render comments that are not from pull request, or from
173 ## pull request and a status change
173 ## pull request and a status change
174 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
174 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
175 ${comment_block(comment)}
175 ${comment_block(comment)}
176 %endif
176 %endif
177 </div>
177 </div>
178 %endfor
178 %endfor
179 ## to anchor ajax comments
179 ## to anchor ajax comments
180 <div id="injected_page_comments"></div>
180 <div id="injected_page_comments"></div>
181 </div>
181 </div>
182 </%def>
182 </%def>
183
183
184
184
185 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
185 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
186
186
187 <div class="comments">
187 <div class="comments">
188 <%
188 <%
189 if is_pull_request:
189 if is_pull_request:
190 placeholder = _('Leave a comment on this Pull Request.')
190 placeholder = _('Leave a comment on this Pull Request.')
191 elif is_compare:
191 elif is_compare:
192 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
192 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
193 else:
193 else:
194 placeholder = _('Leave a comment on this Commit.')
194 placeholder = _('Leave a comment on this Commit.')
195 %>
195 %>
196
196
197 % if c.rhodecode_user.username != h.DEFAULT_USER:
197 % if c.rhodecode_user.username != h.DEFAULT_USER:
198 <div class="js-template" id="cb-comment-general-form-template">
198 <div class="js-template" id="cb-comment-general-form-template">
199 ## template generated for injection
199 ## template generated for injection
200 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
200 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
201 </div>
201 </div>
202
202
203 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
203 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
204 ## inject form here
204 ## inject form here
205 </div>
205 </div>
206 <script type="text/javascript">
206 <script type="text/javascript">
207 var lineNo = 'general';
207 var lineNo = 'general';
208 var resolvesCommentId = null;
208 var resolvesCommentId = null;
209 var generalCommentForm = Rhodecode.comments.createGeneralComment(
209 var generalCommentForm = Rhodecode.comments.createGeneralComment(
210 lineNo, "${placeholder}", resolvesCommentId);
210 lineNo, "${placeholder}", resolvesCommentId);
211
211
212 // set custom success callback on rangeCommit
212 // set custom success callback on rangeCommit
213 % if is_compare:
213 % if is_compare:
214 generalCommentForm.setHandleFormSubmit(function(o) {
214 generalCommentForm.setHandleFormSubmit(function(o) {
215 var self = generalCommentForm;
215 var self = generalCommentForm;
216
216
217 var text = self.cm.getValue();
217 var text = self.cm.getValue();
218 var status = self.getCommentStatus();
218 var status = self.getCommentStatus();
219 var commentType = self.getCommentType();
219 var commentType = self.getCommentType();
220
220
221 if (text === "" && !status) {
221 if (text === "" && !status) {
222 return;
222 return;
223 }
223 }
224
224
225 // we can pick which commits we want to make the comment by
225 // we can pick which commits we want to make the comment by
226 // selecting them via click on preview pane, this will alter the hidden inputs
226 // selecting them via click on preview pane, this will alter the hidden inputs
227 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
227 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
228
228
229 var commitIds = [];
229 var commitIds = [];
230 $('#changeset_compare_view_content .compare_select').each(function(el) {
230 $('#changeset_compare_view_content .compare_select').each(function(el) {
231 var commitId = this.id.replace('row-', '');
231 var commitId = this.id.replace('row-', '');
232 if ($(this).hasClass('hl') || !cherryPicked) {
232 if ($(this).hasClass('hl') || !cherryPicked) {
233 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
233 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
234 commitIds.push(commitId);
234 commitIds.push(commitId);
235 } else {
235 } else {
236 $("input[data-commit-id='{0}']".format(commitId)).val('')
236 $("input[data-commit-id='{0}']".format(commitId)).val('')
237 }
237 }
238 });
238 });
239
239
240 self.setActionButtonsDisabled(true);
240 self.setActionButtonsDisabled(true);
241 self.cm.setOption("readOnly", true);
241 self.cm.setOption("readOnly", true);
242 var postData = {
242 var postData = {
243 'text': text,
243 'text': text,
244 'changeset_status': status,
244 'changeset_status': status,
245 'comment_type': commentType,
245 'comment_type': commentType,
246 'commit_ids': commitIds,
246 'commit_ids': commitIds,
247 'csrf_token': CSRF_TOKEN
247 'csrf_token': CSRF_TOKEN
248 };
248 };
249
249
250 var submitSuccessCallback = function(o) {
250 var submitSuccessCallback = function(o) {
251 location.reload(true);
251 location.reload(true);
252 };
252 };
253 var submitFailCallback = function(){
253 var submitFailCallback = function(){
254 self.resetCommentFormState(text)
254 self.resetCommentFormState(text)
255 };
255 };
256 self.submitAjaxPOST(
256 self.submitAjaxPOST(
257 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
257 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
258 });
258 });
259 % endif
259 % endif
260
260
261
261
262 </script>
262 </script>
263 % else:
263 % else:
264 ## form state when not logged in
264 ## form state when not logged in
265 <div class="comment-form ac">
265 <div class="comment-form ac">
266
266
267 <div class="comment-area">
267 <div class="comment-area">
268 <div class="comment-area-header">
268 <div class="comment-area-header">
269 <ul class="nav-links clearfix">
269 <ul class="nav-links clearfix">
270 <li class="active">
270 <li class="active">
271 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
271 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
272 </li>
272 </li>
273 <li class="">
273 <li class="">
274 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
274 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
275 </li>
275 </li>
276 </ul>
276 </ul>
277 </div>
277 </div>
278
278
279 <div class="comment-area-write" style="display: block;">
279 <div class="comment-area-write" style="display: block;">
280 <div id="edit-container">
280 <div id="edit-container">
281 <div style="padding: 40px 0">
281 <div style="padding: 40px 0">
282 ${_('You need to be logged in to leave comments.')}
282 ${_('You need to be logged in to leave comments.')}
283 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
283 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
284 </div>
284 </div>
285 </div>
285 </div>
286 <div id="preview-container" class="clearfix" style="display: none;">
286 <div id="preview-container" class="clearfix" style="display: none;">
287 <div id="preview-box" class="preview-box"></div>
287 <div id="preview-box" class="preview-box"></div>
288 </div>
288 </div>
289 </div>
289 </div>
290
290
291 <div class="comment-area-footer">
291 <div class="comment-area-footer">
292 <div class="toolbar">
292 <div class="toolbar">
293 <div class="toolbar-text">
293 <div class="toolbar-text">
294 </div>
294 </div>
295 </div>
295 </div>
296 </div>
296 </div>
297 </div>
297 </div>
298
298
299 <div class="comment-footer">
299 <div class="comment-footer">
300 </div>
300 </div>
301
301
302 </div>
302 </div>
303 % endif
303 % endif
304
304
305 <script type="text/javascript">
305 <script type="text/javascript">
306 bindToggleButtons();
306 bindToggleButtons();
307 </script>
307 </script>
308 </div>
308 </div>
309 </%def>
309 </%def>
310
310
311
311
312 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
312 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
313 ## comment injected based on assumption that user is logged in
313 ## comment injected based on assumption that user is logged in
314
314
315 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
315 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
316
316
317 <div class="comment-area">
317 <div class="comment-area">
318 <div class="comment-area-header">
318 <div class="comment-area-header">
319 <ul class="nav-links clearfix">
319 <ul class="nav-links clearfix">
320 <li class="active">
320 <li class="active">
321 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
321 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
322 </li>
322 </li>
323 <li class="">
323 <li class="">
324 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
324 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
325 </li>
325 </li>
326 <li class="pull-right">
326 <li class="pull-right">
327 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
327 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
328 % for val in c.visual.comment_types:
328 % for val in c.visual.comment_types:
329 <option value="${val}">${val.upper()}</option>
329 <option value="${val}">${val.upper()}</option>
330 % endfor
330 % endfor
331 </select>
331 </select>
332 </li>
332 </li>
333 </ul>
333 </ul>
334 </div>
334 </div>
335
335
336 <div class="comment-area-write" style="display: block;">
336 <div class="comment-area-write" style="display: block;">
337 <div id="edit-container_${lineno_id}">
337 <div id="edit-container_${lineno_id}">
338 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
338 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
339 </div>
339 </div>
340 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
340 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
341 <div id="preview-box_${lineno_id}" class="preview-box"></div>
341 <div id="preview-box_${lineno_id}" class="preview-box"></div>
342 </div>
342 </div>
343 </div>
343 </div>
344
344
345 <div class="comment-area-footer">
345 <div class="comment-area-footer">
346 <div class="toolbar">
346 <div class="toolbar">
347 <div class="toolbar-text">
347 <div class="toolbar-text">
348 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
348 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
349 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
349 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
350 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
350 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
351 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
351 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
352 )
352 )
353 )|n}
353 )|n}
354 </div>
354 </div>
355 </div>
355 </div>
356 </div>
356 </div>
357 </div>
357 </div>
358
358
359 <div class="comment-footer">
359 <div class="comment-footer">
360
360
361 % if review_statuses:
361 % if review_statuses:
362 <div class="status_box">
362 <div class="status_box">
363 <select id="change_status_${lineno_id}" name="changeset_status">
363 <select id="change_status_${lineno_id}" name="changeset_status">
364 <option></option> ## Placeholder
364 <option></option> ## Placeholder
365 % for status, lbl in review_statuses:
365 % for status, lbl in review_statuses:
366 <option value="${status}" data-status="${status}">${lbl}</option>
366 <option value="${status}" data-status="${status}">${lbl}</option>
367 %if is_pull_request and change_status and status in ('approved', 'rejected'):
367 %if is_pull_request and change_status and status in ('approved', 'rejected'):
368 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
368 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
369 %endif
369 %endif
370 % endfor
370 % endfor
371 </select>
371 </select>
372 </div>
372 </div>
373 % endif
373 % endif
374
374
375 ## inject extra inputs into the form
375 ## inject extra inputs into the form
376 % if form_extras and isinstance(form_extras, (list, tuple)):
376 % if form_extras and isinstance(form_extras, (list, tuple)):
377 <div id="comment_form_extras">
377 <div id="comment_form_extras">
378 % for form_ex_el in form_extras:
378 % for form_ex_el in form_extras:
379 ${form_ex_el|n}
379 ${form_ex_el|n}
380 % endfor
380 % endfor
381 </div>
381 </div>
382 % endif
382 % endif
383
383
384 <div class="action-buttons">
384 <div class="action-buttons">
385 ## inline for has a file, and line-number together with cancel hide button.
385 ## inline for has a file, and line-number together with cancel hide button.
386 % if form_type == 'inline':
386 % if form_type == 'inline':
387 <input type="hidden" name="f_path" value="{0}">
387 <input type="hidden" name="f_path" value="{0}">
388 <input type="hidden" name="line" value="${lineno_id}">
388 <input type="hidden" name="line" value="${lineno_id}">
389 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
389 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
390 ${_('Cancel')}
390 ${_('Cancel')}
391 </button>
391 </button>
392 % endif
392 % endif
393
393
394 % if form_type != 'inline':
394 % if form_type != 'inline':
395 <div class="action-buttons-extra"></div>
395 <div class="action-buttons-extra"></div>
396 % endif
396 % endif
397
397
398 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
398 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
399
399
400 </div>
400 </div>
401 </div>
401 </div>
402
402
403 </form>
403 </form>
404
404
405 </%def> No newline at end of file
405 </%def>
@@ -1,317 +1,317 b''
1 ## DATA TABLE RE USABLE ELEMENTS
1 ## DATA TABLE RE USABLE ELEMENTS
2 ## usage:
2 ## usage:
3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="base" file="/base/base.mako"/>
4 <%namespace name="base" file="/base/base.mako"/>
5
5
6 ## REPOSITORY RENDERERS
6 ## REPOSITORY RENDERERS
7 <%def name="quick_menu(repo_name)">
7 <%def name="quick_menu(repo_name)">
8 <i class="pointer icon-more"></i>
8 <i class="pointer icon-more"></i>
9 <div class="menu_items_container hidden">
9 <div class="menu_items_container hidden">
10 <ul class="menu_items">
10 <ul class="menu_items">
11 <li>
11 <li>
12 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
12 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
13 <span>${_('Summary')}</span>
13 <span>${_('Summary')}</span>
14 </a>
14 </a>
15 </li>
15 </li>
16 <li>
16 <li>
17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
18 <span>${_('Changelog')}</span>
18 <span>${_('Changelog')}</span>
19 </a>
19 </a>
20 </li>
20 </li>
21 <li>
21 <li>
22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
23 <span>${_('Files')}</span>
23 <span>${_('Files')}</span>
24 </a>
24 </a>
25 </li>
25 </li>
26 <li>
26 <li>
27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 <span>${_('Fork')}</span>
28 <span>${_('Fork')}</span>
29 </a>
29 </a>
30 </li>
30 </li>
31 </ul>
31 </ul>
32 </div>
32 </div>
33 </%def>
33 </%def>
34
34
35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 <%
36 <%
37 def get_name(name,short_name=short_name):
37 def get_name(name,short_name=short_name):
38 if short_name:
38 if short_name:
39 return name.split('/')[-1]
39 return name.split('/')[-1]
40 else:
40 else:
41 return name
41 return name
42 %>
42 %>
43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 ##NAME
44 ##NAME
45 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
45 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
46
46
47 ##TYPE OF REPO
47 ##TYPE OF REPO
48 %if h.is_hg(rtype):
48 %if h.is_hg(rtype):
49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 %elif h.is_git(rtype):
50 %elif h.is_git(rtype):
51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 %elif h.is_svn(rtype):
52 %elif h.is_svn(rtype):
53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 %endif
54 %endif
55
55
56 ##PRIVATE/PUBLIC
56 ##PRIVATE/PUBLIC
57 %if private and c.visual.show_private_icon:
57 %if private and c.visual.show_private_icon:
58 <i class="icon-lock" title="${_('Private repository')}"></i>
58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 %elif not private and c.visual.show_public_icon:
59 %elif not private and c.visual.show_public_icon:
60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 %else:
61 %else:
62 <span></span>
62 <span></span>
63 %endif
63 %endif
64 ${get_name(name)}
64 ${get_name(name)}
65 </a>
65 </a>
66 %if fork_of:
66 %if fork_of:
67 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
67 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 %endif
68 %endif
69 %if rstate == 'repo_state_pending':
69 %if rstate == 'repo_state_pending':
70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 %endif
71 %endif
72 </div>
72 </div>
73 </%def>
73 </%def>
74
74
75 <%def name="repo_desc(description)">
75 <%def name="repo_desc(description)">
76 <div class="truncate-wrap">${description}</div>
76 <div class="truncate-wrap">${description}</div>
77 </%def>
77 </%def>
78
78
79 <%def name="last_change(last_change)">
79 <%def name="last_change(last_change)">
80 ${h.age_component(last_change)}
80 ${h.age_component(last_change)}
81 </%def>
81 </%def>
82
82
83 <%def name="revision(name,rev,tip,author,last_msg)">
83 <%def name="revision(name,rev,tip,author,last_msg)">
84 <div>
84 <div>
85 %if rev >= 0:
85 %if rev >= 0:
86 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
86 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
87 %else:
87 %else:
88 ${_('No commits yet')}
88 ${_('No commits yet')}
89 %endif
89 %endif
90 </div>
90 </div>
91 </%def>
91 </%def>
92
92
93 <%def name="rss(name)">
93 <%def name="rss(name)">
94 %if c.rhodecode_user.username != h.DEFAULT_USER:
94 %if c.rhodecode_user.username != h.DEFAULT_USER:
95 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
95 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
96 %else:
96 %else:
97 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
97 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
98 %endif
98 %endif
99 </%def>
99 </%def>
100
100
101 <%def name="atom(name)">
101 <%def name="atom(name)">
102 %if c.rhodecode_user.username != h.DEFAULT_USER:
102 %if c.rhodecode_user.username != h.DEFAULT_USER:
103 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
103 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
104 %else:
104 %else:
105 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
105 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
106 %endif
106 %endif
107 </%def>
107 </%def>
108
108
109 <%def name="user_gravatar(email, size=16)">
109 <%def name="user_gravatar(email, size=16)">
110 <div class="rc-user tooltip" title="${h.author_string(email)}">
110 <div class="rc-user tooltip" title="${h.author_string(email)}">
111 ${base.gravatar(email, 16)}
111 ${base.gravatar(email, 16)}
112 </div>
112 </div>
113 </%def>
113 </%def>
114
114
115 <%def name="repo_actions(repo_name, super_user=True)">
115 <%def name="repo_actions(repo_name, super_user=True)">
116 <div>
116 <div>
117 <div class="grid_edit">
117 <div class="grid_edit">
118 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
118 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
119 <i class="icon-pencil"></i>Edit</a>
119 <i class="icon-pencil"></i>Edit</a>
120 </div>
120 </div>
121 <div class="grid_delete">
121 <div class="grid_delete">
122 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), method='POST')}
122 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), method='POST')}
123 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
123 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
124 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
124 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
125 ${h.end_form()}
125 ${h.end_form()}
126 </div>
126 </div>
127 </div>
127 </div>
128 </%def>
128 </%def>
129
129
130 <%def name="repo_state(repo_state)">
130 <%def name="repo_state(repo_state)">
131 <div>
131 <div>
132 %if repo_state == 'repo_state_pending':
132 %if repo_state == 'repo_state_pending':
133 <div class="tag tag4">${_('Creating')}</div>
133 <div class="tag tag4">${_('Creating')}</div>
134 %elif repo_state == 'repo_state_created':
134 %elif repo_state == 'repo_state_created':
135 <div class="tag tag1">${_('Created')}</div>
135 <div class="tag tag1">${_('Created')}</div>
136 %else:
136 %else:
137 <div class="tag alert2" title="${repo_state}">invalid</div>
137 <div class="tag alert2" title="${repo_state}">invalid</div>
138 %endif
138 %endif
139 </div>
139 </div>
140 </%def>
140 </%def>
141
141
142
142
143 ## REPO GROUP RENDERERS
143 ## REPO GROUP RENDERERS
144 <%def name="quick_repo_group_menu(repo_group_name)">
144 <%def name="quick_repo_group_menu(repo_group_name)">
145 <i class="pointer icon-more"></i>
145 <i class="pointer icon-more"></i>
146 <div class="menu_items_container hidden">
146 <div class="menu_items_container hidden">
147 <ul class="menu_items">
147 <ul class="menu_items">
148 <li>
148 <li>
149 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
149 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
150 <span class="icon">
150 <span class="icon">
151 <i class="icon-file-text"></i>
151 <i class="icon-file-text"></i>
152 </span>
152 </span>
153 <span>${_('Summary')}</span>
153 <span>${_('Summary')}</span>
154 </a>
154 </a>
155 </li>
155 </li>
156
156
157 </ul>
157 </ul>
158 </div>
158 </div>
159 </%def>
159 </%def>
160
160
161 <%def name="repo_group_name(repo_group_name, children_groups=None)">
161 <%def name="repo_group_name(repo_group_name, children_groups=None)">
162 <div>
162 <div>
163 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
163 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
164 <i class="icon-folder-close" title="${_('Repository group')}"></i>
164 <i class="icon-folder-close" title="${_('Repository group')}"></i>
165 %if children_groups:
165 %if children_groups:
166 ${h.literal(' &raquo; '.join(children_groups))}
166 ${h.literal(' &raquo; '.join(children_groups))}
167 %else:
167 %else:
168 ${repo_group_name}
168 ${repo_group_name}
169 %endif
169 %endif
170 </a>
170 </a>
171 </div>
171 </div>
172 </%def>
172 </%def>
173
173
174 <%def name="repo_group_desc(description)">
174 <%def name="repo_group_desc(description)">
175 <div class="truncate-wrap">${description}</div>
175 <div class="truncate-wrap">${description}</div>
176 </%def>
176 </%def>
177
177
178 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
178 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
179 <div class="grid_edit">
179 <div class="grid_edit">
180 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
180 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
181 </div>
181 </div>
182 <div class="grid_delete">
182 <div class="grid_delete">
183 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
183 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
184 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
184 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
185 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
185 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
186 ${h.end_form()}
186 ${h.end_form()}
187 </div>
187 </div>
188 </%def>
188 </%def>
189
189
190
190
191 <%def name="user_actions(user_id, username)">
191 <%def name="user_actions(user_id, username)">
192 <div class="grid_edit">
192 <div class="grid_edit">
193 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
193 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
194 <i class="icon-pencil"></i>Edit</a>
194 <i class="icon-pencil"></i>Edit</a>
195 </div>
195 </div>
196 <div class="grid_delete">
196 <div class="grid_delete">
197 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
197 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
198 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
198 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
199 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
199 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
200 ${h.end_form()}
200 ${h.end_form()}
201 </div>
201 </div>
202 </%def>
202 </%def>
203
203
204 <%def name="user_group_actions(user_group_id, user_group_name)">
204 <%def name="user_group_actions(user_group_id, user_group_name)">
205 <div class="grid_edit">
205 <div class="grid_edit">
206 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
206 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
207 </div>
207 </div>
208 <div class="grid_delete">
208 <div class="grid_delete">
209 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
209 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
210 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
210 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
211 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
211 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
212 ${h.end_form()}
212 ${h.end_form()}
213 </div>
213 </div>
214 </%def>
214 </%def>
215
215
216
216
217 <%def name="user_name(user_id, username)">
217 <%def name="user_name(user_id, username)">
218 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
218 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
219 </%def>
219 </%def>
220
220
221 <%def name="user_profile(username)">
221 <%def name="user_profile(username)">
222 ${base.gravatar_with_user(username, 16)}
222 ${base.gravatar_with_user(username, 16)}
223 </%def>
223 </%def>
224
224
225 <%def name="user_group_name(user_group_id, user_group_name)">
225 <%def name="user_group_name(user_group_id, user_group_name)">
226 <div>
226 <div>
227 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
227 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
228 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
228 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
229 </div>
229 </div>
230 </%def>
230 </%def>
231
231
232
232
233 ## GISTS
233 ## GISTS
234
234
235 <%def name="gist_gravatar(full_contact)">
235 <%def name="gist_gravatar(full_contact)">
236 <div class="gist_gravatar">
236 <div class="gist_gravatar">
237 ${base.gravatar(full_contact, 30)}
237 ${base.gravatar(full_contact, 30)}
238 </div>
238 </div>
239 </%def>
239 </%def>
240
240
241 <%def name="gist_access_id(gist_access_id, full_contact)">
241 <%def name="gist_access_id(gist_access_id, full_contact)">
242 <div>
242 <div>
243 <b>
243 <b>
244 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
244 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
245 </b>
245 </b>
246 </div>
246 </div>
247 </%def>
247 </%def>
248
248
249 <%def name="gist_author(full_contact, created_on, expires)">
249 <%def name="gist_author(full_contact, created_on, expires)">
250 ${base.gravatar_with_user(full_contact, 16)}
250 ${base.gravatar_with_user(full_contact, 16)}
251 </%def>
251 </%def>
252
252
253
253
254 <%def name="gist_created(created_on)">
254 <%def name="gist_created(created_on)">
255 <div class="created">
255 <div class="created">
256 ${h.age_component(created_on, time_is_local=True)}
256 ${h.age_component(created_on, time_is_local=True)}
257 </div>
257 </div>
258 </%def>
258 </%def>
259
259
260 <%def name="gist_expires(expires)">
260 <%def name="gist_expires(expires)">
261 <div class="created">
261 <div class="created">
262 %if expires == -1:
262 %if expires == -1:
263 ${_('never')}
263 ${_('never')}
264 %else:
264 %else:
265 ${h.age_component(h.time_to_utcdatetime(expires))}
265 ${h.age_component(h.time_to_utcdatetime(expires))}
266 %endif
266 %endif
267 </div>
267 </div>
268 </%def>
268 </%def>
269
269
270 <%def name="gist_type(gist_type)">
270 <%def name="gist_type(gist_type)">
271 %if gist_type != 'public':
271 %if gist_type != 'public':
272 <div class="tag">${_('Private')}</div>
272 <div class="tag">${_('Private')}</div>
273 %endif
273 %endif
274 </%def>
274 </%def>
275
275
276 <%def name="gist_description(gist_description)">
276 <%def name="gist_description(gist_description)">
277 ${gist_description}
277 ${gist_description}
278 </%def>
278 </%def>
279
279
280
280
281 ## PULL REQUESTS GRID RENDERERS
281 ## PULL REQUESTS GRID RENDERERS
282
282
283 <%def name="pullrequest_target_repo(repo_name)">
283 <%def name="pullrequest_target_repo(repo_name)">
284 <div class="truncate">
284 <div class="truncate">
285 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
285 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
286 </div>
286 </div>
287 </%def>
287 </%def>
288 <%def name="pullrequest_status(status)">
288 <%def name="pullrequest_status(status)">
289 <div class="${'flag_status %s' % status} pull-left"></div>
289 <div class="${'flag_status %s' % status} pull-left"></div>
290 </%def>
290 </%def>
291
291
292 <%def name="pullrequest_title(title, description)">
292 <%def name="pullrequest_title(title, description)">
293 ${title} <br/>
293 ${title} <br/>
294 ${h.shorter(description, 40)}
294 ${h.shorter(description, 40)}
295 </%def>
295 </%def>
296
296
297 <%def name="pullrequest_comments(comments_nr)">
297 <%def name="pullrequest_comments(comments_nr)">
298 <i class="icon-comment"></i> ${comments_nr}
298 <i class="icon-comment"></i> ${comments_nr}
299 </%def>
299 </%def>
300
300
301 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
301 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
302 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
302 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
303 % if short:
303 % if short:
304 #${pull_request_id}
304 #${pull_request_id}
305 % else:
305 % else:
306 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
306 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
307 % endif
307 % endif
308 </a>
308 </a>
309 </%def>
309 </%def>
310
310
311 <%def name="pullrequest_updated_on(updated_on)">
311 <%def name="pullrequest_updated_on(updated_on)">
312 ${h.age_component(h.time_to_utcdatetime(updated_on))}
312 ${h.age_component(h.time_to_utcdatetime(updated_on))}
313 </%def>
313 </%def>
314
314
315 <%def name="pullrequest_author(full_contact)">
315 <%def name="pullrequest_author(full_contact)">
316 ${base.gravatar_with_user(full_contact, 16)}
316 ${base.gravatar_with_user(full_contact, 16)}
317 </%def>
317 </%def>
@@ -1,136 +1,136 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 %if c.repo_commits:
3 %if c.repo_commits:
4 <table class="rctable repo_summary table_disp">
4 <table class="rctable repo_summary table_disp">
5 <tr>
5 <tr>
6
6
7 <th class="status" colspan="2"></th>
7 <th class="status" colspan="2"></th>
8 <th>${_('Commit')}</th>
8 <th>${_('Commit')}</th>
9 <th>${_('Commit message')}</th>
9 <th>${_('Commit message')}</th>
10 <th>${_('Age')}</th>
10 <th>${_('Age')}</th>
11 <th>${_('Author')}</th>
11 <th>${_('Author')}</th>
12 <th>${_('Refs')}</th>
12 <th>${_('Refs')}</th>
13 </tr>
13 </tr>
14 %for cnt,cs in enumerate(c.repo_commits):
14 %for cnt,cs in enumerate(c.repo_commits):
15 <tr class="parity${cnt%2}">
15 <tr class="parity${cnt%2}">
16
16
17 <td class="td-status">
17 <td class="td-status">
18 %if c.statuses.get(cs.raw_id):
18 %if c.statuses.get(cs.raw_id):
19 <div class="changeset-status-ico shortlog">
19 <div class="changeset-status-ico shortlog">
20 %if c.statuses.get(cs.raw_id)[2]:
20 %if c.statuses.get(cs.raw_id)[2]:
21 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (c.statuses.get(cs.raw_id)[0], c.statuses.get(cs.raw_id)[2])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
21 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (c.statuses.get(cs.raw_id)[0], c.statuses.get(cs.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
22 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
22 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
23 </a>
23 </a>
24 %else:
24 %else:
25 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(cs.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
25 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(cs.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
26 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
26 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
27 </a>
27 </a>
28 %endif
28 %endif
29 </div>
29 </div>
30 %else:
30 %else:
31 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
31 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
32 %endif
32 %endif
33 </td>
33 </td>
34 <td class="td-comments">
34 <td class="td-comments">
35 %if c.comments.get(cs.raw_id,[]):
35 %if c.comments.get(cs.raw_id,[]):
36 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
36 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
37 <i class="icon-comment"></i> ${len(c.comments[cs.raw_id])}
37 <i class="icon-comment"></i> ${len(c.comments[cs.raw_id])}
38 </a>
38 </a>
39 %endif
39 %endif
40 </td>
40 </td>
41 <td class="td-commit">
41 <td class="td-commit">
42 <pre><a href="${h.url('changeset_home', repo_name=c.repo_name, revision=cs.raw_id)}">${h.show_id(cs)}</a></pre>
42 <pre><a href="${h.url('changeset_home', repo_name=c.repo_name, revision=cs.raw_id)}">${h.show_id(cs)}</a></pre>
43 </td>
43 </td>
44
44
45 <td class="td-description mid">
45 <td class="td-description mid">
46 <div class="log-container truncate-wrap">
46 <div class="log-container truncate-wrap">
47 <div class="message truncate" id="c-${cs.raw_id}">${h.urlify_commit_message(cs.message, c.repo_name)}</div>
47 <div class="message truncate" id="c-${cs.raw_id}">${h.urlify_commit_message(cs.message, c.repo_name)}</div>
48 </div>
48 </div>
49 </td>
49 </td>
50
50
51 <td class="td-time">
51 <td class="td-time">
52 ${h.age_component(cs.date)}
52 ${h.age_component(cs.date)}
53 </td>
53 </td>
54 <td class="td-user author">
54 <td class="td-user author">
55 ${base.gravatar_with_user(cs.author)}
55 ${base.gravatar_with_user(cs.author)}
56 </td>
56 </td>
57
57
58 <td class="td-tags">
58 <td class="td-tags">
59 <div class="autoexpand">
59 <div class="autoexpand">
60 %if h.is_hg(c.rhodecode_repo):
60 %if h.is_hg(c.rhodecode_repo):
61 %for book in cs.bookmarks:
61 %for book in cs.bookmarks:
62 <span class="booktag tag" title="${_('Bookmark %s') % book}">
62 <span class="booktag tag" title="${_('Bookmark %s') % book}">
63 <a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
63 <a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
64 </span>
64 </span>
65 %endfor
65 %endfor
66 %endif
66 %endif
67 ## tags
67 ## tags
68 %for tag in cs.tags:
68 %for tag in cs.tags:
69 <span class="tagtag tag" title="${_('Tag %s') % tag}">
69 <span class="tagtag tag" title="${_('Tag %s') % tag}">
70 <a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
70 <a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
71 </span>
71 </span>
72 %endfor
72 %endfor
73
73
74 ## branch
74 ## branch
75 %if cs.branch:
75 %if cs.branch:
76 <span class="branchtag tag" title="${_('Branch %s') % cs.branch}">
76 <span class="branchtag tag" title="${_('Branch %s') % cs.branch}">
77 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch)}"><i class="icon-code-fork"></i>${h.shorter(cs.branch)}</a>
77 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch)}"><i class="icon-code-fork"></i>${h.shorter(cs.branch)}</a>
78 </span>
78 </span>
79 %endif
79 %endif
80 </div>
80 </div>
81 </td>
81 </td>
82 </tr>
82 </tr>
83 %endfor
83 %endfor
84
84
85 </table>
85 </table>
86
86
87 <script type="text/javascript">
87 <script type="text/javascript">
88 $(document).pjax('#shortlog_data .pager_link','#shortlog_data', {timeout: 2000, scrollTo: false });
88 $(document).pjax('#shortlog_data .pager_link','#shortlog_data', {timeout: 2000, scrollTo: false });
89 $(document).on('pjax:success', function(){ timeagoActivate(); });
89 $(document).on('pjax:success', function(){ timeagoActivate(); });
90 </script>
90 </script>
91
91
92 <div class="pagination-wh pagination-left">
92 <div class="pagination-wh pagination-left">
93 ${c.repo_commits.pager('$link_previous ~2~ $link_next')}
93 ${c.repo_commits.pager('$link_previous ~2~ $link_next')}
94 </div>
94 </div>
95 %else:
95 %else:
96
96
97 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
97 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
98 <div class="quick_start">
98 <div class="quick_start">
99 <div class="fieldset">
99 <div class="fieldset">
100 <div class="left-label">${_('Add or upload files directly via RhodeCode:')}</div>
100 <div class="left-label">${_('Add or upload files directly via RhodeCode:')}</div>
101 <div class="right-content">
101 <div class="right-content">
102 <div id="add_node_id" class="add_node">
102 <div id="add_node_id" class="add_node">
103 <a href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='', anchor='edit')}" class="btn btn-default">${_('Add New File')}</a>
103 <a href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='', anchor='edit')}" class="btn btn-default">${_('Add New File')}</a>
104 </div>
104 </div>
105 </div>
105 </div>
106 %endif
106 %endif
107 </div>
107 </div>
108
108
109 %if not h.is_svn(c.rhodecode_repo):
109 %if not h.is_svn(c.rhodecode_repo):
110 <div class="fieldset">
110 <div class="fieldset">
111 <div class="left-label">${_('Push new repo:')}</div>
111 <div class="left-label">${_('Push new repo:')}</div>
112 <div class="right-content">
112 <div class="right-content">
113 <pre>
113 <pre>
114 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
114 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
115 ${c.rhodecode_repo.alias} add README # add first file
115 ${c.rhodecode_repo.alias} add README # add first file
116 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
116 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
117 ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
117 ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
118 </pre>
118 </pre>
119 </div>
119 </div>
120 </div>
120 </div>
121 <div class="fieldset">
121 <div class="fieldset">
122 <div class="left-label">${_('Existing repository?')}</div>
122 <div class="left-label">${_('Existing repository?')}</div>
123 <div class="right-content">
123 <div class="right-content">
124 <pre>
124 <pre>
125 %if h.is_git(c.rhodecode_repo):
125 %if h.is_git(c.rhodecode_repo):
126 git remote add origin ${c.clone_repo_url}
126 git remote add origin ${c.clone_repo_url}
127 git push -u origin master
127 git push -u origin master
128 %else:
128 %else:
129 hg push ${c.clone_repo_url}
129 hg push ${c.clone_repo_url}
130 %endif
130 %endif
131 </pre>
131 </pre>
132 </div>
132 </div>
133 </div>
133 </div>
134 %endif
134 %endif
135 </div>
135 </div>
136 %endif
136 %endif
General Comments 0
You need to be logged in to leave comments. Login now