##// END OF EJS Templates
events: properly refresh comment object to load it's relationship....
marcink -
r2470:4400cfcf default
parent child Browse files
Show More
@@ -1,1236 +1,1240 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 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63
63
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'rhodecode:templates/data_table/_dt_elements.mako')
72 'rhodecode:templates/data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176 self.load_default_context()
176 self.load_default_context()
177
177
178 # additional filters
178 # additional filters
179 req_get = self.request.GET
179 req_get = self.request.GET
180 source = str2bool(req_get.get('source'))
180 source = str2bool(req_get.get('source'))
181 closed = str2bool(req_get.get('closed'))
181 closed = str2bool(req_get.get('closed'))
182 my = str2bool(req_get.get('my'))
182 my = str2bool(req_get.get('my'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185
185
186 filter_type = 'awaiting_review' if awaiting_review \
186 filter_type = 'awaiting_review' if awaiting_review \
187 else 'awaiting_my_review' if awaiting_my_review \
187 else 'awaiting_my_review' if awaiting_my_review \
188 else None
188 else None
189
189
190 opened_by = None
190 opened_by = None
191 if my:
191 if my:
192 opened_by = [self._rhodecode_user.user_id]
192 opened_by = [self._rhodecode_user.user_id]
193
193
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 if closed:
195 if closed:
196 statuses = [PullRequest.STATUS_CLOSED]
196 statuses = [PullRequest.STATUS_CLOSED]
197
197
198 data = self._get_pull_requests_list(
198 data = self._get_pull_requests_list(
199 repo_name=self.db_repo_name, source=source,
199 repo_name=self.db_repo_name, source=source,
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201
201
202 return data
202 return data
203
203
204 def _get_diffset(self, source_repo_name, source_repo,
204 def _get_diffset(self, source_repo_name, source_repo,
205 source_ref_id, target_ref_id,
205 source_ref_id, target_ref_id,
206 target_commit, source_commit, diff_limit, fulldiff,
206 target_commit, source_commit, diff_limit, fulldiff,
207 file_limit, display_inline_comments):
207 file_limit, display_inline_comments):
208
208
209 vcs_diff = PullRequestModel().get_diff(
209 vcs_diff = PullRequestModel().get_diff(
210 source_repo, source_ref_id, target_ref_id)
210 source_repo, source_ref_id, target_ref_id)
211
211
212 diff_processor = diffs.DiffProcessor(
212 diff_processor = diffs.DiffProcessor(
213 vcs_diff, format='newdiff', diff_limit=diff_limit,
213 vcs_diff, format='newdiff', diff_limit=diff_limit,
214 file_limit=file_limit, show_full_diff=fulldiff)
214 file_limit=file_limit, show_full_diff=fulldiff)
215
215
216 _parsed = diff_processor.prepare()
216 _parsed = diff_processor.prepare()
217
217
218 def _node_getter(commit):
218 def _node_getter(commit):
219 def get_node(fname):
219 def get_node(fname):
220 try:
220 try:
221 return commit.get_node(fname)
221 return commit.get_node(fname)
222 except NodeDoesNotExistError:
222 except NodeDoesNotExistError:
223 return None
223 return None
224
224
225 return get_node
225 return get_node
226
226
227 diffset = codeblocks.DiffSet(
227 diffset = codeblocks.DiffSet(
228 repo_name=self.db_repo_name,
228 repo_name=self.db_repo_name,
229 source_repo_name=source_repo_name,
229 source_repo_name=source_repo_name,
230 source_node_getter=_node_getter(target_commit),
230 source_node_getter=_node_getter(target_commit),
231 target_node_getter=_node_getter(source_commit),
231 target_node_getter=_node_getter(source_commit),
232 comments=display_inline_comments
232 comments=display_inline_comments
233 )
233 )
234 diffset = diffset.render_patchset(
234 diffset = diffset.render_patchset(
235 _parsed, target_commit.raw_id, source_commit.raw_id)
235 _parsed, target_commit.raw_id, source_commit.raw_id)
236
236
237 return diffset
237 return diffset
238
238
239 @LoginRequired()
239 @LoginRequired()
240 @HasRepoPermissionAnyDecorator(
240 @HasRepoPermissionAnyDecorator(
241 'repository.read', 'repository.write', 'repository.admin')
241 'repository.read', 'repository.write', 'repository.admin')
242 @view_config(
242 @view_config(
243 route_name='pullrequest_show', request_method='GET',
243 route_name='pullrequest_show', request_method='GET',
244 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
244 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
245 def pull_request_show(self):
245 def pull_request_show(self):
246 pull_request_id = self.request.matchdict['pull_request_id']
246 pull_request_id = self.request.matchdict['pull_request_id']
247
247
248 c = self.load_default_context()
248 c = self.load_default_context()
249
249
250 version = self.request.GET.get('version')
250 version = self.request.GET.get('version')
251 from_version = self.request.GET.get('from_version') or version
251 from_version = self.request.GET.get('from_version') or version
252 merge_checks = self.request.GET.get('merge_checks')
252 merge_checks = self.request.GET.get('merge_checks')
253 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
253 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
254
254
255 (pull_request_latest,
255 (pull_request_latest,
256 pull_request_at_ver,
256 pull_request_at_ver,
257 pull_request_display_obj,
257 pull_request_display_obj,
258 at_version) = PullRequestModel().get_pr_version(
258 at_version) = PullRequestModel().get_pr_version(
259 pull_request_id, version=version)
259 pull_request_id, version=version)
260 pr_closed = pull_request_latest.is_closed()
260 pr_closed = pull_request_latest.is_closed()
261
261
262 if pr_closed and (version or from_version):
262 if pr_closed and (version or from_version):
263 # not allow to browse versions
263 # not allow to browse versions
264 raise HTTPFound(h.route_path(
264 raise HTTPFound(h.route_path(
265 'pullrequest_show', repo_name=self.db_repo_name,
265 'pullrequest_show', repo_name=self.db_repo_name,
266 pull_request_id=pull_request_id))
266 pull_request_id=pull_request_id))
267
267
268 versions = pull_request_display_obj.versions()
268 versions = pull_request_display_obj.versions()
269
269
270 c.at_version = at_version
270 c.at_version = at_version
271 c.at_version_num = (at_version
271 c.at_version_num = (at_version
272 if at_version and at_version != 'latest'
272 if at_version and at_version != 'latest'
273 else None)
273 else None)
274 c.at_version_pos = ChangesetComment.get_index_from_version(
274 c.at_version_pos = ChangesetComment.get_index_from_version(
275 c.at_version_num, versions)
275 c.at_version_num, versions)
276
276
277 (prev_pull_request_latest,
277 (prev_pull_request_latest,
278 prev_pull_request_at_ver,
278 prev_pull_request_at_ver,
279 prev_pull_request_display_obj,
279 prev_pull_request_display_obj,
280 prev_at_version) = PullRequestModel().get_pr_version(
280 prev_at_version) = PullRequestModel().get_pr_version(
281 pull_request_id, version=from_version)
281 pull_request_id, version=from_version)
282
282
283 c.from_version = prev_at_version
283 c.from_version = prev_at_version
284 c.from_version_num = (prev_at_version
284 c.from_version_num = (prev_at_version
285 if prev_at_version and prev_at_version != 'latest'
285 if prev_at_version and prev_at_version != 'latest'
286 else None)
286 else None)
287 c.from_version_pos = ChangesetComment.get_index_from_version(
287 c.from_version_pos = ChangesetComment.get_index_from_version(
288 c.from_version_num, versions)
288 c.from_version_num, versions)
289
289
290 # define if we're in COMPARE mode or VIEW at version mode
290 # define if we're in COMPARE mode or VIEW at version mode
291 compare = at_version != prev_at_version
291 compare = at_version != prev_at_version
292
292
293 # pull_requests repo_name we opened it against
293 # pull_requests repo_name we opened it against
294 # ie. target_repo must match
294 # ie. target_repo must match
295 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
295 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
296 raise HTTPNotFound()
296 raise HTTPNotFound()
297
297
298 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
298 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
299 pull_request_at_ver)
299 pull_request_at_ver)
300
300
301 c.pull_request = pull_request_display_obj
301 c.pull_request = pull_request_display_obj
302 c.pull_request_latest = pull_request_latest
302 c.pull_request_latest = pull_request_latest
303
303
304 if compare or (at_version and not at_version == 'latest'):
304 if compare or (at_version and not at_version == 'latest'):
305 c.allowed_to_change_status = False
305 c.allowed_to_change_status = False
306 c.allowed_to_update = False
306 c.allowed_to_update = False
307 c.allowed_to_merge = False
307 c.allowed_to_merge = False
308 c.allowed_to_delete = False
308 c.allowed_to_delete = False
309 c.allowed_to_comment = False
309 c.allowed_to_comment = False
310 c.allowed_to_close = False
310 c.allowed_to_close = False
311 else:
311 else:
312 can_change_status = PullRequestModel().check_user_change_status(
312 can_change_status = PullRequestModel().check_user_change_status(
313 pull_request_at_ver, self._rhodecode_user)
313 pull_request_at_ver, self._rhodecode_user)
314 c.allowed_to_change_status = can_change_status and not pr_closed
314 c.allowed_to_change_status = can_change_status and not pr_closed
315
315
316 c.allowed_to_update = PullRequestModel().check_user_update(
316 c.allowed_to_update = PullRequestModel().check_user_update(
317 pull_request_latest, self._rhodecode_user) and not pr_closed
317 pull_request_latest, self._rhodecode_user) and not pr_closed
318 c.allowed_to_merge = PullRequestModel().check_user_merge(
318 c.allowed_to_merge = PullRequestModel().check_user_merge(
319 pull_request_latest, self._rhodecode_user) and not pr_closed
319 pull_request_latest, self._rhodecode_user) and not pr_closed
320 c.allowed_to_delete = PullRequestModel().check_user_delete(
320 c.allowed_to_delete = PullRequestModel().check_user_delete(
321 pull_request_latest, self._rhodecode_user) and not pr_closed
321 pull_request_latest, self._rhodecode_user) and not pr_closed
322 c.allowed_to_comment = not pr_closed
322 c.allowed_to_comment = not pr_closed
323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
324
324
325 c.forbid_adding_reviewers = False
325 c.forbid_adding_reviewers = False
326 c.forbid_author_to_review = False
326 c.forbid_author_to_review = False
327 c.forbid_commit_author_to_review = False
327 c.forbid_commit_author_to_review = False
328
328
329 if pull_request_latest.reviewer_data and \
329 if pull_request_latest.reviewer_data and \
330 'rules' in pull_request_latest.reviewer_data:
330 'rules' in pull_request_latest.reviewer_data:
331 rules = pull_request_latest.reviewer_data['rules'] or {}
331 rules = pull_request_latest.reviewer_data['rules'] or {}
332 try:
332 try:
333 c.forbid_adding_reviewers = rules.get(
333 c.forbid_adding_reviewers = rules.get(
334 'forbid_adding_reviewers')
334 'forbid_adding_reviewers')
335 c.forbid_author_to_review = rules.get(
335 c.forbid_author_to_review = rules.get(
336 'forbid_author_to_review')
336 'forbid_author_to_review')
337 c.forbid_commit_author_to_review = rules.get(
337 c.forbid_commit_author_to_review = rules.get(
338 'forbid_commit_author_to_review')
338 'forbid_commit_author_to_review')
339 except Exception:
339 except Exception:
340 pass
340 pass
341
341
342 # check merge capabilities
342 # check merge capabilities
343 _merge_check = MergeCheck.validate(
343 _merge_check = MergeCheck.validate(
344 pull_request_latest, user=self._rhodecode_user,
344 pull_request_latest, user=self._rhodecode_user,
345 translator=self.request.translate)
345 translator=self.request.translate)
346 c.pr_merge_errors = _merge_check.error_details
346 c.pr_merge_errors = _merge_check.error_details
347 c.pr_merge_possible = not _merge_check.failed
347 c.pr_merge_possible = not _merge_check.failed
348 c.pr_merge_message = _merge_check.merge_msg
348 c.pr_merge_message = _merge_check.merge_msg
349
349
350 c.pr_merge_info = MergeCheck.get_merge_conditions(
350 c.pr_merge_info = MergeCheck.get_merge_conditions(
351 pull_request_latest, translator=self.request.translate)
351 pull_request_latest, translator=self.request.translate)
352
352
353 c.pull_request_review_status = _merge_check.review_status
353 c.pull_request_review_status = _merge_check.review_status
354 if merge_checks:
354 if merge_checks:
355 self.request.override_renderer = \
355 self.request.override_renderer = \
356 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
356 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
357 return self._get_template_context(c)
357 return self._get_template_context(c)
358
358
359 comments_model = CommentsModel()
359 comments_model = CommentsModel()
360
360
361 # reviewers and statuses
361 # reviewers and statuses
362 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
362 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
363 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
363 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
364
364
365 # GENERAL COMMENTS with versions #
365 # GENERAL COMMENTS with versions #
366 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
366 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
367 q = q.order_by(ChangesetComment.comment_id.asc())
367 q = q.order_by(ChangesetComment.comment_id.asc())
368 general_comments = q
368 general_comments = q
369
369
370 # pick comments we want to render at current version
370 # pick comments we want to render at current version
371 c.comment_versions = comments_model.aggregate_comments(
371 c.comment_versions = comments_model.aggregate_comments(
372 general_comments, versions, c.at_version_num)
372 general_comments, versions, c.at_version_num)
373 c.comments = c.comment_versions[c.at_version_num]['until']
373 c.comments = c.comment_versions[c.at_version_num]['until']
374
374
375 # INLINE COMMENTS with versions #
375 # INLINE COMMENTS with versions #
376 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
376 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
377 q = q.order_by(ChangesetComment.comment_id.asc())
377 q = q.order_by(ChangesetComment.comment_id.asc())
378 inline_comments = q
378 inline_comments = q
379
379
380 c.inline_versions = comments_model.aggregate_comments(
380 c.inline_versions = comments_model.aggregate_comments(
381 inline_comments, versions, c.at_version_num, inline=True)
381 inline_comments, versions, c.at_version_num, inline=True)
382
382
383 # inject latest version
383 # inject latest version
384 latest_ver = PullRequest.get_pr_display_object(
384 latest_ver = PullRequest.get_pr_display_object(
385 pull_request_latest, pull_request_latest)
385 pull_request_latest, pull_request_latest)
386
386
387 c.versions = versions + [latest_ver]
387 c.versions = versions + [latest_ver]
388
388
389 # if we use version, then do not show later comments
389 # if we use version, then do not show later comments
390 # than current version
390 # than current version
391 display_inline_comments = collections.defaultdict(
391 display_inline_comments = collections.defaultdict(
392 lambda: collections.defaultdict(list))
392 lambda: collections.defaultdict(list))
393 for co in inline_comments:
393 for co in inline_comments:
394 if c.at_version_num:
394 if c.at_version_num:
395 # pick comments that are at least UPTO given version, so we
395 # pick comments that are at least UPTO given version, so we
396 # don't render comments for higher version
396 # don't render comments for higher version
397 should_render = co.pull_request_version_id and \
397 should_render = co.pull_request_version_id and \
398 co.pull_request_version_id <= c.at_version_num
398 co.pull_request_version_id <= c.at_version_num
399 else:
399 else:
400 # showing all, for 'latest'
400 # showing all, for 'latest'
401 should_render = True
401 should_render = True
402
402
403 if should_render:
403 if should_render:
404 display_inline_comments[co.f_path][co.line_no].append(co)
404 display_inline_comments[co.f_path][co.line_no].append(co)
405
405
406 # load diff data into template context, if we use compare mode then
406 # load diff data into template context, if we use compare mode then
407 # diff is calculated based on changes between versions of PR
407 # diff is calculated based on changes between versions of PR
408
408
409 source_repo = pull_request_at_ver.source_repo
409 source_repo = pull_request_at_ver.source_repo
410 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
410 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
411
411
412 target_repo = pull_request_at_ver.target_repo
412 target_repo = pull_request_at_ver.target_repo
413 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
413 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
414
414
415 if compare:
415 if compare:
416 # in compare switch the diff base to latest commit from prev version
416 # in compare switch the diff base to latest commit from prev version
417 target_ref_id = prev_pull_request_display_obj.revisions[0]
417 target_ref_id = prev_pull_request_display_obj.revisions[0]
418
418
419 # despite opening commits for bookmarks/branches/tags, we always
419 # despite opening commits for bookmarks/branches/tags, we always
420 # convert this to rev to prevent changes after bookmark or branch change
420 # convert this to rev to prevent changes after bookmark or branch change
421 c.source_ref_type = 'rev'
421 c.source_ref_type = 'rev'
422 c.source_ref = source_ref_id
422 c.source_ref = source_ref_id
423
423
424 c.target_ref_type = 'rev'
424 c.target_ref_type = 'rev'
425 c.target_ref = target_ref_id
425 c.target_ref = target_ref_id
426
426
427 c.source_repo = source_repo
427 c.source_repo = source_repo
428 c.target_repo = target_repo
428 c.target_repo = target_repo
429
429
430 c.commit_ranges = []
430 c.commit_ranges = []
431 source_commit = EmptyCommit()
431 source_commit = EmptyCommit()
432 target_commit = EmptyCommit()
432 target_commit = EmptyCommit()
433 c.missing_requirements = False
433 c.missing_requirements = False
434
434
435 source_scm = source_repo.scm_instance()
435 source_scm = source_repo.scm_instance()
436 target_scm = target_repo.scm_instance()
436 target_scm = target_repo.scm_instance()
437
437
438 # try first shadow repo, fallback to regular repo
438 # try first shadow repo, fallback to regular repo
439 try:
439 try:
440 commits_source_repo = pull_request_latest.get_shadow_repo()
440 commits_source_repo = pull_request_latest.get_shadow_repo()
441 except Exception:
441 except Exception:
442 log.debug('Failed to get shadow repo', exc_info=True)
442 log.debug('Failed to get shadow repo', exc_info=True)
443 commits_source_repo = source_scm
443 commits_source_repo = source_scm
444
444
445 c.commits_source_repo = commits_source_repo
445 c.commits_source_repo = commits_source_repo
446 commit_cache = {}
446 commit_cache = {}
447 try:
447 try:
448 pre_load = ["author", "branch", "date", "message"]
448 pre_load = ["author", "branch", "date", "message"]
449 show_revs = pull_request_at_ver.revisions
449 show_revs = pull_request_at_ver.revisions
450 for rev in show_revs:
450 for rev in show_revs:
451 comm = commits_source_repo.get_commit(
451 comm = commits_source_repo.get_commit(
452 commit_id=rev, pre_load=pre_load)
452 commit_id=rev, pre_load=pre_load)
453 c.commit_ranges.append(comm)
453 c.commit_ranges.append(comm)
454 commit_cache[comm.raw_id] = comm
454 commit_cache[comm.raw_id] = comm
455
455
456 # Order here matters, we first need to get target, and then
456 # Order here matters, we first need to get target, and then
457 # the source
457 # the source
458 target_commit = commits_source_repo.get_commit(
458 target_commit = commits_source_repo.get_commit(
459 commit_id=safe_str(target_ref_id))
459 commit_id=safe_str(target_ref_id))
460
460
461 source_commit = commits_source_repo.get_commit(
461 source_commit = commits_source_repo.get_commit(
462 commit_id=safe_str(source_ref_id))
462 commit_id=safe_str(source_ref_id))
463
463
464 except CommitDoesNotExistError:
464 except CommitDoesNotExistError:
465 log.warning(
465 log.warning(
466 'Failed to get commit from `{}` repo'.format(
466 'Failed to get commit from `{}` repo'.format(
467 commits_source_repo), exc_info=True)
467 commits_source_repo), exc_info=True)
468 except RepositoryRequirementError:
468 except RepositoryRequirementError:
469 log.warning(
469 log.warning(
470 'Failed to get all required data from repo', exc_info=True)
470 'Failed to get all required data from repo', exc_info=True)
471 c.missing_requirements = True
471 c.missing_requirements = True
472
472
473 c.ancestor = None # set it to None, to hide it from PR view
473 c.ancestor = None # set it to None, to hide it from PR view
474
474
475 try:
475 try:
476 ancestor_id = source_scm.get_common_ancestor(
476 ancestor_id = source_scm.get_common_ancestor(
477 source_commit.raw_id, target_commit.raw_id, target_scm)
477 source_commit.raw_id, target_commit.raw_id, target_scm)
478 c.ancestor_commit = source_scm.get_commit(ancestor_id)
478 c.ancestor_commit = source_scm.get_commit(ancestor_id)
479 except Exception:
479 except Exception:
480 c.ancestor_commit = None
480 c.ancestor_commit = None
481
481
482 c.statuses = source_repo.statuses(
482 c.statuses = source_repo.statuses(
483 [x.raw_id for x in c.commit_ranges])
483 [x.raw_id for x in c.commit_ranges])
484
484
485 # auto collapse if we have more than limit
485 # auto collapse if we have more than limit
486 collapse_limit = diffs.DiffProcessor._collapse_commits_over
486 collapse_limit = diffs.DiffProcessor._collapse_commits_over
487 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
487 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
488 c.compare_mode = compare
488 c.compare_mode = compare
489
489
490 # diff_limit is the old behavior, will cut off the whole diff
490 # diff_limit is the old behavior, will cut off the whole diff
491 # if the limit is applied otherwise will just hide the
491 # if the limit is applied otherwise will just hide the
492 # big files from the front-end
492 # big files from the front-end
493 diff_limit = c.visual.cut_off_limit_diff
493 diff_limit = c.visual.cut_off_limit_diff
494 file_limit = c.visual.cut_off_limit_file
494 file_limit = c.visual.cut_off_limit_file
495
495
496 c.missing_commits = False
496 c.missing_commits = False
497 if (c.missing_requirements
497 if (c.missing_requirements
498 or isinstance(source_commit, EmptyCommit)
498 or isinstance(source_commit, EmptyCommit)
499 or source_commit == target_commit):
499 or source_commit == target_commit):
500
500
501 c.missing_commits = True
501 c.missing_commits = True
502 else:
502 else:
503
503
504 c.diffset = self._get_diffset(
504 c.diffset = self._get_diffset(
505 c.source_repo.repo_name, commits_source_repo,
505 c.source_repo.repo_name, commits_source_repo,
506 source_ref_id, target_ref_id,
506 source_ref_id, target_ref_id,
507 target_commit, source_commit,
507 target_commit, source_commit,
508 diff_limit, c.fulldiff, file_limit, display_inline_comments)
508 diff_limit, c.fulldiff, file_limit, display_inline_comments)
509
509
510 c.limited_diff = c.diffset.limited_diff
510 c.limited_diff = c.diffset.limited_diff
511
511
512 # calculate removed files that are bound to comments
512 # calculate removed files that are bound to comments
513 comment_deleted_files = [
513 comment_deleted_files = [
514 fname for fname in display_inline_comments
514 fname for fname in display_inline_comments
515 if fname not in c.diffset.file_stats]
515 if fname not in c.diffset.file_stats]
516
516
517 c.deleted_files_comments = collections.defaultdict(dict)
517 c.deleted_files_comments = collections.defaultdict(dict)
518 for fname, per_line_comments in display_inline_comments.items():
518 for fname, per_line_comments in display_inline_comments.items():
519 if fname in comment_deleted_files:
519 if fname in comment_deleted_files:
520 c.deleted_files_comments[fname]['stats'] = 0
520 c.deleted_files_comments[fname]['stats'] = 0
521 c.deleted_files_comments[fname]['comments'] = list()
521 c.deleted_files_comments[fname]['comments'] = list()
522 for lno, comments in per_line_comments.items():
522 for lno, comments in per_line_comments.items():
523 c.deleted_files_comments[fname]['comments'].extend(
523 c.deleted_files_comments[fname]['comments'].extend(
524 comments)
524 comments)
525
525
526 # this is a hack to properly display links, when creating PR, the
526 # this is a hack to properly display links, when creating PR, the
527 # compare view and others uses different notation, and
527 # compare view and others uses different notation, and
528 # compare_commits.mako renders links based on the target_repo.
528 # compare_commits.mako renders links based on the target_repo.
529 # We need to swap that here to generate it properly on the html side
529 # We need to swap that here to generate it properly on the html side
530 c.target_repo = c.source_repo
530 c.target_repo = c.source_repo
531
531
532 c.commit_statuses = ChangesetStatus.STATUSES
532 c.commit_statuses = ChangesetStatus.STATUSES
533
533
534 c.show_version_changes = not pr_closed
534 c.show_version_changes = not pr_closed
535 if c.show_version_changes:
535 if c.show_version_changes:
536 cur_obj = pull_request_at_ver
536 cur_obj = pull_request_at_ver
537 prev_obj = prev_pull_request_at_ver
537 prev_obj = prev_pull_request_at_ver
538
538
539 old_commit_ids = prev_obj.revisions
539 old_commit_ids = prev_obj.revisions
540 new_commit_ids = cur_obj.revisions
540 new_commit_ids = cur_obj.revisions
541 commit_changes = PullRequestModel()._calculate_commit_id_changes(
541 commit_changes = PullRequestModel()._calculate_commit_id_changes(
542 old_commit_ids, new_commit_ids)
542 old_commit_ids, new_commit_ids)
543 c.commit_changes_summary = commit_changes
543 c.commit_changes_summary = commit_changes
544
544
545 # calculate the diff for commits between versions
545 # calculate the diff for commits between versions
546 c.commit_changes = []
546 c.commit_changes = []
547 mark = lambda cs, fw: list(
547 mark = lambda cs, fw: list(
548 h.itertools.izip_longest([], cs, fillvalue=fw))
548 h.itertools.izip_longest([], cs, fillvalue=fw))
549 for c_type, raw_id in mark(commit_changes.added, 'a') \
549 for c_type, raw_id in mark(commit_changes.added, 'a') \
550 + mark(commit_changes.removed, 'r') \
550 + mark(commit_changes.removed, 'r') \
551 + mark(commit_changes.common, 'c'):
551 + mark(commit_changes.common, 'c'):
552
552
553 if raw_id in commit_cache:
553 if raw_id in commit_cache:
554 commit = commit_cache[raw_id]
554 commit = commit_cache[raw_id]
555 else:
555 else:
556 try:
556 try:
557 commit = commits_source_repo.get_commit(raw_id)
557 commit = commits_source_repo.get_commit(raw_id)
558 except CommitDoesNotExistError:
558 except CommitDoesNotExistError:
559 # in case we fail extracting still use "dummy" commit
559 # in case we fail extracting still use "dummy" commit
560 # for display in commit diff
560 # for display in commit diff
561 commit = h.AttributeDict(
561 commit = h.AttributeDict(
562 {'raw_id': raw_id,
562 {'raw_id': raw_id,
563 'message': 'EMPTY or MISSING COMMIT'})
563 'message': 'EMPTY or MISSING COMMIT'})
564 c.commit_changes.append([c_type, commit])
564 c.commit_changes.append([c_type, commit])
565
565
566 # current user review statuses for each version
566 # current user review statuses for each version
567 c.review_versions = {}
567 c.review_versions = {}
568 if self._rhodecode_user.user_id in allowed_reviewers:
568 if self._rhodecode_user.user_id in allowed_reviewers:
569 for co in general_comments:
569 for co in general_comments:
570 if co.author.user_id == self._rhodecode_user.user_id:
570 if co.author.user_id == self._rhodecode_user.user_id:
571 # each comment has a status change
571 # each comment has a status change
572 status = co.status_change
572 status = co.status_change
573 if status:
573 if status:
574 _ver_pr = status[0].comment.pull_request_version_id
574 _ver_pr = status[0].comment.pull_request_version_id
575 c.review_versions[_ver_pr] = status[0]
575 c.review_versions[_ver_pr] = status[0]
576
576
577 return self._get_template_context(c)
577 return self._get_template_context(c)
578
578
579 def assure_not_empty_repo(self):
579 def assure_not_empty_repo(self):
580 _ = self.request.translate
580 _ = self.request.translate
581
581
582 try:
582 try:
583 self.db_repo.scm_instance().get_commit()
583 self.db_repo.scm_instance().get_commit()
584 except EmptyRepositoryError:
584 except EmptyRepositoryError:
585 h.flash(h.literal(_('There are no commits yet')),
585 h.flash(h.literal(_('There are no commits yet')),
586 category='warning')
586 category='warning')
587 raise HTTPFound(
587 raise HTTPFound(
588 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
588 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
589
589
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator(
592 @HasRepoPermissionAnyDecorator(
593 'repository.read', 'repository.write', 'repository.admin')
593 'repository.read', 'repository.write', 'repository.admin')
594 @view_config(
594 @view_config(
595 route_name='pullrequest_new', request_method='GET',
595 route_name='pullrequest_new', request_method='GET',
596 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
596 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
597 def pull_request_new(self):
597 def pull_request_new(self):
598 _ = self.request.translate
598 _ = self.request.translate
599 c = self.load_default_context()
599 c = self.load_default_context()
600
600
601 self.assure_not_empty_repo()
601 self.assure_not_empty_repo()
602 source_repo = self.db_repo
602 source_repo = self.db_repo
603
603
604 commit_id = self.request.GET.get('commit')
604 commit_id = self.request.GET.get('commit')
605 branch_ref = self.request.GET.get('branch')
605 branch_ref = self.request.GET.get('branch')
606 bookmark_ref = self.request.GET.get('bookmark')
606 bookmark_ref = self.request.GET.get('bookmark')
607
607
608 try:
608 try:
609 source_repo_data = PullRequestModel().generate_repo_data(
609 source_repo_data = PullRequestModel().generate_repo_data(
610 source_repo, commit_id=commit_id,
610 source_repo, commit_id=commit_id,
611 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
611 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
612 except CommitDoesNotExistError as e:
612 except CommitDoesNotExistError as e:
613 log.exception(e)
613 log.exception(e)
614 h.flash(_('Commit does not exist'), 'error')
614 h.flash(_('Commit does not exist'), 'error')
615 raise HTTPFound(
615 raise HTTPFound(
616 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
616 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
617
617
618 default_target_repo = source_repo
618 default_target_repo = source_repo
619
619
620 if source_repo.parent:
620 if source_repo.parent:
621 parent_vcs_obj = source_repo.parent.scm_instance()
621 parent_vcs_obj = source_repo.parent.scm_instance()
622 if parent_vcs_obj and not parent_vcs_obj.is_empty():
622 if parent_vcs_obj and not parent_vcs_obj.is_empty():
623 # change default if we have a parent repo
623 # change default if we have a parent repo
624 default_target_repo = source_repo.parent
624 default_target_repo = source_repo.parent
625
625
626 target_repo_data = PullRequestModel().generate_repo_data(
626 target_repo_data = PullRequestModel().generate_repo_data(
627 default_target_repo, translator=self.request.translate)
627 default_target_repo, translator=self.request.translate)
628
628
629 selected_source_ref = source_repo_data['refs']['selected_ref']
629 selected_source_ref = source_repo_data['refs']['selected_ref']
630
630
631 title_source_ref = selected_source_ref.split(':', 2)[1]
631 title_source_ref = selected_source_ref.split(':', 2)[1]
632 c.default_title = PullRequestModel().generate_pullrequest_title(
632 c.default_title = PullRequestModel().generate_pullrequest_title(
633 source=source_repo.repo_name,
633 source=source_repo.repo_name,
634 source_ref=title_source_ref,
634 source_ref=title_source_ref,
635 target=default_target_repo.repo_name
635 target=default_target_repo.repo_name
636 )
636 )
637
637
638 c.default_repo_data = {
638 c.default_repo_data = {
639 'source_repo_name': source_repo.repo_name,
639 'source_repo_name': source_repo.repo_name,
640 'source_refs_json': json.dumps(source_repo_data),
640 'source_refs_json': json.dumps(source_repo_data),
641 'target_repo_name': default_target_repo.repo_name,
641 'target_repo_name': default_target_repo.repo_name,
642 'target_refs_json': json.dumps(target_repo_data),
642 'target_refs_json': json.dumps(target_repo_data),
643 }
643 }
644 c.default_source_ref = selected_source_ref
644 c.default_source_ref = selected_source_ref
645
645
646 return self._get_template_context(c)
646 return self._get_template_context(c)
647
647
648 @LoginRequired()
648 @LoginRequired()
649 @NotAnonymous()
649 @NotAnonymous()
650 @HasRepoPermissionAnyDecorator(
650 @HasRepoPermissionAnyDecorator(
651 'repository.read', 'repository.write', 'repository.admin')
651 'repository.read', 'repository.write', 'repository.admin')
652 @view_config(
652 @view_config(
653 route_name='pullrequest_repo_refs', request_method='GET',
653 route_name='pullrequest_repo_refs', request_method='GET',
654 renderer='json_ext', xhr=True)
654 renderer='json_ext', xhr=True)
655 def pull_request_repo_refs(self):
655 def pull_request_repo_refs(self):
656 self.load_default_context()
656 self.load_default_context()
657 target_repo_name = self.request.matchdict['target_repo_name']
657 target_repo_name = self.request.matchdict['target_repo_name']
658 repo = Repository.get_by_repo_name(target_repo_name)
658 repo = Repository.get_by_repo_name(target_repo_name)
659 if not repo:
659 if not repo:
660 raise HTTPNotFound()
660 raise HTTPNotFound()
661
661
662 target_perm = HasRepoPermissionAny(
662 target_perm = HasRepoPermissionAny(
663 'repository.read', 'repository.write', 'repository.admin')(
663 'repository.read', 'repository.write', 'repository.admin')(
664 target_repo_name)
664 target_repo_name)
665 if not target_perm:
665 if not target_perm:
666 raise HTTPNotFound()
666 raise HTTPNotFound()
667
667
668 return PullRequestModel().generate_repo_data(
668 return PullRequestModel().generate_repo_data(
669 repo, translator=self.request.translate)
669 repo, translator=self.request.translate)
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator(
673 @HasRepoPermissionAnyDecorator(
674 'repository.read', 'repository.write', 'repository.admin')
674 'repository.read', 'repository.write', 'repository.admin')
675 @view_config(
675 @view_config(
676 route_name='pullrequest_repo_destinations', request_method='GET',
676 route_name='pullrequest_repo_destinations', request_method='GET',
677 renderer='json_ext', xhr=True)
677 renderer='json_ext', xhr=True)
678 def pull_request_repo_destinations(self):
678 def pull_request_repo_destinations(self):
679 _ = self.request.translate
679 _ = self.request.translate
680 filter_query = self.request.GET.get('query')
680 filter_query = self.request.GET.get('query')
681
681
682 query = Repository.query() \
682 query = Repository.query() \
683 .order_by(func.length(Repository.repo_name)) \
683 .order_by(func.length(Repository.repo_name)) \
684 .filter(
684 .filter(
685 or_(Repository.repo_name == self.db_repo.repo_name,
685 or_(Repository.repo_name == self.db_repo.repo_name,
686 Repository.fork_id == self.db_repo.repo_id))
686 Repository.fork_id == self.db_repo.repo_id))
687
687
688 if filter_query:
688 if filter_query:
689 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
689 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
690 query = query.filter(
690 query = query.filter(
691 Repository.repo_name.ilike(ilike_expression))
691 Repository.repo_name.ilike(ilike_expression))
692
692
693 add_parent = False
693 add_parent = False
694 if self.db_repo.parent:
694 if self.db_repo.parent:
695 if filter_query in self.db_repo.parent.repo_name:
695 if filter_query in self.db_repo.parent.repo_name:
696 parent_vcs_obj = self.db_repo.parent.scm_instance()
696 parent_vcs_obj = self.db_repo.parent.scm_instance()
697 if parent_vcs_obj and not parent_vcs_obj.is_empty():
697 if parent_vcs_obj and not parent_vcs_obj.is_empty():
698 add_parent = True
698 add_parent = True
699
699
700 limit = 20 - 1 if add_parent else 20
700 limit = 20 - 1 if add_parent else 20
701 all_repos = query.limit(limit).all()
701 all_repos = query.limit(limit).all()
702 if add_parent:
702 if add_parent:
703 all_repos += [self.db_repo.parent]
703 all_repos += [self.db_repo.parent]
704
704
705 repos = []
705 repos = []
706 for obj in ScmModel().get_repos(all_repos):
706 for obj in ScmModel().get_repos(all_repos):
707 repos.append({
707 repos.append({
708 'id': obj['name'],
708 'id': obj['name'],
709 'text': obj['name'],
709 'text': obj['name'],
710 'type': 'repo',
710 'type': 'repo',
711 'obj': obj['dbrepo']
711 'obj': obj['dbrepo']
712 })
712 })
713
713
714 data = {
714 data = {
715 'more': False,
715 'more': False,
716 'results': [{
716 'results': [{
717 'text': _('Repositories'),
717 'text': _('Repositories'),
718 'children': repos
718 'children': repos
719 }] if repos else []
719 }] if repos else []
720 }
720 }
721 return data
721 return data
722
722
723 @LoginRequired()
723 @LoginRequired()
724 @NotAnonymous()
724 @NotAnonymous()
725 @HasRepoPermissionAnyDecorator(
725 @HasRepoPermissionAnyDecorator(
726 'repository.read', 'repository.write', 'repository.admin')
726 'repository.read', 'repository.write', 'repository.admin')
727 @CSRFRequired()
727 @CSRFRequired()
728 @view_config(
728 @view_config(
729 route_name='pullrequest_create', request_method='POST',
729 route_name='pullrequest_create', request_method='POST',
730 renderer=None)
730 renderer=None)
731 def pull_request_create(self):
731 def pull_request_create(self):
732 _ = self.request.translate
732 _ = self.request.translate
733 self.assure_not_empty_repo()
733 self.assure_not_empty_repo()
734 self.load_default_context()
734 self.load_default_context()
735
735
736 controls = peppercorn.parse(self.request.POST.items())
736 controls = peppercorn.parse(self.request.POST.items())
737
737
738 try:
738 try:
739 form = PullRequestForm(
739 form = PullRequestForm(
740 self.request.translate, self.db_repo.repo_id)()
740 self.request.translate, self.db_repo.repo_id)()
741 _form = form.to_python(controls)
741 _form = form.to_python(controls)
742 except formencode.Invalid as errors:
742 except formencode.Invalid as errors:
743 if errors.error_dict.get('revisions'):
743 if errors.error_dict.get('revisions'):
744 msg = 'Revisions: %s' % errors.error_dict['revisions']
744 msg = 'Revisions: %s' % errors.error_dict['revisions']
745 elif errors.error_dict.get('pullrequest_title'):
745 elif errors.error_dict.get('pullrequest_title'):
746 msg = _('Pull request requires a title with min. 3 chars')
746 msg = _('Pull request requires a title with min. 3 chars')
747 else:
747 else:
748 msg = _('Error creating pull request: {}').format(errors)
748 msg = _('Error creating pull request: {}').format(errors)
749 log.exception(msg)
749 log.exception(msg)
750 h.flash(msg, 'error')
750 h.flash(msg, 'error')
751
751
752 # would rather just go back to form ...
752 # would rather just go back to form ...
753 raise HTTPFound(
753 raise HTTPFound(
754 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
754 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
755
755
756 source_repo = _form['source_repo']
756 source_repo = _form['source_repo']
757 source_ref = _form['source_ref']
757 source_ref = _form['source_ref']
758 target_repo = _form['target_repo']
758 target_repo = _form['target_repo']
759 target_ref = _form['target_ref']
759 target_ref = _form['target_ref']
760 commit_ids = _form['revisions'][::-1]
760 commit_ids = _form['revisions'][::-1]
761
761
762 # find the ancestor for this pr
762 # find the ancestor for this pr
763 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
763 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
764 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
764 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
765
765
766 # re-check permissions again here
766 # re-check permissions again here
767 # source_repo we must have read permissions
767 # source_repo we must have read permissions
768
768
769 source_perm = HasRepoPermissionAny(
769 source_perm = HasRepoPermissionAny(
770 'repository.read',
770 'repository.read',
771 'repository.write', 'repository.admin')(source_db_repo.repo_name)
771 'repository.write', 'repository.admin')(source_db_repo.repo_name)
772 if not source_perm:
772 if not source_perm:
773 msg = _('Not Enough permissions to source repo `{}`.'.format(
773 msg = _('Not Enough permissions to source repo `{}`.'.format(
774 source_db_repo.repo_name))
774 source_db_repo.repo_name))
775 h.flash(msg, category='error')
775 h.flash(msg, category='error')
776 # copy the args back to redirect
776 # copy the args back to redirect
777 org_query = self.request.GET.mixed()
777 org_query = self.request.GET.mixed()
778 raise HTTPFound(
778 raise HTTPFound(
779 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
779 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
780 _query=org_query))
780 _query=org_query))
781
781
782 # target repo we must have read permissions, and also later on
782 # target repo we must have read permissions, and also later on
783 # we want to check branch permissions here
783 # we want to check branch permissions here
784 target_perm = HasRepoPermissionAny(
784 target_perm = HasRepoPermissionAny(
785 'repository.read',
785 'repository.read',
786 'repository.write', 'repository.admin')(target_db_repo.repo_name)
786 'repository.write', 'repository.admin')(target_db_repo.repo_name)
787 if not target_perm:
787 if not target_perm:
788 msg = _('Not Enough permissions to target repo `{}`.'.format(
788 msg = _('Not Enough permissions to target repo `{}`.'.format(
789 target_db_repo.repo_name))
789 target_db_repo.repo_name))
790 h.flash(msg, category='error')
790 h.flash(msg, category='error')
791 # copy the args back to redirect
791 # copy the args back to redirect
792 org_query = self.request.GET.mixed()
792 org_query = self.request.GET.mixed()
793 raise HTTPFound(
793 raise HTTPFound(
794 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
794 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
795 _query=org_query))
795 _query=org_query))
796
796
797 source_scm = source_db_repo.scm_instance()
797 source_scm = source_db_repo.scm_instance()
798 target_scm = target_db_repo.scm_instance()
798 target_scm = target_db_repo.scm_instance()
799
799
800 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
800 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
801 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
801 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
802
802
803 ancestor = source_scm.get_common_ancestor(
803 ancestor = source_scm.get_common_ancestor(
804 source_commit.raw_id, target_commit.raw_id, target_scm)
804 source_commit.raw_id, target_commit.raw_id, target_scm)
805
805
806 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
806 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
807 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
807 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
808
808
809 pullrequest_title = _form['pullrequest_title']
809 pullrequest_title = _form['pullrequest_title']
810 title_source_ref = source_ref.split(':', 2)[1]
810 title_source_ref = source_ref.split(':', 2)[1]
811 if not pullrequest_title:
811 if not pullrequest_title:
812 pullrequest_title = PullRequestModel().generate_pullrequest_title(
812 pullrequest_title = PullRequestModel().generate_pullrequest_title(
813 source=source_repo,
813 source=source_repo,
814 source_ref=title_source_ref,
814 source_ref=title_source_ref,
815 target=target_repo
815 target=target_repo
816 )
816 )
817
817
818 description = _form['pullrequest_desc']
818 description = _form['pullrequest_desc']
819
819
820 get_default_reviewers_data, validate_default_reviewers = \
820 get_default_reviewers_data, validate_default_reviewers = \
821 PullRequestModel().get_reviewer_functions()
821 PullRequestModel().get_reviewer_functions()
822
822
823 # recalculate reviewers logic, to make sure we can validate this
823 # recalculate reviewers logic, to make sure we can validate this
824 reviewer_rules = get_default_reviewers_data(
824 reviewer_rules = get_default_reviewers_data(
825 self._rhodecode_db_user, source_db_repo,
825 self._rhodecode_db_user, source_db_repo,
826 source_commit, target_db_repo, target_commit)
826 source_commit, target_db_repo, target_commit)
827
827
828 given_reviewers = _form['review_members']
828 given_reviewers = _form['review_members']
829 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
829 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
830
830
831 try:
831 try:
832 pull_request = PullRequestModel().create(
832 pull_request = PullRequestModel().create(
833 self._rhodecode_user.user_id, source_repo, source_ref,
833 self._rhodecode_user.user_id, source_repo, source_ref,
834 target_repo, target_ref, commit_ids, reviewers,
834 target_repo, target_ref, commit_ids, reviewers,
835 pullrequest_title, description, reviewer_rules
835 pullrequest_title, description, reviewer_rules
836 )
836 )
837 Session().commit()
837 Session().commit()
838
838
839 h.flash(_('Successfully opened new pull request'),
839 h.flash(_('Successfully opened new pull request'),
840 category='success')
840 category='success')
841 except Exception:
841 except Exception:
842 msg = _('Error occurred during creation of this pull request.')
842 msg = _('Error occurred during creation of this pull request.')
843 log.exception(msg)
843 log.exception(msg)
844 h.flash(msg, category='error')
844 h.flash(msg, category='error')
845
845
846 # copy the args back to redirect
846 # copy the args back to redirect
847 org_query = self.request.GET.mixed()
847 org_query = self.request.GET.mixed()
848 raise HTTPFound(
848 raise HTTPFound(
849 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
849 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
850 _query=org_query))
850 _query=org_query))
851
851
852 raise HTTPFound(
852 raise HTTPFound(
853 h.route_path('pullrequest_show', repo_name=target_repo,
853 h.route_path('pullrequest_show', repo_name=target_repo,
854 pull_request_id=pull_request.pull_request_id))
854 pull_request_id=pull_request.pull_request_id))
855
855
856 @LoginRequired()
856 @LoginRequired()
857 @NotAnonymous()
857 @NotAnonymous()
858 @HasRepoPermissionAnyDecorator(
858 @HasRepoPermissionAnyDecorator(
859 'repository.read', 'repository.write', 'repository.admin')
859 'repository.read', 'repository.write', 'repository.admin')
860 @CSRFRequired()
860 @CSRFRequired()
861 @view_config(
861 @view_config(
862 route_name='pullrequest_update', request_method='POST',
862 route_name='pullrequest_update', request_method='POST',
863 renderer='json_ext')
863 renderer='json_ext')
864 def pull_request_update(self):
864 def pull_request_update(self):
865 pull_request = PullRequest.get_or_404(
865 pull_request = PullRequest.get_or_404(
866 self.request.matchdict['pull_request_id'])
866 self.request.matchdict['pull_request_id'])
867 _ = self.request.translate
867 _ = self.request.translate
868
868
869 self.load_default_context()
869 self.load_default_context()
870
870
871 if pull_request.is_closed():
871 if pull_request.is_closed():
872 log.debug('update: forbidden because pull request is closed')
872 log.debug('update: forbidden because pull request is closed')
873 msg = _(u'Cannot update closed pull requests.')
873 msg = _(u'Cannot update closed pull requests.')
874 h.flash(msg, category='error')
874 h.flash(msg, category='error')
875 return True
875 return True
876
876
877 # only owner or admin can update it
877 # only owner or admin can update it
878 allowed_to_update = PullRequestModel().check_user_update(
878 allowed_to_update = PullRequestModel().check_user_update(
879 pull_request, self._rhodecode_user)
879 pull_request, self._rhodecode_user)
880 if allowed_to_update:
880 if allowed_to_update:
881 controls = peppercorn.parse(self.request.POST.items())
881 controls = peppercorn.parse(self.request.POST.items())
882
882
883 if 'review_members' in controls:
883 if 'review_members' in controls:
884 self._update_reviewers(
884 self._update_reviewers(
885 pull_request, controls['review_members'],
885 pull_request, controls['review_members'],
886 pull_request.reviewer_data)
886 pull_request.reviewer_data)
887 elif str2bool(self.request.POST.get('update_commits', 'false')):
887 elif str2bool(self.request.POST.get('update_commits', 'false')):
888 self._update_commits(pull_request)
888 self._update_commits(pull_request)
889 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
889 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
890 self._edit_pull_request(pull_request)
890 self._edit_pull_request(pull_request)
891 else:
891 else:
892 raise HTTPBadRequest()
892 raise HTTPBadRequest()
893 return True
893 return True
894 raise HTTPForbidden()
894 raise HTTPForbidden()
895
895
896 def _edit_pull_request(self, pull_request):
896 def _edit_pull_request(self, pull_request):
897 _ = self.request.translate
897 _ = self.request.translate
898 try:
898 try:
899 PullRequestModel().edit(
899 PullRequestModel().edit(
900 pull_request, self.request.POST.get('title'),
900 pull_request, self.request.POST.get('title'),
901 self.request.POST.get('description'), self._rhodecode_user)
901 self.request.POST.get('description'), self._rhodecode_user)
902 except ValueError:
902 except ValueError:
903 msg = _(u'Cannot update closed pull requests.')
903 msg = _(u'Cannot update closed pull requests.')
904 h.flash(msg, category='error')
904 h.flash(msg, category='error')
905 return
905 return
906 else:
906 else:
907 Session().commit()
907 Session().commit()
908
908
909 msg = _(u'Pull request title & description updated.')
909 msg = _(u'Pull request title & description updated.')
910 h.flash(msg, category='success')
910 h.flash(msg, category='success')
911 return
911 return
912
912
913 def _update_commits(self, pull_request):
913 def _update_commits(self, pull_request):
914 _ = self.request.translate
914 _ = self.request.translate
915 resp = PullRequestModel().update_commits(pull_request)
915 resp = PullRequestModel().update_commits(pull_request)
916
916
917 if resp.executed:
917 if resp.executed:
918
918
919 if resp.target_changed and resp.source_changed:
919 if resp.target_changed and resp.source_changed:
920 changed = 'target and source repositories'
920 changed = 'target and source repositories'
921 elif resp.target_changed and not resp.source_changed:
921 elif resp.target_changed and not resp.source_changed:
922 changed = 'target repository'
922 changed = 'target repository'
923 elif not resp.target_changed and resp.source_changed:
923 elif not resp.target_changed and resp.source_changed:
924 changed = 'source repository'
924 changed = 'source repository'
925 else:
925 else:
926 changed = 'nothing'
926 changed = 'nothing'
927
927
928 msg = _(
928 msg = _(
929 u'Pull request updated to "{source_commit_id}" with '
929 u'Pull request updated to "{source_commit_id}" with '
930 u'{count_added} added, {count_removed} removed commits. '
930 u'{count_added} added, {count_removed} removed commits. '
931 u'Source of changes: {change_source}')
931 u'Source of changes: {change_source}')
932 msg = msg.format(
932 msg = msg.format(
933 source_commit_id=pull_request.source_ref_parts.commit_id,
933 source_commit_id=pull_request.source_ref_parts.commit_id,
934 count_added=len(resp.changes.added),
934 count_added=len(resp.changes.added),
935 count_removed=len(resp.changes.removed),
935 count_removed=len(resp.changes.removed),
936 change_source=changed)
936 change_source=changed)
937 h.flash(msg, category='success')
937 h.flash(msg, category='success')
938
938
939 channel = '/repo${}$/pr/{}'.format(
939 channel = '/repo${}$/pr/{}'.format(
940 pull_request.target_repo.repo_name,
940 pull_request.target_repo.repo_name,
941 pull_request.pull_request_id)
941 pull_request.pull_request_id)
942 message = msg + (
942 message = msg + (
943 ' - <a onclick="window.location.reload()">'
943 ' - <a onclick="window.location.reload()">'
944 '<strong>{}</strong></a>'.format(_('Reload page')))
944 '<strong>{}</strong></a>'.format(_('Reload page')))
945 channelstream.post_message(
945 channelstream.post_message(
946 channel, message, self._rhodecode_user.username,
946 channel, message, self._rhodecode_user.username,
947 registry=self.request.registry)
947 registry=self.request.registry)
948 else:
948 else:
949 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
949 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
950 warning_reasons = [
950 warning_reasons = [
951 UpdateFailureReason.NO_CHANGE,
951 UpdateFailureReason.NO_CHANGE,
952 UpdateFailureReason.WRONG_REF_TYPE,
952 UpdateFailureReason.WRONG_REF_TYPE,
953 ]
953 ]
954 category = 'warning' if resp.reason in warning_reasons else 'error'
954 category = 'warning' if resp.reason in warning_reasons else 'error'
955 h.flash(msg, category=category)
955 h.flash(msg, category=category)
956
956
957 @LoginRequired()
957 @LoginRequired()
958 @NotAnonymous()
958 @NotAnonymous()
959 @HasRepoPermissionAnyDecorator(
959 @HasRepoPermissionAnyDecorator(
960 'repository.read', 'repository.write', 'repository.admin')
960 'repository.read', 'repository.write', 'repository.admin')
961 @CSRFRequired()
961 @CSRFRequired()
962 @view_config(
962 @view_config(
963 route_name='pullrequest_merge', request_method='POST',
963 route_name='pullrequest_merge', request_method='POST',
964 renderer='json_ext')
964 renderer='json_ext')
965 def pull_request_merge(self):
965 def pull_request_merge(self):
966 """
966 """
967 Merge will perform a server-side merge of the specified
967 Merge will perform a server-side merge of the specified
968 pull request, if the pull request is approved and mergeable.
968 pull request, if the pull request is approved and mergeable.
969 After successful merging, the pull request is automatically
969 After successful merging, the pull request is automatically
970 closed, with a relevant comment.
970 closed, with a relevant comment.
971 """
971 """
972 pull_request = PullRequest.get_or_404(
972 pull_request = PullRequest.get_or_404(
973 self.request.matchdict['pull_request_id'])
973 self.request.matchdict['pull_request_id'])
974
974
975 self.load_default_context()
975 self.load_default_context()
976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
977 translator=self.request.translate)
977 translator=self.request.translate)
978 merge_possible = not check.failed
978 merge_possible = not check.failed
979
979
980 for err_type, error_msg in check.errors:
980 for err_type, error_msg in check.errors:
981 h.flash(error_msg, category=err_type)
981 h.flash(error_msg, category=err_type)
982
982
983 if merge_possible:
983 if merge_possible:
984 log.debug("Pre-conditions checked, trying to merge.")
984 log.debug("Pre-conditions checked, trying to merge.")
985 extras = vcs_operation_context(
985 extras = vcs_operation_context(
986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
987 username=self._rhodecode_db_user.username, action='push',
987 username=self._rhodecode_db_user.username, action='push',
988 scm=pull_request.target_repo.repo_type)
988 scm=pull_request.target_repo.repo_type)
989 self._merge_pull_request(
989 self._merge_pull_request(
990 pull_request, self._rhodecode_db_user, extras)
990 pull_request, self._rhodecode_db_user, extras)
991 else:
991 else:
992 log.debug("Pre-conditions failed, NOT merging.")
992 log.debug("Pre-conditions failed, NOT merging.")
993
993
994 raise HTTPFound(
994 raise HTTPFound(
995 h.route_path('pullrequest_show',
995 h.route_path('pullrequest_show',
996 repo_name=pull_request.target_repo.repo_name,
996 repo_name=pull_request.target_repo.repo_name,
997 pull_request_id=pull_request.pull_request_id))
997 pull_request_id=pull_request.pull_request_id))
998
998
999 def _merge_pull_request(self, pull_request, user, extras):
999 def _merge_pull_request(self, pull_request, user, extras):
1000 _ = self.request.translate
1000 _ = self.request.translate
1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1002
1002
1003 if merge_resp.executed:
1003 if merge_resp.executed:
1004 log.debug("The merge was successful, closing the pull request.")
1004 log.debug("The merge was successful, closing the pull request.")
1005 PullRequestModel().close_pull_request(
1005 PullRequestModel().close_pull_request(
1006 pull_request.pull_request_id, user)
1006 pull_request.pull_request_id, user)
1007 Session().commit()
1007 Session().commit()
1008 msg = _('Pull request was successfully merged and closed.')
1008 msg = _('Pull request was successfully merged and closed.')
1009 h.flash(msg, category='success')
1009 h.flash(msg, category='success')
1010 else:
1010 else:
1011 log.debug(
1011 log.debug(
1012 "The merge was not successful. Merge response: %s",
1012 "The merge was not successful. Merge response: %s",
1013 merge_resp)
1013 merge_resp)
1014 msg = PullRequestModel().merge_status_message(
1014 msg = PullRequestModel().merge_status_message(
1015 merge_resp.failure_reason)
1015 merge_resp.failure_reason)
1016 h.flash(msg, category='error')
1016 h.flash(msg, category='error')
1017
1017
1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1019 _ = self.request.translate
1019 _ = self.request.translate
1020 get_default_reviewers_data, validate_default_reviewers = \
1020 get_default_reviewers_data, validate_default_reviewers = \
1021 PullRequestModel().get_reviewer_functions()
1021 PullRequestModel().get_reviewer_functions()
1022
1022
1023 try:
1023 try:
1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1025 except ValueError as e:
1025 except ValueError as e:
1026 log.error('Reviewers Validation: {}'.format(e))
1026 log.error('Reviewers Validation: {}'.format(e))
1027 h.flash(e, category='error')
1027 h.flash(e, category='error')
1028 return
1028 return
1029
1029
1030 PullRequestModel().update_reviewers(
1030 PullRequestModel().update_reviewers(
1031 pull_request, reviewers, self._rhodecode_user)
1031 pull_request, reviewers, self._rhodecode_user)
1032 h.flash(_('Pull request reviewers updated.'), category='success')
1032 h.flash(_('Pull request reviewers updated.'), category='success')
1033 Session().commit()
1033 Session().commit()
1034
1034
1035 @LoginRequired()
1035 @LoginRequired()
1036 @NotAnonymous()
1036 @NotAnonymous()
1037 @HasRepoPermissionAnyDecorator(
1037 @HasRepoPermissionAnyDecorator(
1038 'repository.read', 'repository.write', 'repository.admin')
1038 'repository.read', 'repository.write', 'repository.admin')
1039 @CSRFRequired()
1039 @CSRFRequired()
1040 @view_config(
1040 @view_config(
1041 route_name='pullrequest_delete', request_method='POST',
1041 route_name='pullrequest_delete', request_method='POST',
1042 renderer='json_ext')
1042 renderer='json_ext')
1043 def pull_request_delete(self):
1043 def pull_request_delete(self):
1044 _ = self.request.translate
1044 _ = self.request.translate
1045
1045
1046 pull_request = PullRequest.get_or_404(
1046 pull_request = PullRequest.get_or_404(
1047 self.request.matchdict['pull_request_id'])
1047 self.request.matchdict['pull_request_id'])
1048 self.load_default_context()
1048 self.load_default_context()
1049
1049
1050 pr_closed = pull_request.is_closed()
1050 pr_closed = pull_request.is_closed()
1051 allowed_to_delete = PullRequestModel().check_user_delete(
1051 allowed_to_delete = PullRequestModel().check_user_delete(
1052 pull_request, self._rhodecode_user) and not pr_closed
1052 pull_request, self._rhodecode_user) and not pr_closed
1053
1053
1054 # only owner can delete it !
1054 # only owner can delete it !
1055 if allowed_to_delete:
1055 if allowed_to_delete:
1056 PullRequestModel().delete(pull_request, self._rhodecode_user)
1056 PullRequestModel().delete(pull_request, self._rhodecode_user)
1057 Session().commit()
1057 Session().commit()
1058 h.flash(_('Successfully deleted pull request'),
1058 h.flash(_('Successfully deleted pull request'),
1059 category='success')
1059 category='success')
1060 raise HTTPFound(h.route_path('pullrequest_show_all',
1060 raise HTTPFound(h.route_path('pullrequest_show_all',
1061 repo_name=self.db_repo_name))
1061 repo_name=self.db_repo_name))
1062
1062
1063 log.warning('user %s tried to delete pull request without access',
1063 log.warning('user %s tried to delete pull request without access',
1064 self._rhodecode_user)
1064 self._rhodecode_user)
1065 raise HTTPNotFound()
1065 raise HTTPNotFound()
1066
1066
1067 @LoginRequired()
1067 @LoginRequired()
1068 @NotAnonymous()
1068 @NotAnonymous()
1069 @HasRepoPermissionAnyDecorator(
1069 @HasRepoPermissionAnyDecorator(
1070 'repository.read', 'repository.write', 'repository.admin')
1070 'repository.read', 'repository.write', 'repository.admin')
1071 @CSRFRequired()
1071 @CSRFRequired()
1072 @view_config(
1072 @view_config(
1073 route_name='pullrequest_comment_create', request_method='POST',
1073 route_name='pullrequest_comment_create', request_method='POST',
1074 renderer='json_ext')
1074 renderer='json_ext')
1075 def pull_request_comment_create(self):
1075 def pull_request_comment_create(self):
1076 _ = self.request.translate
1076 _ = self.request.translate
1077
1077
1078 pull_request = PullRequest.get_or_404(
1078 pull_request = PullRequest.get_or_404(
1079 self.request.matchdict['pull_request_id'])
1079 self.request.matchdict['pull_request_id'])
1080 pull_request_id = pull_request.pull_request_id
1080 pull_request_id = pull_request.pull_request_id
1081
1081
1082 if pull_request.is_closed():
1082 if pull_request.is_closed():
1083 log.debug('comment: forbidden because pull request is closed')
1083 log.debug('comment: forbidden because pull request is closed')
1084 raise HTTPForbidden()
1084 raise HTTPForbidden()
1085
1085
1086 allowed_to_comment = PullRequestModel().check_user_comment(
1086 allowed_to_comment = PullRequestModel().check_user_comment(
1087 pull_request, self._rhodecode_user)
1087 pull_request, self._rhodecode_user)
1088 if not allowed_to_comment:
1088 if not allowed_to_comment:
1089 log.debug(
1089 log.debug(
1090 'comment: forbidden because pull request is from forbidden repo')
1090 'comment: forbidden because pull request is from forbidden repo')
1091 raise HTTPForbidden()
1091 raise HTTPForbidden()
1092
1092
1093 c = self.load_default_context()
1093 c = self.load_default_context()
1094
1094
1095 status = self.request.POST.get('changeset_status', None)
1095 status = self.request.POST.get('changeset_status', None)
1096 text = self.request.POST.get('text')
1096 text = self.request.POST.get('text')
1097 comment_type = self.request.POST.get('comment_type')
1097 comment_type = self.request.POST.get('comment_type')
1098 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1098 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1099 close_pull_request = self.request.POST.get('close_pull_request')
1099 close_pull_request = self.request.POST.get('close_pull_request')
1100
1100
1101 # the logic here should work like following, if we submit close
1101 # the logic here should work like following, if we submit close
1102 # pr comment, use `close_pull_request_with_comment` function
1102 # pr comment, use `close_pull_request_with_comment` function
1103 # else handle regular comment logic
1103 # else handle regular comment logic
1104
1104
1105 if close_pull_request:
1105 if close_pull_request:
1106 # only owner or admin or person with write permissions
1106 # only owner or admin or person with write permissions
1107 allowed_to_close = PullRequestModel().check_user_update(
1107 allowed_to_close = PullRequestModel().check_user_update(
1108 pull_request, self._rhodecode_user)
1108 pull_request, self._rhodecode_user)
1109 if not allowed_to_close:
1109 if not allowed_to_close:
1110 log.debug('comment: forbidden because not allowed to close '
1110 log.debug('comment: forbidden because not allowed to close '
1111 'pull request %s', pull_request_id)
1111 'pull request %s', pull_request_id)
1112 raise HTTPForbidden()
1112 raise HTTPForbidden()
1113 comment, status = PullRequestModel().close_pull_request_with_comment(
1113 comment, status = PullRequestModel().close_pull_request_with_comment(
1114 pull_request, self._rhodecode_user, self.db_repo, message=text)
1114 pull_request, self._rhodecode_user, self.db_repo, message=text)
1115 Session().flush()
1115 Session().flush()
1116 events.trigger(
1116 events.trigger(
1117 events.PullRequestCommentEvent(pull_request, comment))
1117 events.PullRequestCommentEvent(pull_request, comment))
1118
1118
1119 else:
1119 else:
1120 # regular comment case, could be inline, or one with status.
1120 # regular comment case, could be inline, or one with status.
1121 # for that one we check also permissions
1121 # for that one we check also permissions
1122
1122
1123 allowed_to_change_status = PullRequestModel().check_user_change_status(
1123 allowed_to_change_status = PullRequestModel().check_user_change_status(
1124 pull_request, self._rhodecode_user)
1124 pull_request, self._rhodecode_user)
1125
1125
1126 if status and allowed_to_change_status:
1126 if status and allowed_to_change_status:
1127 message = (_('Status change %(transition_icon)s %(status)s')
1127 message = (_('Status change %(transition_icon)s %(status)s')
1128 % {'transition_icon': '>',
1128 % {'transition_icon': '>',
1129 'status': ChangesetStatus.get_status_lbl(status)})
1129 'status': ChangesetStatus.get_status_lbl(status)})
1130 text = text or message
1130 text = text or message
1131
1131
1132 comment = CommentsModel().create(
1132 comment = CommentsModel().create(
1133 text=text,
1133 text=text,
1134 repo=self.db_repo.repo_id,
1134 repo=self.db_repo.repo_id,
1135 user=self._rhodecode_user.user_id,
1135 user=self._rhodecode_user.user_id,
1136 pull_request=pull_request,
1136 pull_request=pull_request,
1137 f_path=self.request.POST.get('f_path'),
1137 f_path=self.request.POST.get('f_path'),
1138 line_no=self.request.POST.get('line'),
1138 line_no=self.request.POST.get('line'),
1139 status_change=(ChangesetStatus.get_status_lbl(status)
1139 status_change=(ChangesetStatus.get_status_lbl(status)
1140 if status and allowed_to_change_status else None),
1140 if status and allowed_to_change_status else None),
1141 status_change_type=(status
1141 status_change_type=(status
1142 if status and allowed_to_change_status else None),
1142 if status and allowed_to_change_status else None),
1143 comment_type=comment_type,
1143 comment_type=comment_type,
1144 resolves_comment_id=resolves_comment_id
1144 resolves_comment_id=resolves_comment_id
1145 )
1145 )
1146
1146
1147 if allowed_to_change_status:
1147 if allowed_to_change_status:
1148 # calculate old status before we change it
1148 # calculate old status before we change it
1149 old_calculated_status = pull_request.calculated_review_status()
1149 old_calculated_status = pull_request.calculated_review_status()
1150
1150
1151 # get status if set !
1151 # get status if set !
1152 if status:
1152 if status:
1153 ChangesetStatusModel().set_status(
1153 ChangesetStatusModel().set_status(
1154 self.db_repo.repo_id,
1154 self.db_repo.repo_id,
1155 status,
1155 status,
1156 self._rhodecode_user.user_id,
1156 self._rhodecode_user.user_id,
1157 comment,
1157 comment,
1158 pull_request=pull_request
1158 pull_request=pull_request
1159 )
1159 )
1160
1160
1161 Session().flush()
1161 Session().flush()
1162 # this is somehow required to get access to some relationship
1163 # loaded on comment
1164 Session().refresh(comment)
1165
1162 events.trigger(
1166 events.trigger(
1163 events.PullRequestCommentEvent(pull_request, comment))
1167 events.PullRequestCommentEvent(pull_request, comment))
1164
1168
1165 # we now calculate the status of pull request, and based on that
1169 # we now calculate the status of pull request, and based on that
1166 # calculation we set the commits status
1170 # calculation we set the commits status
1167 calculated_status = pull_request.calculated_review_status()
1171 calculated_status = pull_request.calculated_review_status()
1168 if old_calculated_status != calculated_status:
1172 if old_calculated_status != calculated_status:
1169 PullRequestModel()._trigger_pull_request_hook(
1173 PullRequestModel()._trigger_pull_request_hook(
1170 pull_request, self._rhodecode_user, 'review_status_change')
1174 pull_request, self._rhodecode_user, 'review_status_change')
1171
1175
1172 Session().commit()
1176 Session().commit()
1173
1177
1174 data = {
1178 data = {
1175 'target_id': h.safeid(h.safe_unicode(
1179 'target_id': h.safeid(h.safe_unicode(
1176 self.request.POST.get('f_path'))),
1180 self.request.POST.get('f_path'))),
1177 }
1181 }
1178 if comment:
1182 if comment:
1179 c.co = comment
1183 c.co = comment
1180 rendered_comment = render(
1184 rendered_comment = render(
1181 'rhodecode:templates/changeset/changeset_comment_block.mako',
1185 'rhodecode:templates/changeset/changeset_comment_block.mako',
1182 self._get_template_context(c), self.request)
1186 self._get_template_context(c), self.request)
1183
1187
1184 data.update(comment.get_dict())
1188 data.update(comment.get_dict())
1185 data.update({'rendered_text': rendered_comment})
1189 data.update({'rendered_text': rendered_comment})
1186
1190
1187 return data
1191 return data
1188
1192
1189 @LoginRequired()
1193 @LoginRequired()
1190 @NotAnonymous()
1194 @NotAnonymous()
1191 @HasRepoPermissionAnyDecorator(
1195 @HasRepoPermissionAnyDecorator(
1192 'repository.read', 'repository.write', 'repository.admin')
1196 'repository.read', 'repository.write', 'repository.admin')
1193 @CSRFRequired()
1197 @CSRFRequired()
1194 @view_config(
1198 @view_config(
1195 route_name='pullrequest_comment_delete', request_method='POST',
1199 route_name='pullrequest_comment_delete', request_method='POST',
1196 renderer='json_ext')
1200 renderer='json_ext')
1197 def pull_request_comment_delete(self):
1201 def pull_request_comment_delete(self):
1198 pull_request = PullRequest.get_or_404(
1202 pull_request = PullRequest.get_or_404(
1199 self.request.matchdict['pull_request_id'])
1203 self.request.matchdict['pull_request_id'])
1200
1204
1201 comment = ChangesetComment.get_or_404(
1205 comment = ChangesetComment.get_or_404(
1202 self.request.matchdict['comment_id'])
1206 self.request.matchdict['comment_id'])
1203 comment_id = comment.comment_id
1207 comment_id = comment.comment_id
1204
1208
1205 if pull_request.is_closed():
1209 if pull_request.is_closed():
1206 log.debug('comment: forbidden because pull request is closed')
1210 log.debug('comment: forbidden because pull request is closed')
1207 raise HTTPForbidden()
1211 raise HTTPForbidden()
1208
1212
1209 if not comment:
1213 if not comment:
1210 log.debug('Comment with id:%s not found, skipping', comment_id)
1214 log.debug('Comment with id:%s not found, skipping', comment_id)
1211 # comment already deleted in another call probably
1215 # comment already deleted in another call probably
1212 return True
1216 return True
1213
1217
1214 if comment.pull_request.is_closed():
1218 if comment.pull_request.is_closed():
1215 # don't allow deleting comments on closed pull request
1219 # don't allow deleting comments on closed pull request
1216 raise HTTPForbidden()
1220 raise HTTPForbidden()
1217
1221
1218 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1222 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1219 super_admin = h.HasPermissionAny('hg.admin')()
1223 super_admin = h.HasPermissionAny('hg.admin')()
1220 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1224 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1221 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1225 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1222 comment_repo_admin = is_repo_admin and is_repo_comment
1226 comment_repo_admin = is_repo_admin and is_repo_comment
1223
1227
1224 if super_admin or comment_owner or comment_repo_admin:
1228 if super_admin or comment_owner or comment_repo_admin:
1225 old_calculated_status = comment.pull_request.calculated_review_status()
1229 old_calculated_status = comment.pull_request.calculated_review_status()
1226 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1230 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1227 Session().commit()
1231 Session().commit()
1228 calculated_status = comment.pull_request.calculated_review_status()
1232 calculated_status = comment.pull_request.calculated_review_status()
1229 if old_calculated_status != calculated_status:
1233 if old_calculated_status != calculated_status:
1230 PullRequestModel()._trigger_pull_request_hook(
1234 PullRequestModel()._trigger_pull_request_hook(
1231 comment.pull_request, self._rhodecode_user, 'review_status_change')
1235 comment.pull_request, self._rhodecode_user, 'review_status_change')
1232 return True
1236 return True
1233 else:
1237 else:
1234 log.warning('No permissions for user %s to delete comment_id: %s',
1238 log.warning('No permissions for user %s to delete comment_id: %s',
1235 self._rhodecode_db_user, comment_id)
1239 self._rhodecode_db_user, comment_id)
1236 raise HTTPNotFound()
1240 raise HTTPNotFound()
@@ -1,269 +1,267 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 """
22 Changeset status conttroller
23 """
24
21
25 import itertools
22 import itertools
26 import logging
23 import logging
27 from collections import defaultdict
24 from collections import defaultdict
28
25
29 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32 from rhodecode.lib.markup_renderer import (
30 from rhodecode.lib.markup_renderer import (
33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
34
32
35 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
36
34
37
35
38 class ChangesetStatusModel(BaseModel):
36 class ChangesetStatusModel(BaseModel):
39
37
40 cls = ChangesetStatus
38 cls = ChangesetStatus
41
39
42 def __get_changeset_status(self, changeset_status):
40 def __get_changeset_status(self, changeset_status):
43 return self._get_instance(ChangesetStatus, changeset_status)
41 return self._get_instance(ChangesetStatus, changeset_status)
44
42
45 def __get_pull_request(self, pull_request):
43 def __get_pull_request(self, pull_request):
46 return self._get_instance(PullRequest, pull_request)
44 return self._get_instance(PullRequest, pull_request)
47
45
48 def _get_status_query(self, repo, revision, pull_request,
46 def _get_status_query(self, repo, revision, pull_request,
49 with_revisions=False):
47 with_revisions=False):
50 repo = self._get_repo(repo)
48 repo = self._get_repo(repo)
51
49
52 q = ChangesetStatus.query()\
50 q = ChangesetStatus.query()\
53 .filter(ChangesetStatus.repo == repo)
51 .filter(ChangesetStatus.repo == repo)
54 if not with_revisions:
52 if not with_revisions:
55 q = q.filter(ChangesetStatus.version == 0)
53 q = q.filter(ChangesetStatus.version == 0)
56
54
57 if revision:
55 if revision:
58 q = q.filter(ChangesetStatus.revision == revision)
56 q = q.filter(ChangesetStatus.revision == revision)
59 elif pull_request:
57 elif pull_request:
60 pull_request = self.__get_pull_request(pull_request)
58 pull_request = self.__get_pull_request(pull_request)
61 # TODO: johbo: Think about the impact of this join, there must
59 # TODO: johbo: Think about the impact of this join, there must
62 # be a reason why ChangesetStatus and ChanagesetComment is linked
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
63 # to the pull request. Might be that we want to do the same for
61 # to the pull request. Might be that we want to do the same for
64 # the pull_request_version_id.
62 # the pull_request_version_id.
65 q = q.join(ChangesetComment).filter(
63 q = q.join(ChangesetComment).filter(
66 ChangesetStatus.pull_request == pull_request,
64 ChangesetStatus.pull_request == pull_request,
67 ChangesetComment.pull_request_version_id == None)
65 ChangesetComment.pull_request_version_id == None)
68 else:
66 else:
69 raise Exception('Please specify revision or pull_request')
67 raise Exception('Please specify revision or pull_request')
70 q = q.order_by(ChangesetStatus.version.asc())
68 q = q.order_by(ChangesetStatus.version.asc())
71 return q
69 return q
72
70
73 def calculate_status(self, statuses_by_reviewers):
71 def calculate_status(self, statuses_by_reviewers):
74 """
72 """
75 Given the approval statuses from reviewers, calculates final approval
73 Given the approval statuses from reviewers, calculates final approval
76 status. There can only be 3 results, all approved, all rejected. If
74 status. There can only be 3 results, all approved, all rejected. If
77 there is no consensus the PR is under review.
75 there is no consensus the PR is under review.
78
76
79 :param statuses_by_reviewers:
77 :param statuses_by_reviewers:
80 """
78 """
81 votes = defaultdict(int)
79 votes = defaultdict(int)
82 reviewers_number = len(statuses_by_reviewers)
80 reviewers_number = len(statuses_by_reviewers)
83 for user, reasons, mandatory, statuses in statuses_by_reviewers:
81 for user, reasons, mandatory, statuses in statuses_by_reviewers:
84 if statuses:
82 if statuses:
85 ver, latest = statuses[0]
83 ver, latest = statuses[0]
86 votes[latest.status] += 1
84 votes[latest.status] += 1
87 else:
85 else:
88 votes[ChangesetStatus.DEFAULT] += 1
86 votes[ChangesetStatus.DEFAULT] += 1
89
87
90 # all approved
88 # all approved
91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
89 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
92 return ChangesetStatus.STATUS_APPROVED
90 return ChangesetStatus.STATUS_APPROVED
93
91
94 # all rejected
92 # all rejected
95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
93 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
96 return ChangesetStatus.STATUS_REJECTED
94 return ChangesetStatus.STATUS_REJECTED
97
95
98 return ChangesetStatus.STATUS_UNDER_REVIEW
96 return ChangesetStatus.STATUS_UNDER_REVIEW
99
97
100 def get_statuses(self, repo, revision=None, pull_request=None,
98 def get_statuses(self, repo, revision=None, pull_request=None,
101 with_revisions=False):
99 with_revisions=False):
102 q = self._get_status_query(repo, revision, pull_request,
100 q = self._get_status_query(repo, revision, pull_request,
103 with_revisions)
101 with_revisions)
104 return q.all()
102 return q.all()
105
103
106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
104 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
107 """
105 """
108 Returns latest status of changeset for given revision or for given
106 Returns latest status of changeset for given revision or for given
109 pull request. Statuses are versioned inside a table itself and
107 pull request. Statuses are versioned inside a table itself and
110 version == 0 is always the current one
108 version == 0 is always the current one
111
109
112 :param repo:
110 :param repo:
113 :param revision: 40char hash or None
111 :param revision: 40char hash or None
114 :param pull_request: pull_request reference
112 :param pull_request: pull_request reference
115 :param as_str: return status as string not object
113 :param as_str: return status as string not object
116 """
114 """
117 q = self._get_status_query(repo, revision, pull_request)
115 q = self._get_status_query(repo, revision, pull_request)
118
116
119 # need to use first here since there can be multiple statuses
117 # need to use first here since there can be multiple statuses
120 # returned from pull_request
118 # returned from pull_request
121 status = q.first()
119 status = q.first()
122 if as_str:
120 if as_str:
123 status = status.status if status else status
121 status = status.status if status else status
124 st = status or ChangesetStatus.DEFAULT
122 st = status or ChangesetStatus.DEFAULT
125 return str(st)
123 return str(st)
126 return status
124 return status
127
125
128 def _render_auto_status_message(
126 def _render_auto_status_message(
129 self, status, commit_id=None, pull_request=None):
127 self, status, commit_id=None, pull_request=None):
130 """
128 """
131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
129 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
132 so it's always looking the same disregarding on which default
130 so it's always looking the same disregarding on which default
133 renderer system is using.
131 renderer system is using.
134
132
135 :param status: status text to change into
133 :param status: status text to change into
136 :param commit_id: the commit_id we change the status for
134 :param commit_id: the commit_id we change the status for
137 :param pull_request: the pull request we change the status for
135 :param pull_request: the pull request we change the status for
138 """
136 """
139
137
140 new_status = ChangesetStatus.get_status_lbl(status)
138 new_status = ChangesetStatus.get_status_lbl(status)
141
139
142 params = {
140 params = {
143 'new_status_label': new_status,
141 'new_status_label': new_status,
144 'pull_request': pull_request,
142 'pull_request': pull_request,
145 'commit_id': commit_id,
143 'commit_id': commit_id,
146 }
144 }
147 renderer = RstTemplateRenderer()
145 renderer = RstTemplateRenderer()
148 return renderer.render('auto_status_change.mako', **params)
146 return renderer.render('auto_status_change.mako', **params)
149
147
150 def set_status(self, repo, status, user, comment=None, revision=None,
148 def set_status(self, repo, status, user, comment=None, revision=None,
151 pull_request=None, dont_allow_on_closed_pull_request=False):
149 pull_request=None, dont_allow_on_closed_pull_request=False):
152 """
150 """
153 Creates new status for changeset or updates the old ones bumping their
151 Creates new status for changeset or updates the old ones bumping their
154 version, leaving the current status at
152 version, leaving the current status at
155
153
156 :param repo:
154 :param repo:
157 :param revision:
155 :param revision:
158 :param status:
156 :param status:
159 :param user:
157 :param user:
160 :param comment:
158 :param comment:
161 :param dont_allow_on_closed_pull_request: don't allow a status change
159 :param dont_allow_on_closed_pull_request: don't allow a status change
162 if last status was for pull request and it's closed. We shouldn't
160 if last status was for pull request and it's closed. We shouldn't
163 mess around this manually
161 mess around this manually
164 """
162 """
165 repo = self._get_repo(repo)
163 repo = self._get_repo(repo)
166
164
167 q = ChangesetStatus.query()
165 q = ChangesetStatus.query()
168
166
169 if revision:
167 if revision:
170 q = q.filter(ChangesetStatus.repo == repo)
168 q = q.filter(ChangesetStatus.repo == repo)
171 q = q.filter(ChangesetStatus.revision == revision)
169 q = q.filter(ChangesetStatus.revision == revision)
172 elif pull_request:
170 elif pull_request:
173 pull_request = self.__get_pull_request(pull_request)
171 pull_request = self.__get_pull_request(pull_request)
174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
172 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
173 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
176 cur_statuses = q.all()
174 cur_statuses = q.all()
177
175
178 # if statuses exists and last is associated with a closed pull request
176 # if statuses exists and last is associated with a closed pull request
179 # we need to check if we can allow this status change
177 # we need to check if we can allow this status change
180 if (dont_allow_on_closed_pull_request and cur_statuses
178 if (dont_allow_on_closed_pull_request and cur_statuses
181 and getattr(cur_statuses[0].pull_request, 'status', '')
179 and getattr(cur_statuses[0].pull_request, 'status', '')
182 == PullRequest.STATUS_CLOSED):
180 == PullRequest.STATUS_CLOSED):
183 raise StatusChangeOnClosedPullRequestError(
181 raise StatusChangeOnClosedPullRequestError(
184 'Changing status on closed pull request is not allowed'
182 'Changing status on closed pull request is not allowed'
185 )
183 )
186
184
187 # update all current statuses with older version
185 # update all current statuses with older version
188 if cur_statuses:
186 if cur_statuses:
189 for st in cur_statuses:
187 for st in cur_statuses:
190 st.version += 1
188 st.version += 1
191 self.sa.add(st)
189 Session().add(st)
192
190
193 def _create_status(user, repo, status, comment, revision, pull_request):
191 def _create_status(user, repo, status, comment, revision, pull_request):
194 new_status = ChangesetStatus()
192 new_status = ChangesetStatus()
195 new_status.author = self._get_user(user)
193 new_status.author = self._get_user(user)
196 new_status.repo = self._get_repo(repo)
194 new_status.repo = self._get_repo(repo)
197 new_status.status = status
195 new_status.status = status
198 new_status.comment = comment
196 new_status.comment = comment
199 new_status.revision = revision
197 new_status.revision = revision
200 new_status.pull_request = pull_request
198 new_status.pull_request = pull_request
201 return new_status
199 return new_status
202
200
203 if not comment:
201 if not comment:
204 from rhodecode.model.comment import CommentsModel
202 from rhodecode.model.comment import CommentsModel
205 comment = CommentsModel().create(
203 comment = CommentsModel().create(
206 text=self._render_auto_status_message(
204 text=self._render_auto_status_message(
207 status, commit_id=revision, pull_request=pull_request),
205 status, commit_id=revision, pull_request=pull_request),
208 repo=repo,
206 repo=repo,
209 user=user,
207 user=user,
210 pull_request=pull_request,
208 pull_request=pull_request,
211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
209 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
212 )
210 )
213
211
214 if revision:
212 if revision:
215 new_status = _create_status(
213 new_status = _create_status(
216 user=user, repo=repo, status=status, comment=comment,
214 user=user, repo=repo, status=status, comment=comment,
217 revision=revision, pull_request=pull_request)
215 revision=revision, pull_request=pull_request)
218 self.sa.add(new_status)
216 Session().add(new_status)
219 return new_status
217 return new_status
220 elif pull_request:
218 elif pull_request:
221 # pull request can have more than one revision associated to it
219 # pull request can have more than one revision associated to it
222 # we need to create new version for each one
220 # we need to create new version for each one
223 new_statuses = []
221 new_statuses = []
224 repo = pull_request.source_repo
222 repo = pull_request.source_repo
225 for rev in pull_request.revisions:
223 for rev in pull_request.revisions:
226 new_status = _create_status(
224 new_status = _create_status(
227 user=user, repo=repo, status=status, comment=comment,
225 user=user, repo=repo, status=status, comment=comment,
228 revision=rev, pull_request=pull_request)
226 revision=rev, pull_request=pull_request)
229 new_statuses.append(new_status)
227 new_statuses.append(new_status)
230 self.sa.add(new_status)
228 Session().add(new_status)
231 return new_statuses
229 return new_statuses
232
230
233 def reviewers_statuses(self, pull_request):
231 def reviewers_statuses(self, pull_request):
234 _commit_statuses = self.get_statuses(
232 _commit_statuses = self.get_statuses(
235 pull_request.source_repo,
233 pull_request.source_repo,
236 pull_request=pull_request,
234 pull_request=pull_request,
237 with_revisions=True)
235 with_revisions=True)
238
236
239 commit_statuses = defaultdict(list)
237 commit_statuses = defaultdict(list)
240 for st in _commit_statuses:
238 for st in _commit_statuses:
241 commit_statuses[st.author.username] += [st]
239 commit_statuses[st.author.username] += [st]
242
240
243 pull_request_reviewers = []
241 pull_request_reviewers = []
244
242
245 def version(commit_status):
243 def version(commit_status):
246 return commit_status.version
244 return commit_status.version
247
245
248 for o in pull_request.reviewers:
246 for o in pull_request.reviewers:
249 if not o.user:
247 if not o.user:
250 continue
248 continue
251 statuses = commit_statuses.get(o.user.username, None)
249 statuses = commit_statuses.get(o.user.username, None)
252 if statuses:
250 if statuses:
253 statuses = [(x, list(y)[0])
251 statuses = [(x, list(y)[0])
254 for x, y in (itertools.groupby(
252 for x, y in (itertools.groupby(
255 sorted(statuses, key=version),version))]
253 sorted(statuses, key=version),version))]
256
254
257 pull_request_reviewers.append(
255 pull_request_reviewers.append(
258 (o.user, o.reasons, o.mandatory, statuses))
256 (o.user, o.reasons, o.mandatory, statuses))
259 return pull_request_reviewers
257 return pull_request_reviewers
260
258
261 def calculated_review_status(self, pull_request, reviewers_statuses=None):
259 def calculated_review_status(self, pull_request, reviewers_statuses=None):
262 """
260 """
263 calculate pull request status based on reviewers, it should be a list
261 calculate pull request status based on reviewers, it should be a list
264 of two element lists.
262 of two element lists.
265
263
266 :param reviewers_statuses:
264 :param reviewers_statuses:
267 """
265 """
268 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
266 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
269 return self.calculate_status(reviewers)
267 return self.calculate_status(reviewers)
@@ -1,659 +1,660 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 pyramid.threadlocal import get_current_registry, get_current_request
29 from pyramid.threadlocal import get_current_registry, get_current_request
30 from sqlalchemy.sql.expression import null
30 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.functions import coalesce
31 from sqlalchemy.sql.functions import coalesce
32
32
33 from rhodecode.lib import helpers as h, diffs, channelstream
33 from rhodecode.lib import helpers as h, diffs, channelstream
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 from rhodecode.model import BaseModel
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import (
37 from rhodecode.model.db import (
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 from rhodecode.model.notification import NotificationModel
39 from rhodecode.model.notification import NotificationModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.settings import VcsSettingsModel
41 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.model.notification import EmailNotificationModel
42 from rhodecode.model.notification import EmailNotificationModel
43 from rhodecode.model.validation_schema.schemas import comment_schema
43 from rhodecode.model.validation_schema.schemas import comment_schema
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class CommentsModel(BaseModel):
49 class CommentsModel(BaseModel):
50
50
51 cls = ChangesetComment
51 cls = ChangesetComment
52
52
53 DIFF_CONTEXT_BEFORE = 3
53 DIFF_CONTEXT_BEFORE = 3
54 DIFF_CONTEXT_AFTER = 3
54 DIFF_CONTEXT_AFTER = 3
55
55
56 def __get_commit_comment(self, changeset_comment):
56 def __get_commit_comment(self, changeset_comment):
57 return self._get_instance(ChangesetComment, changeset_comment)
57 return self._get_instance(ChangesetComment, changeset_comment)
58
58
59 def __get_pull_request(self, pull_request):
59 def __get_pull_request(self, pull_request):
60 return self._get_instance(PullRequest, pull_request)
60 return self._get_instance(PullRequest, pull_request)
61
61
62 def _extract_mentions(self, s):
62 def _extract_mentions(self, s):
63 user_objects = []
63 user_objects = []
64 for username in extract_mentioned_users(s):
64 for username in extract_mentioned_users(s):
65 user_obj = User.get_by_username(username, case_insensitive=True)
65 user_obj = User.get_by_username(username, case_insensitive=True)
66 if user_obj:
66 if user_obj:
67 user_objects.append(user_obj)
67 user_objects.append(user_obj)
68 return user_objects
68 return user_objects
69
69
70 def _get_renderer(self, global_renderer='rst', request=None):
70 def _get_renderer(self, global_renderer='rst', request=None):
71 request = request or get_current_request()
71 request = request or get_current_request()
72
72
73 try:
73 try:
74 global_renderer = request.call_context.visual.default_renderer
74 global_renderer = request.call_context.visual.default_renderer
75 except AttributeError:
75 except AttributeError:
76 log.debug("Renderer not set, falling back "
76 log.debug("Renderer not set, falling back "
77 "to default renderer '%s'", global_renderer)
77 "to default renderer '%s'", global_renderer)
78 except Exception:
78 except Exception:
79 log.error(traceback.format_exc())
79 log.error(traceback.format_exc())
80 return global_renderer
80 return global_renderer
81
81
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 # group by versions, and count until, and display objects
83 # group by versions, and count until, and display objects
84
84
85 comment_groups = collections.defaultdict(list)
85 comment_groups = collections.defaultdict(list)
86 [comment_groups[
86 [comment_groups[
87 _co.pull_request_version_id].append(_co) for _co in comments]
87 _co.pull_request_version_id].append(_co) for _co in comments]
88
88
89 def yield_comments(pos):
89 def yield_comments(pos):
90 for co in comment_groups[pos]:
90 for co in comment_groups[pos]:
91 yield co
91 yield co
92
92
93 comment_versions = collections.defaultdict(
93 comment_versions = collections.defaultdict(
94 lambda: collections.defaultdict(list))
94 lambda: collections.defaultdict(list))
95 prev_prvid = -1
95 prev_prvid = -1
96 # fake last entry with None, to aggregate on "latest" version which
96 # fake last entry with None, to aggregate on "latest" version which
97 # doesn't have an pull_request_version_id
97 # doesn't have an pull_request_version_id
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 prvid = ver.pull_request_version_id
99 prvid = ver.pull_request_version_id
100 if prev_prvid == -1:
100 if prev_prvid == -1:
101 prev_prvid = prvid
101 prev_prvid = prvid
102
102
103 for co in yield_comments(prvid):
103 for co in yield_comments(prvid):
104 comment_versions[prvid]['at'].append(co)
104 comment_versions[prvid]['at'].append(co)
105
105
106 # save until
106 # save until
107 current = comment_versions[prvid]['at']
107 current = comment_versions[prvid]['at']
108 prev_until = comment_versions[prev_prvid]['until']
108 prev_until = comment_versions[prev_prvid]['until']
109 cur_until = prev_until + current
109 cur_until = prev_until + current
110 comment_versions[prvid]['until'].extend(cur_until)
110 comment_versions[prvid]['until'].extend(cur_until)
111
111
112 # save outdated
112 # save outdated
113 if inline:
113 if inline:
114 outdated = [x for x in cur_until
114 outdated = [x for x in cur_until
115 if x.outdated_at_version(show_version)]
115 if x.outdated_at_version(show_version)]
116 else:
116 else:
117 outdated = [x for x in cur_until
117 outdated = [x for x in cur_until
118 if x.older_than_version(show_version)]
118 if x.older_than_version(show_version)]
119 display = [x for x in cur_until if x not in outdated]
119 display = [x for x in cur_until if x not in outdated]
120
120
121 comment_versions[prvid]['outdated'] = outdated
121 comment_versions[prvid]['outdated'] = outdated
122 comment_versions[prvid]['display'] = display
122 comment_versions[prvid]['display'] = display
123
123
124 prev_prvid = prvid
124 prev_prvid = prvid
125
125
126 return comment_versions
126 return comment_versions
127
127
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
129
129
130 todos = Session().query(ChangesetComment) \
130 todos = Session().query(ChangesetComment) \
131 .filter(ChangesetComment.pull_request == pull_request) \
131 .filter(ChangesetComment.pull_request == pull_request) \
132 .filter(ChangesetComment.resolved_by == None) \
132 .filter(ChangesetComment.resolved_by == None) \
133 .filter(ChangesetComment.comment_type
133 .filter(ChangesetComment.comment_type
134 == ChangesetComment.COMMENT_TYPE_TODO)
134 == ChangesetComment.COMMENT_TYPE_TODO)
135
135
136 if not show_outdated:
136 if not show_outdated:
137 todos = todos.filter(
137 todos = todos.filter(
138 coalesce(ChangesetComment.display_state, '') !=
138 coalesce(ChangesetComment.display_state, '') !=
139 ChangesetComment.COMMENT_OUTDATED)
139 ChangesetComment.COMMENT_OUTDATED)
140
140
141 todos = todos.all()
141 todos = todos.all()
142
142
143 return todos
143 return todos
144
144
145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
146
146
147 todos = Session().query(ChangesetComment) \
147 todos = Session().query(ChangesetComment) \
148 .filter(ChangesetComment.revision == commit_id) \
148 .filter(ChangesetComment.revision == commit_id) \
149 .filter(ChangesetComment.resolved_by == None) \
149 .filter(ChangesetComment.resolved_by == None) \
150 .filter(ChangesetComment.comment_type
150 .filter(ChangesetComment.comment_type
151 == ChangesetComment.COMMENT_TYPE_TODO)
151 == ChangesetComment.COMMENT_TYPE_TODO)
152
152
153 if not show_outdated:
153 if not show_outdated:
154 todos = todos.filter(
154 todos = todos.filter(
155 coalesce(ChangesetComment.display_state, '') !=
155 coalesce(ChangesetComment.display_state, '') !=
156 ChangesetComment.COMMENT_OUTDATED)
156 ChangesetComment.COMMENT_OUTDATED)
157
157
158 todos = todos.all()
158 todos = todos.all()
159
159
160 return todos
160 return todos
161
161
162 def _log_audit_action(self, action, action_data, user, comment):
162 def _log_audit_action(self, action, action_data, user, comment):
163 audit_logger.store(
163 audit_logger.store(
164 action=action,
164 action=action,
165 action_data=action_data,
165 action_data=action_data,
166 user=user,
166 user=user,
167 repo=comment.repo)
167 repo=comment.repo)
168
168
169 def create(self, text, repo, user, commit_id=None, pull_request=None,
169 def create(self, text, repo, user, commit_id=None, pull_request=None,
170 f_path=None, line_no=None, status_change=None,
170 f_path=None, line_no=None, status_change=None,
171 status_change_type=None, comment_type=None,
171 status_change_type=None, comment_type=None,
172 resolves_comment_id=None, closing_pr=False, send_email=True,
172 resolves_comment_id=None, closing_pr=False, send_email=True,
173 renderer=None):
173 renderer=None):
174 """
174 """
175 Creates new comment for commit or pull request.
175 Creates new comment for commit or pull request.
176 IF status_change is not none this comment is associated with a
176 IF status_change is not none this comment is associated with a
177 status change of commit or commit associated with pull request
177 status change of commit or commit associated with pull request
178
178
179 :param text:
179 :param text:
180 :param repo:
180 :param repo:
181 :param user:
181 :param user:
182 :param commit_id:
182 :param commit_id:
183 :param pull_request:
183 :param pull_request:
184 :param f_path:
184 :param f_path:
185 :param line_no:
185 :param line_no:
186 :param status_change: Label for status change
186 :param status_change: Label for status change
187 :param comment_type: Type of comment
187 :param comment_type: Type of comment
188 :param status_change_type: type of status change
188 :param status_change_type: type of status change
189 :param closing_pr:
189 :param closing_pr:
190 :param send_email:
190 :param send_email:
191 :param renderer: pick renderer for this comment
191 :param renderer: pick renderer for this comment
192 """
192 """
193 if not text:
193 if not text:
194 log.warning('Missing text for comment, skipping...')
194 log.warning('Missing text for comment, skipping...')
195 return
195 return
196 request = get_current_request()
196 request = get_current_request()
197 _ = request.translate
197 _ = request.translate
198
198
199 if not renderer:
199 if not renderer:
200 renderer = self._get_renderer(request=request)
200 renderer = self._get_renderer(request=request)
201
201
202 repo = self._get_repo(repo)
202 repo = self._get_repo(repo)
203 user = self._get_user(user)
203 user = self._get_user(user)
204
204
205 schema = comment_schema.CommentSchema()
205 schema = comment_schema.CommentSchema()
206 validated_kwargs = schema.deserialize(dict(
206 validated_kwargs = schema.deserialize(dict(
207 comment_body=text,
207 comment_body=text,
208 comment_type=comment_type,
208 comment_type=comment_type,
209 comment_file=f_path,
209 comment_file=f_path,
210 comment_line=line_no,
210 comment_line=line_no,
211 renderer_type=renderer,
211 renderer_type=renderer,
212 status_change=status_change_type,
212 status_change=status_change_type,
213 resolves_comment_id=resolves_comment_id,
213 resolves_comment_id=resolves_comment_id,
214 repo=repo.repo_id,
214 repo=repo.repo_id,
215 user=user.user_id,
215 user=user.user_id,
216 ))
216 ))
217
217
218 comment = ChangesetComment()
218 comment = ChangesetComment()
219 comment.renderer = validated_kwargs['renderer_type']
219 comment.renderer = validated_kwargs['renderer_type']
220 comment.text = validated_kwargs['comment_body']
220 comment.text = validated_kwargs['comment_body']
221 comment.f_path = validated_kwargs['comment_file']
221 comment.f_path = validated_kwargs['comment_file']
222 comment.line_no = validated_kwargs['comment_line']
222 comment.line_no = validated_kwargs['comment_line']
223 comment.comment_type = validated_kwargs['comment_type']
223 comment.comment_type = validated_kwargs['comment_type']
224
224
225 comment.repo = repo
225 comment.repo = repo
226 comment.author = user
226 comment.author = user
227 resolved_comment = self.__get_commit_comment(
227 resolved_comment = self.__get_commit_comment(
228 validated_kwargs['resolves_comment_id'])
228 validated_kwargs['resolves_comment_id'])
229 # check if the comment actually belongs to this PR
229 # check if the comment actually belongs to this PR
230 if resolved_comment and resolved_comment.pull_request and \
230 if resolved_comment and resolved_comment.pull_request and \
231 resolved_comment.pull_request != pull_request:
231 resolved_comment.pull_request != pull_request:
232 # comment not bound to this pull request, forbid
232 # comment not bound to this pull request, forbid
233 resolved_comment = None
233 resolved_comment = None
234 comment.resolved_comment = resolved_comment
234 comment.resolved_comment = resolved_comment
235
235
236 pull_request_id = pull_request
236 pull_request_id = pull_request
237
237
238 commit_obj = None
238 commit_obj = None
239 pull_request_obj = None
239 pull_request_obj = None
240
240
241 if commit_id:
241 if commit_id:
242 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
242 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
243 # do a lookup, so we don't pass something bad here
243 # do a lookup, so we don't pass something bad here
244 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
244 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
245 comment.revision = commit_obj.raw_id
245 comment.revision = commit_obj.raw_id
246
246
247 elif pull_request_id:
247 elif pull_request_id:
248 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
248 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
249 pull_request_obj = self.__get_pull_request(pull_request_id)
249 pull_request_obj = self.__get_pull_request(pull_request_id)
250 comment.pull_request = pull_request_obj
250 comment.pull_request = pull_request_obj
251 else:
251 else:
252 raise Exception('Please specify commit or pull_request_id')
252 raise Exception('Please specify commit or pull_request_id')
253
253
254 Session().add(comment)
254 Session().add(comment)
255 Session().flush()
255 Session().flush()
256 kwargs = {
256 kwargs = {
257 'user': user,
257 'user': user,
258 'renderer_type': renderer,
258 'renderer_type': renderer,
259 'repo_name': repo.repo_name,
259 'repo_name': repo.repo_name,
260 'status_change': status_change,
260 'status_change': status_change,
261 'status_change_type': status_change_type,
261 'status_change_type': status_change_type,
262 'comment_body': text,
262 'comment_body': text,
263 'comment_file': f_path,
263 'comment_file': f_path,
264 'comment_line': line_no,
264 'comment_line': line_no,
265 'comment_type': comment_type or 'note'
265 'comment_type': comment_type or 'note'
266 }
266 }
267
267
268 if commit_obj:
268 if commit_obj:
269 recipients = ChangesetComment.get_users(
269 recipients = ChangesetComment.get_users(
270 revision=commit_obj.raw_id)
270 revision=commit_obj.raw_id)
271 # add commit author if it's in RhodeCode system
271 # add commit author if it's in RhodeCode system
272 cs_author = User.get_from_cs_author(commit_obj.author)
272 cs_author = User.get_from_cs_author(commit_obj.author)
273 if not cs_author:
273 if not cs_author:
274 # use repo owner if we cannot extract the author correctly
274 # use repo owner if we cannot extract the author correctly
275 cs_author = repo.user
275 cs_author = repo.user
276 recipients += [cs_author]
276 recipients += [cs_author]
277
277
278 commit_comment_url = self.get_url(comment, request=request)
278 commit_comment_url = self.get_url(comment, request=request)
279
279
280 target_repo_url = h.link_to(
280 target_repo_url = h.link_to(
281 repo.repo_name,
281 repo.repo_name,
282 h.route_url('repo_summary', repo_name=repo.repo_name))
282 h.route_url('repo_summary', repo_name=repo.repo_name))
283
283
284 # commit specifics
284 # commit specifics
285 kwargs.update({
285 kwargs.update({
286 'commit': commit_obj,
286 'commit': commit_obj,
287 'commit_message': commit_obj.message,
287 'commit_message': commit_obj.message,
288 'commit_target_repo': target_repo_url,
288 'commit_target_repo': target_repo_url,
289 'commit_comment_url': commit_comment_url,
289 'commit_comment_url': commit_comment_url,
290 })
290 })
291
291
292 elif pull_request_obj:
292 elif pull_request_obj:
293 # get the current participants of this pull request
293 # get the current participants of this pull request
294 recipients = ChangesetComment.get_users(
294 recipients = ChangesetComment.get_users(
295 pull_request_id=pull_request_obj.pull_request_id)
295 pull_request_id=pull_request_obj.pull_request_id)
296 # add pull request author
296 # add pull request author
297 recipients += [pull_request_obj.author]
297 recipients += [pull_request_obj.author]
298
298
299 # add the reviewers to notification
299 # add the reviewers to notification
300 recipients += [x.user for x in pull_request_obj.reviewers]
300 recipients += [x.user for x in pull_request_obj.reviewers]
301
301
302 pr_target_repo = pull_request_obj.target_repo
302 pr_target_repo = pull_request_obj.target_repo
303 pr_source_repo = pull_request_obj.source_repo
303 pr_source_repo = pull_request_obj.source_repo
304
304
305 pr_comment_url = h.route_url(
305 pr_comment_url = h.route_url(
306 'pullrequest_show',
306 'pullrequest_show',
307 repo_name=pr_target_repo.repo_name,
307 repo_name=pr_target_repo.repo_name,
308 pull_request_id=pull_request_obj.pull_request_id,
308 pull_request_id=pull_request_obj.pull_request_id,
309 _anchor='comment-%s' % comment.comment_id)
309 _anchor='comment-%s' % comment.comment_id)
310
310
311 # set some variables for email notification
311 # set some variables for email notification
312 pr_target_repo_url = h.route_url(
312 pr_target_repo_url = h.route_url(
313 'repo_summary', repo_name=pr_target_repo.repo_name)
313 'repo_summary', repo_name=pr_target_repo.repo_name)
314
314
315 pr_source_repo_url = h.route_url(
315 pr_source_repo_url = h.route_url(
316 'repo_summary', repo_name=pr_source_repo.repo_name)
316 'repo_summary', repo_name=pr_source_repo.repo_name)
317
317
318 # pull request specifics
318 # pull request specifics
319 kwargs.update({
319 kwargs.update({
320 'pull_request': pull_request_obj,
320 'pull_request': pull_request_obj,
321 'pr_id': pull_request_obj.pull_request_id,
321 'pr_id': pull_request_obj.pull_request_id,
322 'pr_target_repo': pr_target_repo,
322 'pr_target_repo': pr_target_repo,
323 'pr_target_repo_url': pr_target_repo_url,
323 'pr_target_repo_url': pr_target_repo_url,
324 'pr_source_repo': pr_source_repo,
324 'pr_source_repo': pr_source_repo,
325 'pr_source_repo_url': pr_source_repo_url,
325 'pr_source_repo_url': pr_source_repo_url,
326 'pr_comment_url': pr_comment_url,
326 'pr_comment_url': pr_comment_url,
327 'pr_closing': closing_pr,
327 'pr_closing': closing_pr,
328 })
328 })
329 if send_email:
329 if send_email:
330 # pre-generate the subject for notification itself
330 # pre-generate the subject for notification itself
331 (subject,
331 (subject,
332 _h, _e, # we don't care about those
332 _h, _e, # we don't care about those
333 body_plaintext) = EmailNotificationModel().render_email(
333 body_plaintext) = EmailNotificationModel().render_email(
334 notification_type, **kwargs)
334 notification_type, **kwargs)
335
335
336 mention_recipients = set(
336 mention_recipients = set(
337 self._extract_mentions(text)).difference(recipients)
337 self._extract_mentions(text)).difference(recipients)
338
338
339 # create notification objects, and emails
339 # create notification objects, and emails
340 NotificationModel().create(
340 NotificationModel().create(
341 created_by=user,
341 created_by=user,
342 notification_subject=subject,
342 notification_subject=subject,
343 notification_body=body_plaintext,
343 notification_body=body_plaintext,
344 notification_type=notification_type,
344 notification_type=notification_type,
345 recipients=recipients,
345 recipients=recipients,
346 mention_recipients=mention_recipients,
346 mention_recipients=mention_recipients,
347 email_kwargs=kwargs,
347 email_kwargs=kwargs,
348 )
348 )
349
349
350 Session().flush()
350 Session().flush()
351 if comment.pull_request:
351 if comment.pull_request:
352 action = 'repo.pull_request.comment.create'
352 action = 'repo.pull_request.comment.create'
353 else:
353 else:
354 action = 'repo.commit.comment.create'
354 action = 'repo.commit.comment.create'
355
355
356 comment_data = comment.get_api_data()
356 comment_data = comment.get_api_data()
357 self._log_audit_action(
357 self._log_audit_action(
358 action, {'data': comment_data}, user, comment)
358 action, {'data': comment_data}, user, comment)
359
359
360 msg_url = ''
360 msg_url = ''
361 channel = None
361 channel = None
362 if commit_obj:
362 if commit_obj:
363 msg_url = commit_comment_url
363 msg_url = commit_comment_url
364 repo_name = repo.repo_name
364 repo_name = repo.repo_name
365 channel = u'/repo${}$/commit/{}'.format(
365 channel = u'/repo${}$/commit/{}'.format(
366 repo_name,
366 repo_name,
367 commit_obj.raw_id
367 commit_obj.raw_id
368 )
368 )
369 elif pull_request_obj:
369 elif pull_request_obj:
370 msg_url = pr_comment_url
370 msg_url = pr_comment_url
371 repo_name = pr_target_repo.repo_name
371 repo_name = pr_target_repo.repo_name
372 channel = u'/repo${}$/pr/{}'.format(
372 channel = u'/repo${}$/pr/{}'.format(
373 repo_name,
373 repo_name,
374 pull_request_id
374 pull_request_id
375 )
375 )
376
376
377 message = '<strong>{}</strong> {} - ' \
377 message = '<strong>{}</strong> {} - ' \
378 '<a onclick="window.location=\'{}\';' \
378 '<a onclick="window.location=\'{}\';' \
379 'window.location.reload()">' \
379 'window.location.reload()">' \
380 '<strong>{}</strong></a>'
380 '<strong>{}</strong></a>'
381 message = message.format(
381 message = message.format(
382 user.username, _('made a comment'), msg_url,
382 user.username, _('made a comment'), msg_url,
383 _('Show it now'))
383 _('Show it now'))
384
384
385 channelstream.post_message(
385 channelstream.post_message(
386 channel, message, user.username,
386 channel, message, user.username,
387 registry=get_current_registry())
387 registry=get_current_registry())
388
388
389 return comment
389 return comment
390
390
391 def delete(self, comment, user):
391 def delete(self, comment, user):
392 """
392 """
393 Deletes given comment
393 Deletes given comment
394 """
394 """
395 comment = self.__get_commit_comment(comment)
395 comment = self.__get_commit_comment(comment)
396 old_data = comment.get_api_data()
396 old_data = comment.get_api_data()
397 Session().delete(comment)
397 Session().delete(comment)
398
398
399 if comment.pull_request:
399 if comment.pull_request:
400 action = 'repo.pull_request.comment.delete'
400 action = 'repo.pull_request.comment.delete'
401 else:
401 else:
402 action = 'repo.commit.comment.delete'
402 action = 'repo.commit.comment.delete'
403
403
404 self._log_audit_action(
404 self._log_audit_action(
405 action, {'old_data': old_data}, user, comment)
405 action, {'old_data': old_data}, user, comment)
406
406
407 return comment
407 return comment
408
408
409 def get_all_comments(self, repo_id, revision=None, pull_request=None):
409 def get_all_comments(self, repo_id, revision=None, pull_request=None):
410 q = ChangesetComment.query()\
410 q = ChangesetComment.query()\
411 .filter(ChangesetComment.repo_id == repo_id)
411 .filter(ChangesetComment.repo_id == repo_id)
412 if revision:
412 if revision:
413 q = q.filter(ChangesetComment.revision == revision)
413 q = q.filter(ChangesetComment.revision == revision)
414 elif pull_request:
414 elif pull_request:
415 pull_request = self.__get_pull_request(pull_request)
415 pull_request = self.__get_pull_request(pull_request)
416 q = q.filter(ChangesetComment.pull_request == pull_request)
416 q = q.filter(ChangesetComment.pull_request == pull_request)
417 else:
417 else:
418 raise Exception('Please specify commit or pull_request')
418 raise Exception('Please specify commit or pull_request')
419 q = q.order_by(ChangesetComment.created_on)
419 q = q.order_by(ChangesetComment.created_on)
420 return q.all()
420 return q.all()
421
421
422 def get_url(self, comment, request=None, permalink=False):
422 def get_url(self, comment, request=None, permalink=False):
423 if not request:
423 if not request:
424 request = get_current_request()
424 request = get_current_request()
425
425
426 comment = self.__get_commit_comment(comment)
426 comment = self.__get_commit_comment(comment)
427 if comment.pull_request:
427 if comment.pull_request:
428 pull_request = comment.pull_request
428 pull_request = comment.pull_request
429 if permalink:
429 if permalink:
430 return request.route_url(
430 return request.route_url(
431 'pull_requests_global',
431 'pull_requests_global',
432 pull_request_id=pull_request.pull_request_id,
432 pull_request_id=pull_request.pull_request_id,
433 _anchor='comment-%s' % comment.comment_id)
433 _anchor='comment-%s' % comment.comment_id)
434 else:
434 else:
435 return request.route_url('pullrequest_show',
435 return request.route_url(
436 'pullrequest_show',
436 repo_name=safe_str(pull_request.target_repo.repo_name),
437 repo_name=safe_str(pull_request.target_repo.repo_name),
437 pull_request_id=pull_request.pull_request_id,
438 pull_request_id=pull_request.pull_request_id,
438 _anchor='comment-%s' % comment.comment_id)
439 _anchor='comment-%s' % comment.comment_id)
439
440
440 else:
441 else:
441 repo = comment.repo
442 repo = comment.repo
442 commit_id = comment.revision
443 commit_id = comment.revision
443
444
444 if permalink:
445 if permalink:
445 return request.route_url(
446 return request.route_url(
446 'repo_commit', repo_name=safe_str(repo.repo_id),
447 'repo_commit', repo_name=safe_str(repo.repo_id),
447 commit_id=commit_id,
448 commit_id=commit_id,
448 _anchor='comment-%s' % comment.comment_id)
449 _anchor='comment-%s' % comment.comment_id)
449
450
450 else:
451 else:
451 return request.route_url(
452 return request.route_url(
452 'repo_commit', repo_name=safe_str(repo.repo_name),
453 'repo_commit', repo_name=safe_str(repo.repo_name),
453 commit_id=commit_id,
454 commit_id=commit_id,
454 _anchor='comment-%s' % comment.comment_id)
455 _anchor='comment-%s' % comment.comment_id)
455
456
456 def get_comments(self, repo_id, revision=None, pull_request=None):
457 def get_comments(self, repo_id, revision=None, pull_request=None):
457 """
458 """
458 Gets main comments based on revision or pull_request_id
459 Gets main comments based on revision or pull_request_id
459
460
460 :param repo_id:
461 :param repo_id:
461 :param revision:
462 :param revision:
462 :param pull_request:
463 :param pull_request:
463 """
464 """
464
465
465 q = ChangesetComment.query()\
466 q = ChangesetComment.query()\
466 .filter(ChangesetComment.repo_id == repo_id)\
467 .filter(ChangesetComment.repo_id == repo_id)\
467 .filter(ChangesetComment.line_no == None)\
468 .filter(ChangesetComment.line_no == None)\
468 .filter(ChangesetComment.f_path == None)
469 .filter(ChangesetComment.f_path == None)
469 if revision:
470 if revision:
470 q = q.filter(ChangesetComment.revision == revision)
471 q = q.filter(ChangesetComment.revision == revision)
471 elif pull_request:
472 elif pull_request:
472 pull_request = self.__get_pull_request(pull_request)
473 pull_request = self.__get_pull_request(pull_request)
473 q = q.filter(ChangesetComment.pull_request == pull_request)
474 q = q.filter(ChangesetComment.pull_request == pull_request)
474 else:
475 else:
475 raise Exception('Please specify commit or pull_request')
476 raise Exception('Please specify commit or pull_request')
476 q = q.order_by(ChangesetComment.created_on)
477 q = q.order_by(ChangesetComment.created_on)
477 return q.all()
478 return q.all()
478
479
479 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
480 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
480 q = self._get_inline_comments_query(repo_id, revision, pull_request)
481 q = self._get_inline_comments_query(repo_id, revision, pull_request)
481 return self._group_comments_by_path_and_line_number(q)
482 return self._group_comments_by_path_and_line_number(q)
482
483
483 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
484 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
484 version=None):
485 version=None):
485 inline_cnt = 0
486 inline_cnt = 0
486 for fname, per_line_comments in inline_comments.iteritems():
487 for fname, per_line_comments in inline_comments.iteritems():
487 for lno, comments in per_line_comments.iteritems():
488 for lno, comments in per_line_comments.iteritems():
488 for comm in comments:
489 for comm in comments:
489 if not comm.outdated_at_version(version) and skip_outdated:
490 if not comm.outdated_at_version(version) and skip_outdated:
490 inline_cnt += 1
491 inline_cnt += 1
491
492
492 return inline_cnt
493 return inline_cnt
493
494
494 def get_outdated_comments(self, repo_id, pull_request):
495 def get_outdated_comments(self, repo_id, pull_request):
495 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
496 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
496 # of a pull request.
497 # of a pull request.
497 q = self._all_inline_comments_of_pull_request(pull_request)
498 q = self._all_inline_comments_of_pull_request(pull_request)
498 q = q.filter(
499 q = q.filter(
499 ChangesetComment.display_state ==
500 ChangesetComment.display_state ==
500 ChangesetComment.COMMENT_OUTDATED
501 ChangesetComment.COMMENT_OUTDATED
501 ).order_by(ChangesetComment.comment_id.asc())
502 ).order_by(ChangesetComment.comment_id.asc())
502
503
503 return self._group_comments_by_path_and_line_number(q)
504 return self._group_comments_by_path_and_line_number(q)
504
505
505 def _get_inline_comments_query(self, repo_id, revision, pull_request):
506 def _get_inline_comments_query(self, repo_id, revision, pull_request):
506 # TODO: johbo: Split this into two methods: One for PR and one for
507 # TODO: johbo: Split this into two methods: One for PR and one for
507 # commit.
508 # commit.
508 if revision:
509 if revision:
509 q = Session().query(ChangesetComment).filter(
510 q = Session().query(ChangesetComment).filter(
510 ChangesetComment.repo_id == repo_id,
511 ChangesetComment.repo_id == repo_id,
511 ChangesetComment.line_no != null(),
512 ChangesetComment.line_no != null(),
512 ChangesetComment.f_path != null(),
513 ChangesetComment.f_path != null(),
513 ChangesetComment.revision == revision)
514 ChangesetComment.revision == revision)
514
515
515 elif pull_request:
516 elif pull_request:
516 pull_request = self.__get_pull_request(pull_request)
517 pull_request = self.__get_pull_request(pull_request)
517 if not CommentsModel.use_outdated_comments(pull_request):
518 if not CommentsModel.use_outdated_comments(pull_request):
518 q = self._visible_inline_comments_of_pull_request(pull_request)
519 q = self._visible_inline_comments_of_pull_request(pull_request)
519 else:
520 else:
520 q = self._all_inline_comments_of_pull_request(pull_request)
521 q = self._all_inline_comments_of_pull_request(pull_request)
521
522
522 else:
523 else:
523 raise Exception('Please specify commit or pull_request_id')
524 raise Exception('Please specify commit or pull_request_id')
524 q = q.order_by(ChangesetComment.comment_id.asc())
525 q = q.order_by(ChangesetComment.comment_id.asc())
525 return q
526 return q
526
527
527 def _group_comments_by_path_and_line_number(self, q):
528 def _group_comments_by_path_and_line_number(self, q):
528 comments = q.all()
529 comments = q.all()
529 paths = collections.defaultdict(lambda: collections.defaultdict(list))
530 paths = collections.defaultdict(lambda: collections.defaultdict(list))
530 for co in comments:
531 for co in comments:
531 paths[co.f_path][co.line_no].append(co)
532 paths[co.f_path][co.line_no].append(co)
532 return paths
533 return paths
533
534
534 @classmethod
535 @classmethod
535 def needed_extra_diff_context(cls):
536 def needed_extra_diff_context(cls):
536 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
537 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
537
538
538 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
539 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
539 if not CommentsModel.use_outdated_comments(pull_request):
540 if not CommentsModel.use_outdated_comments(pull_request):
540 return
541 return
541
542
542 comments = self._visible_inline_comments_of_pull_request(pull_request)
543 comments = self._visible_inline_comments_of_pull_request(pull_request)
543 comments_to_outdate = comments.all()
544 comments_to_outdate = comments.all()
544
545
545 for comment in comments_to_outdate:
546 for comment in comments_to_outdate:
546 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
547 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
547
548
548 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
549 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
549 diff_line = _parse_comment_line_number(comment.line_no)
550 diff_line = _parse_comment_line_number(comment.line_no)
550
551
551 try:
552 try:
552 old_context = old_diff_proc.get_context_of_line(
553 old_context = old_diff_proc.get_context_of_line(
553 path=comment.f_path, diff_line=diff_line)
554 path=comment.f_path, diff_line=diff_line)
554 new_context = new_diff_proc.get_context_of_line(
555 new_context = new_diff_proc.get_context_of_line(
555 path=comment.f_path, diff_line=diff_line)
556 path=comment.f_path, diff_line=diff_line)
556 except (diffs.LineNotInDiffException,
557 except (diffs.LineNotInDiffException,
557 diffs.FileNotInDiffException):
558 diffs.FileNotInDiffException):
558 comment.display_state = ChangesetComment.COMMENT_OUTDATED
559 comment.display_state = ChangesetComment.COMMENT_OUTDATED
559 return
560 return
560
561
561 if old_context == new_context:
562 if old_context == new_context:
562 return
563 return
563
564
564 if self._should_relocate_diff_line(diff_line):
565 if self._should_relocate_diff_line(diff_line):
565 new_diff_lines = new_diff_proc.find_context(
566 new_diff_lines = new_diff_proc.find_context(
566 path=comment.f_path, context=old_context,
567 path=comment.f_path, context=old_context,
567 offset=self.DIFF_CONTEXT_BEFORE)
568 offset=self.DIFF_CONTEXT_BEFORE)
568 if not new_diff_lines:
569 if not new_diff_lines:
569 comment.display_state = ChangesetComment.COMMENT_OUTDATED
570 comment.display_state = ChangesetComment.COMMENT_OUTDATED
570 else:
571 else:
571 new_diff_line = self._choose_closest_diff_line(
572 new_diff_line = self._choose_closest_diff_line(
572 diff_line, new_diff_lines)
573 diff_line, new_diff_lines)
573 comment.line_no = _diff_to_comment_line_number(new_diff_line)
574 comment.line_no = _diff_to_comment_line_number(new_diff_line)
574 else:
575 else:
575 comment.display_state = ChangesetComment.COMMENT_OUTDATED
576 comment.display_state = ChangesetComment.COMMENT_OUTDATED
576
577
577 def _should_relocate_diff_line(self, diff_line):
578 def _should_relocate_diff_line(self, diff_line):
578 """
579 """
579 Checks if relocation shall be tried for the given `diff_line`.
580 Checks if relocation shall be tried for the given `diff_line`.
580
581
581 If a comment points into the first lines, then we can have a situation
582 If a comment points into the first lines, then we can have a situation
582 that after an update another line has been added on top. In this case
583 that after an update another line has been added on top. In this case
583 we would find the context still and move the comment around. This
584 we would find the context still and move the comment around. This
584 would be wrong.
585 would be wrong.
585 """
586 """
586 should_relocate = (
587 should_relocate = (
587 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
588 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
588 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
589 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
589 return should_relocate
590 return should_relocate
590
591
591 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
592 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
592 candidate = new_diff_lines[0]
593 candidate = new_diff_lines[0]
593 best_delta = _diff_line_delta(diff_line, candidate)
594 best_delta = _diff_line_delta(diff_line, candidate)
594 for new_diff_line in new_diff_lines[1:]:
595 for new_diff_line in new_diff_lines[1:]:
595 delta = _diff_line_delta(diff_line, new_diff_line)
596 delta = _diff_line_delta(diff_line, new_diff_line)
596 if delta < best_delta:
597 if delta < best_delta:
597 candidate = new_diff_line
598 candidate = new_diff_line
598 best_delta = delta
599 best_delta = delta
599 return candidate
600 return candidate
600
601
601 def _visible_inline_comments_of_pull_request(self, pull_request):
602 def _visible_inline_comments_of_pull_request(self, pull_request):
602 comments = self._all_inline_comments_of_pull_request(pull_request)
603 comments = self._all_inline_comments_of_pull_request(pull_request)
603 comments = comments.filter(
604 comments = comments.filter(
604 coalesce(ChangesetComment.display_state, '') !=
605 coalesce(ChangesetComment.display_state, '') !=
605 ChangesetComment.COMMENT_OUTDATED)
606 ChangesetComment.COMMENT_OUTDATED)
606 return comments
607 return comments
607
608
608 def _all_inline_comments_of_pull_request(self, pull_request):
609 def _all_inline_comments_of_pull_request(self, pull_request):
609 comments = Session().query(ChangesetComment)\
610 comments = Session().query(ChangesetComment)\
610 .filter(ChangesetComment.line_no != None)\
611 .filter(ChangesetComment.line_no != None)\
611 .filter(ChangesetComment.f_path != None)\
612 .filter(ChangesetComment.f_path != None)\
612 .filter(ChangesetComment.pull_request == pull_request)
613 .filter(ChangesetComment.pull_request == pull_request)
613 return comments
614 return comments
614
615
615 def _all_general_comments_of_pull_request(self, pull_request):
616 def _all_general_comments_of_pull_request(self, pull_request):
616 comments = Session().query(ChangesetComment)\
617 comments = Session().query(ChangesetComment)\
617 .filter(ChangesetComment.line_no == None)\
618 .filter(ChangesetComment.line_no == None)\
618 .filter(ChangesetComment.f_path == None)\
619 .filter(ChangesetComment.f_path == None)\
619 .filter(ChangesetComment.pull_request == pull_request)
620 .filter(ChangesetComment.pull_request == pull_request)
620 return comments
621 return comments
621
622
622 @staticmethod
623 @staticmethod
623 def use_outdated_comments(pull_request):
624 def use_outdated_comments(pull_request):
624 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
625 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
625 settings = settings_model.get_general_settings()
626 settings = settings_model.get_general_settings()
626 return settings.get('rhodecode_use_outdated_comments', False)
627 return settings.get('rhodecode_use_outdated_comments', False)
627
628
628
629
629 def _parse_comment_line_number(line_no):
630 def _parse_comment_line_number(line_no):
630 """
631 """
631 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
632 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
632 """
633 """
633 old_line = None
634 old_line = None
634 new_line = None
635 new_line = None
635 if line_no.startswith('o'):
636 if line_no.startswith('o'):
636 old_line = int(line_no[1:])
637 old_line = int(line_no[1:])
637 elif line_no.startswith('n'):
638 elif line_no.startswith('n'):
638 new_line = int(line_no[1:])
639 new_line = int(line_no[1:])
639 else:
640 else:
640 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
641 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
641 return diffs.DiffLineNumber(old_line, new_line)
642 return diffs.DiffLineNumber(old_line, new_line)
642
643
643
644
644 def _diff_to_comment_line_number(diff_line):
645 def _diff_to_comment_line_number(diff_line):
645 if diff_line.new is not None:
646 if diff_line.new is not None:
646 return u'n{}'.format(diff_line.new)
647 return u'n{}'.format(diff_line.new)
647 elif diff_line.old is not None:
648 elif diff_line.old is not None:
648 return u'o{}'.format(diff_line.old)
649 return u'o{}'.format(diff_line.old)
649 return u''
650 return u''
650
651
651
652
652 def _diff_line_delta(a, b):
653 def _diff_line_delta(a, b):
653 if None not in (a.new, b.new):
654 if None not in (a.new, b.new):
654 return abs(a.new - b.new)
655 return abs(a.new - b.new)
655 elif None not in (a.old, b.old):
656 elif None not in (a.old, b.old):
656 return abs(a.old - b.old)
657 return abs(a.old - b.old)
657 else:
658 else:
658 raise ValueError(
659 raise ValueError(
659 "Cannot compute delta between {} and {}".format(a, b))
660 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now