##// END OF EJS Templates
pull-requests: comments don't order by versions since we anyway order by unique number.
marcink -
r1706:2260b99c default
parent child Browse files
Show More
@@ -1,1095 +1,1096 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def __before__(self):
70 def __before__(self):
71 super(PullrequestsController, self).__before__()
71 super(PullrequestsController, self).__before__()
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
74
74
75 def _extract_ordering(self, request):
75 def _extract_ordering(self, request):
76 column_index = safe_int(request.GET.get('order[0][column]'))
76 column_index = safe_int(request.GET.get('order[0][column]'))
77 order_dir = request.GET.get('order[0][dir]', 'desc')
77 order_dir = request.GET.get('order[0][dir]', 'desc')
78 order_by = request.GET.get(
78 order_by = request.GET.get(
79 'columns[%s][data][sort]' % column_index, 'name_raw')
79 'columns[%s][data][sort]' % column_index, 'name_raw')
80 return order_by, order_dir
80 return order_by, order_dir
81
81
82 @LoginRequired()
82 @LoginRequired()
83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
83 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
84 'repository.admin')
84 'repository.admin')
85 @HasAcceptedRepoType('git', 'hg')
85 @HasAcceptedRepoType('git', 'hg')
86 def show_all(self, repo_name):
86 def show_all(self, repo_name):
87 # filter types
87 # filter types
88 c.active = 'open'
88 c.active = 'open'
89 c.source = str2bool(request.GET.get('source'))
89 c.source = str2bool(request.GET.get('source'))
90 c.closed = str2bool(request.GET.get('closed'))
90 c.closed = str2bool(request.GET.get('closed'))
91 c.my = str2bool(request.GET.get('my'))
91 c.my = str2bool(request.GET.get('my'))
92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
92 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
93 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
94 c.repo_name = repo_name
94 c.repo_name = repo_name
95
95
96 opened_by = None
96 opened_by = None
97 if c.my:
97 if c.my:
98 c.active = 'my'
98 c.active = 'my'
99 opened_by = [c.rhodecode_user.user_id]
99 opened_by = [c.rhodecode_user.user_id]
100
100
101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
101 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
102 if c.closed:
102 if c.closed:
103 c.active = 'closed'
103 c.active = 'closed'
104 statuses = [PullRequest.STATUS_CLOSED]
104 statuses = [PullRequest.STATUS_CLOSED]
105
105
106 if c.awaiting_review and not c.source:
106 if c.awaiting_review and not c.source:
107 c.active = 'awaiting'
107 c.active = 'awaiting'
108 if c.source and not c.awaiting_review:
108 if c.source and not c.awaiting_review:
109 c.active = 'source'
109 c.active = 'source'
110 if c.awaiting_my_review:
110 if c.awaiting_my_review:
111 c.active = 'awaiting_my'
111 c.active = 'awaiting_my'
112
112
113 data = self._get_pull_requests_list(
113 data = self._get_pull_requests_list(
114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
114 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
115 if not request.is_xhr:
115 if not request.is_xhr:
116 c.data = json.dumps(data['data'])
116 c.data = json.dumps(data['data'])
117 c.records_total = data['recordsTotal']
117 c.records_total = data['recordsTotal']
118 return render('/pullrequests/pullrequests.mako')
118 return render('/pullrequests/pullrequests.mako')
119 else:
119 else:
120 return json.dumps(data)
120 return json.dumps(data)
121
121
122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
122 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
123 # pagination
123 # pagination
124 start = safe_int(request.GET.get('start'), 0)
124 start = safe_int(request.GET.get('start'), 0)
125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
125 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
126 order_by, order_dir = self._extract_ordering(request)
126 order_by, order_dir = self._extract_ordering(request)
127
127
128 if c.awaiting_review:
128 if c.awaiting_review:
129 pull_requests = PullRequestModel().get_awaiting_review(
129 pull_requests = PullRequestModel().get_awaiting_review(
130 repo_name, source=c.source, opened_by=opened_by,
130 repo_name, source=c.source, opened_by=opened_by,
131 statuses=statuses, offset=start, length=length,
131 statuses=statuses, offset=start, length=length,
132 order_by=order_by, order_dir=order_dir)
132 order_by=order_by, order_dir=order_dir)
133 pull_requests_total_count = PullRequestModel(
133 pull_requests_total_count = PullRequestModel(
134 ).count_awaiting_review(
134 ).count_awaiting_review(
135 repo_name, source=c.source, statuses=statuses,
135 repo_name, source=c.source, statuses=statuses,
136 opened_by=opened_by)
136 opened_by=opened_by)
137 elif c.awaiting_my_review:
137 elif c.awaiting_my_review:
138 pull_requests = PullRequestModel().get_awaiting_my_review(
138 pull_requests = PullRequestModel().get_awaiting_my_review(
139 repo_name, source=c.source, opened_by=opened_by,
139 repo_name, source=c.source, opened_by=opened_by,
140 user_id=c.rhodecode_user.user_id, statuses=statuses,
140 user_id=c.rhodecode_user.user_id, statuses=statuses,
141 offset=start, length=length, order_by=order_by,
141 offset=start, length=length, order_by=order_by,
142 order_dir=order_dir)
142 order_dir=order_dir)
143 pull_requests_total_count = PullRequestModel(
143 pull_requests_total_count = PullRequestModel(
144 ).count_awaiting_my_review(
144 ).count_awaiting_my_review(
145 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
145 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
146 statuses=statuses, opened_by=opened_by)
146 statuses=statuses, opened_by=opened_by)
147 else:
147 else:
148 pull_requests = PullRequestModel().get_all(
148 pull_requests = PullRequestModel().get_all(
149 repo_name, source=c.source, opened_by=opened_by,
149 repo_name, source=c.source, opened_by=opened_by,
150 statuses=statuses, offset=start, length=length,
150 statuses=statuses, offset=start, length=length,
151 order_by=order_by, order_dir=order_dir)
151 order_by=order_by, order_dir=order_dir)
152 pull_requests_total_count = PullRequestModel().count_all(
152 pull_requests_total_count = PullRequestModel().count_all(
153 repo_name, source=c.source, statuses=statuses,
153 repo_name, source=c.source, statuses=statuses,
154 opened_by=opened_by)
154 opened_by=opened_by)
155
155
156 from rhodecode.lib.utils import PartialRenderer
156 from rhodecode.lib.utils import PartialRenderer
157 _render = PartialRenderer('data_table/_dt_elements.mako')
157 _render = PartialRenderer('data_table/_dt_elements.mako')
158 data = []
158 data = []
159 for pr in pull_requests:
159 for pr in pull_requests:
160 comments = CommentsModel().get_all_comments(
160 comments = CommentsModel().get_all_comments(
161 c.rhodecode_db_repo.repo_id, pull_request=pr)
161 c.rhodecode_db_repo.repo_id, pull_request=pr)
162
162
163 data.append({
163 data.append({
164 'name': _render('pullrequest_name',
164 'name': _render('pullrequest_name',
165 pr.pull_request_id, pr.target_repo.repo_name),
165 pr.pull_request_id, pr.target_repo.repo_name),
166 'name_raw': pr.pull_request_id,
166 'name_raw': pr.pull_request_id,
167 'status': _render('pullrequest_status',
167 'status': _render('pullrequest_status',
168 pr.calculated_review_status()),
168 pr.calculated_review_status()),
169 'title': _render(
169 'title': _render(
170 'pullrequest_title', pr.title, pr.description),
170 'pullrequest_title', pr.title, pr.description),
171 'description': h.escape(pr.description),
171 'description': h.escape(pr.description),
172 'updated_on': _render('pullrequest_updated_on',
172 'updated_on': _render('pullrequest_updated_on',
173 h.datetime_to_time(pr.updated_on)),
173 h.datetime_to_time(pr.updated_on)),
174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
174 'updated_on_raw': h.datetime_to_time(pr.updated_on),
175 'created_on': _render('pullrequest_updated_on',
175 'created_on': _render('pullrequest_updated_on',
176 h.datetime_to_time(pr.created_on)),
176 h.datetime_to_time(pr.created_on)),
177 'created_on_raw': h.datetime_to_time(pr.created_on),
177 'created_on_raw': h.datetime_to_time(pr.created_on),
178 'author': _render('pullrequest_author',
178 'author': _render('pullrequest_author',
179 pr.author.full_contact, ),
179 pr.author.full_contact, ),
180 'author_raw': pr.author.full_name,
180 'author_raw': pr.author.full_name,
181 'comments': _render('pullrequest_comments', len(comments)),
181 'comments': _render('pullrequest_comments', len(comments)),
182 'comments_raw': len(comments),
182 'comments_raw': len(comments),
183 'closed': pr.is_closed(),
183 'closed': pr.is_closed(),
184 })
184 })
185 # json used to render the grid
185 # json used to render the grid
186 data = ({
186 data = ({
187 'data': data,
187 'data': data,
188 'recordsTotal': pull_requests_total_count,
188 'recordsTotal': pull_requests_total_count,
189 'recordsFiltered': pull_requests_total_count,
189 'recordsFiltered': pull_requests_total_count,
190 })
190 })
191 return data
191 return data
192
192
193 @LoginRequired()
193 @LoginRequired()
194 @NotAnonymous()
194 @NotAnonymous()
195 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
195 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 'repository.admin')
196 'repository.admin')
197 @HasAcceptedRepoType('git', 'hg')
197 @HasAcceptedRepoType('git', 'hg')
198 def index(self):
198 def index(self):
199 source_repo = c.rhodecode_db_repo
199 source_repo = c.rhodecode_db_repo
200
200
201 try:
201 try:
202 source_repo.scm_instance().get_commit()
202 source_repo.scm_instance().get_commit()
203 except EmptyRepositoryError:
203 except EmptyRepositoryError:
204 h.flash(h.literal(_('There are no commits yet')),
204 h.flash(h.literal(_('There are no commits yet')),
205 category='warning')
205 category='warning')
206 redirect(url('summary_home', repo_name=source_repo.repo_name))
206 redirect(url('summary_home', repo_name=source_repo.repo_name))
207
207
208 commit_id = request.GET.get('commit')
208 commit_id = request.GET.get('commit')
209 branch_ref = request.GET.get('branch')
209 branch_ref = request.GET.get('branch')
210 bookmark_ref = request.GET.get('bookmark')
210 bookmark_ref = request.GET.get('bookmark')
211
211
212 try:
212 try:
213 source_repo_data = PullRequestModel().generate_repo_data(
213 source_repo_data = PullRequestModel().generate_repo_data(
214 source_repo, commit_id=commit_id,
214 source_repo, commit_id=commit_id,
215 branch=branch_ref, bookmark=bookmark_ref)
215 branch=branch_ref, bookmark=bookmark_ref)
216 except CommitDoesNotExistError as e:
216 except CommitDoesNotExistError as e:
217 log.exception(e)
217 log.exception(e)
218 h.flash(_('Commit does not exist'), 'error')
218 h.flash(_('Commit does not exist'), 'error')
219 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
219 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
220
220
221 default_target_repo = source_repo
221 default_target_repo = source_repo
222
222
223 if source_repo.parent:
223 if source_repo.parent:
224 parent_vcs_obj = source_repo.parent.scm_instance()
224 parent_vcs_obj = source_repo.parent.scm_instance()
225 if parent_vcs_obj and not parent_vcs_obj.is_empty():
225 if parent_vcs_obj and not parent_vcs_obj.is_empty():
226 # change default if we have a parent repo
226 # change default if we have a parent repo
227 default_target_repo = source_repo.parent
227 default_target_repo = source_repo.parent
228
228
229 target_repo_data = PullRequestModel().generate_repo_data(
229 target_repo_data = PullRequestModel().generate_repo_data(
230 default_target_repo)
230 default_target_repo)
231
231
232 selected_source_ref = source_repo_data['refs']['selected_ref']
232 selected_source_ref = source_repo_data['refs']['selected_ref']
233
233
234 title_source_ref = selected_source_ref.split(':', 2)[1]
234 title_source_ref = selected_source_ref.split(':', 2)[1]
235 c.default_title = PullRequestModel().generate_pullrequest_title(
235 c.default_title = PullRequestModel().generate_pullrequest_title(
236 source=source_repo.repo_name,
236 source=source_repo.repo_name,
237 source_ref=title_source_ref,
237 source_ref=title_source_ref,
238 target=default_target_repo.repo_name
238 target=default_target_repo.repo_name
239 )
239 )
240
240
241 c.default_repo_data = {
241 c.default_repo_data = {
242 'source_repo_name': source_repo.repo_name,
242 'source_repo_name': source_repo.repo_name,
243 'source_refs_json': json.dumps(source_repo_data),
243 'source_refs_json': json.dumps(source_repo_data),
244 'target_repo_name': default_target_repo.repo_name,
244 'target_repo_name': default_target_repo.repo_name,
245 'target_refs_json': json.dumps(target_repo_data),
245 'target_refs_json': json.dumps(target_repo_data),
246 }
246 }
247 c.default_source_ref = selected_source_ref
247 c.default_source_ref = selected_source_ref
248
248
249 return render('/pullrequests/pullrequest.mako')
249 return render('/pullrequests/pullrequest.mako')
250
250
251 @LoginRequired()
251 @LoginRequired()
252 @NotAnonymous()
252 @NotAnonymous()
253 @XHRRequired()
253 @XHRRequired()
254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 'repository.admin')
255 'repository.admin')
256 @jsonify
256 @jsonify
257 def get_repo_refs(self, repo_name, target_repo_name):
257 def get_repo_refs(self, repo_name, target_repo_name):
258 repo = Repository.get_by_repo_name(target_repo_name)
258 repo = Repository.get_by_repo_name(target_repo_name)
259 if not repo:
259 if not repo:
260 raise HTTPNotFound
260 raise HTTPNotFound
261 return PullRequestModel().generate_repo_data(repo)
261 return PullRequestModel().generate_repo_data(repo)
262
262
263 @LoginRequired()
263 @LoginRequired()
264 @NotAnonymous()
264 @NotAnonymous()
265 @XHRRequired()
265 @XHRRequired()
266 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
266 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
267 'repository.admin')
267 'repository.admin')
268 @jsonify
268 @jsonify
269 def get_repo_destinations(self, repo_name):
269 def get_repo_destinations(self, repo_name):
270 repo = Repository.get_by_repo_name(repo_name)
270 repo = Repository.get_by_repo_name(repo_name)
271 if not repo:
271 if not repo:
272 raise HTTPNotFound
272 raise HTTPNotFound
273 filter_query = request.GET.get('query')
273 filter_query = request.GET.get('query')
274
274
275 query = Repository.query() \
275 query = Repository.query() \
276 .order_by(func.length(Repository.repo_name)) \
276 .order_by(func.length(Repository.repo_name)) \
277 .filter(or_(
277 .filter(or_(
278 Repository.repo_name == repo.repo_name,
278 Repository.repo_name == repo.repo_name,
279 Repository.fork_id == repo.repo_id))
279 Repository.fork_id == repo.repo_id))
280
280
281 if filter_query:
281 if filter_query:
282 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
282 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
283 query = query.filter(
283 query = query.filter(
284 Repository.repo_name.ilike(ilike_expression))
284 Repository.repo_name.ilike(ilike_expression))
285
285
286 add_parent = False
286 add_parent = False
287 if repo.parent:
287 if repo.parent:
288 if filter_query in repo.parent.repo_name:
288 if filter_query in repo.parent.repo_name:
289 parent_vcs_obj = repo.parent.scm_instance()
289 parent_vcs_obj = repo.parent.scm_instance()
290 if parent_vcs_obj and not parent_vcs_obj.is_empty():
290 if parent_vcs_obj and not parent_vcs_obj.is_empty():
291 add_parent = True
291 add_parent = True
292
292
293 limit = 20 - 1 if add_parent else 20
293 limit = 20 - 1 if add_parent else 20
294 all_repos = query.limit(limit).all()
294 all_repos = query.limit(limit).all()
295 if add_parent:
295 if add_parent:
296 all_repos += [repo.parent]
296 all_repos += [repo.parent]
297
297
298 repos = []
298 repos = []
299 for obj in self.scm_model.get_repos(all_repos):
299 for obj in self.scm_model.get_repos(all_repos):
300 repos.append({
300 repos.append({
301 'id': obj['name'],
301 'id': obj['name'],
302 'text': obj['name'],
302 'text': obj['name'],
303 'type': 'repo',
303 'type': 'repo',
304 'obj': obj['dbrepo']
304 'obj': obj['dbrepo']
305 })
305 })
306
306
307 data = {
307 data = {
308 'more': False,
308 'more': False,
309 'results': [{
309 'results': [{
310 'text': _('Repositories'),
310 'text': _('Repositories'),
311 'children': repos
311 'children': repos
312 }] if repos else []
312 }] if repos else []
313 }
313 }
314 return data
314 return data
315
315
316 @LoginRequired()
316 @LoginRequired()
317 @NotAnonymous()
317 @NotAnonymous()
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 'repository.admin')
319 'repository.admin')
320 @HasAcceptedRepoType('git', 'hg')
320 @HasAcceptedRepoType('git', 'hg')
321 @auth.CSRFRequired()
321 @auth.CSRFRequired()
322 def create(self, repo_name):
322 def create(self, repo_name):
323 repo = Repository.get_by_repo_name(repo_name)
323 repo = Repository.get_by_repo_name(repo_name)
324 if not repo:
324 if not repo:
325 raise HTTPNotFound
325 raise HTTPNotFound
326
326
327 controls = peppercorn.parse(request.POST.items())
327 controls = peppercorn.parse(request.POST.items())
328
328
329 try:
329 try:
330 _form = PullRequestForm(repo.repo_id)().to_python(controls)
330 _form = PullRequestForm(repo.repo_id)().to_python(controls)
331 except formencode.Invalid as errors:
331 except formencode.Invalid as errors:
332 if errors.error_dict.get('revisions'):
332 if errors.error_dict.get('revisions'):
333 msg = 'Revisions: %s' % errors.error_dict['revisions']
333 msg = 'Revisions: %s' % errors.error_dict['revisions']
334 elif errors.error_dict.get('pullrequest_title'):
334 elif errors.error_dict.get('pullrequest_title'):
335 msg = _('Pull request requires a title with min. 3 chars')
335 msg = _('Pull request requires a title with min. 3 chars')
336 else:
336 else:
337 msg = _('Error creating pull request: {}').format(errors)
337 msg = _('Error creating pull request: {}').format(errors)
338 log.exception(msg)
338 log.exception(msg)
339 h.flash(msg, 'error')
339 h.flash(msg, 'error')
340
340
341 # would rather just go back to form ...
341 # would rather just go back to form ...
342 return redirect(url('pullrequest_home', repo_name=repo_name))
342 return redirect(url('pullrequest_home', repo_name=repo_name))
343
343
344 source_repo = _form['source_repo']
344 source_repo = _form['source_repo']
345 source_ref = _form['source_ref']
345 source_ref = _form['source_ref']
346 target_repo = _form['target_repo']
346 target_repo = _form['target_repo']
347 target_ref = _form['target_ref']
347 target_ref = _form['target_ref']
348 commit_ids = _form['revisions'][::-1]
348 commit_ids = _form['revisions'][::-1]
349 reviewers = [
349 reviewers = [
350 (r['user_id'], r['reasons']) for r in _form['review_members']]
350 (r['user_id'], r['reasons']) for r in _form['review_members']]
351
351
352 # find the ancestor for this pr
352 # find the ancestor for this pr
353 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
353 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
354 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
354 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
355
355
356 source_scm = source_db_repo.scm_instance()
356 source_scm = source_db_repo.scm_instance()
357 target_scm = target_db_repo.scm_instance()
357 target_scm = target_db_repo.scm_instance()
358
358
359 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
359 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
360 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
360 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
361
361
362 ancestor = source_scm.get_common_ancestor(
362 ancestor = source_scm.get_common_ancestor(
363 source_commit.raw_id, target_commit.raw_id, target_scm)
363 source_commit.raw_id, target_commit.raw_id, target_scm)
364
364
365 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
365 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
366 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
366 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
367
367
368 pullrequest_title = _form['pullrequest_title']
368 pullrequest_title = _form['pullrequest_title']
369 title_source_ref = source_ref.split(':', 2)[1]
369 title_source_ref = source_ref.split(':', 2)[1]
370 if not pullrequest_title:
370 if not pullrequest_title:
371 pullrequest_title = PullRequestModel().generate_pullrequest_title(
371 pullrequest_title = PullRequestModel().generate_pullrequest_title(
372 source=source_repo,
372 source=source_repo,
373 source_ref=title_source_ref,
373 source_ref=title_source_ref,
374 target=target_repo
374 target=target_repo
375 )
375 )
376
376
377 description = _form['pullrequest_desc']
377 description = _form['pullrequest_desc']
378 try:
378 try:
379 pull_request = PullRequestModel().create(
379 pull_request = PullRequestModel().create(
380 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
380 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
381 target_ref, commit_ids, reviewers, pullrequest_title,
381 target_ref, commit_ids, reviewers, pullrequest_title,
382 description
382 description
383 )
383 )
384 Session().commit()
384 Session().commit()
385 h.flash(_('Successfully opened new pull request'),
385 h.flash(_('Successfully opened new pull request'),
386 category='success')
386 category='success')
387 except Exception as e:
387 except Exception as e:
388 msg = _('Error occurred during sending pull request')
388 msg = _('Error occurred during sending pull request')
389 log.exception(msg)
389 log.exception(msg)
390 h.flash(msg, category='error')
390 h.flash(msg, category='error')
391 return redirect(url('pullrequest_home', repo_name=repo_name))
391 return redirect(url('pullrequest_home', repo_name=repo_name))
392
392
393 return redirect(url('pullrequest_show', repo_name=target_repo,
393 return redirect(url('pullrequest_show', repo_name=target_repo,
394 pull_request_id=pull_request.pull_request_id))
394 pull_request_id=pull_request.pull_request_id))
395
395
396 @LoginRequired()
396 @LoginRequired()
397 @NotAnonymous()
397 @NotAnonymous()
398 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
399 'repository.admin')
399 'repository.admin')
400 @auth.CSRFRequired()
400 @auth.CSRFRequired()
401 @jsonify
401 @jsonify
402 def update(self, repo_name, pull_request_id):
402 def update(self, repo_name, pull_request_id):
403 pull_request_id = safe_int(pull_request_id)
403 pull_request_id = safe_int(pull_request_id)
404 pull_request = PullRequest.get_or_404(pull_request_id)
404 pull_request = PullRequest.get_or_404(pull_request_id)
405 # only owner or admin can update it
405 # only owner or admin can update it
406 allowed_to_update = PullRequestModel().check_user_update(
406 allowed_to_update = PullRequestModel().check_user_update(
407 pull_request, c.rhodecode_user)
407 pull_request, c.rhodecode_user)
408 if allowed_to_update:
408 if allowed_to_update:
409 controls = peppercorn.parse(request.POST.items())
409 controls = peppercorn.parse(request.POST.items())
410
410
411 if 'review_members' in controls:
411 if 'review_members' in controls:
412 self._update_reviewers(
412 self._update_reviewers(
413 pull_request_id, controls['review_members'])
413 pull_request_id, controls['review_members'])
414 elif str2bool(request.POST.get('update_commits', 'false')):
414 elif str2bool(request.POST.get('update_commits', 'false')):
415 self._update_commits(pull_request)
415 self._update_commits(pull_request)
416 elif str2bool(request.POST.get('close_pull_request', 'false')):
416 elif str2bool(request.POST.get('close_pull_request', 'false')):
417 self._reject_close(pull_request)
417 self._reject_close(pull_request)
418 elif str2bool(request.POST.get('edit_pull_request', 'false')):
418 elif str2bool(request.POST.get('edit_pull_request', 'false')):
419 self._edit_pull_request(pull_request)
419 self._edit_pull_request(pull_request)
420 else:
420 else:
421 raise HTTPBadRequest()
421 raise HTTPBadRequest()
422 return True
422 return True
423 raise HTTPForbidden()
423 raise HTTPForbidden()
424
424
425 def _edit_pull_request(self, pull_request):
425 def _edit_pull_request(self, pull_request):
426 try:
426 try:
427 PullRequestModel().edit(
427 PullRequestModel().edit(
428 pull_request, request.POST.get('title'),
428 pull_request, request.POST.get('title'),
429 request.POST.get('description'))
429 request.POST.get('description'))
430 except ValueError:
430 except ValueError:
431 msg = _(u'Cannot update closed pull requests.')
431 msg = _(u'Cannot update closed pull requests.')
432 h.flash(msg, category='error')
432 h.flash(msg, category='error')
433 return
433 return
434 else:
434 else:
435 Session().commit()
435 Session().commit()
436
436
437 msg = _(u'Pull request title & description updated.')
437 msg = _(u'Pull request title & description updated.')
438 h.flash(msg, category='success')
438 h.flash(msg, category='success')
439 return
439 return
440
440
441 def _update_commits(self, pull_request):
441 def _update_commits(self, pull_request):
442 resp = PullRequestModel().update_commits(pull_request)
442 resp = PullRequestModel().update_commits(pull_request)
443
443
444 if resp.executed:
444 if resp.executed:
445
445
446 if resp.target_changed and resp.source_changed:
446 if resp.target_changed and resp.source_changed:
447 changed = 'target and source repositories'
447 changed = 'target and source repositories'
448 elif resp.target_changed and not resp.source_changed:
448 elif resp.target_changed and not resp.source_changed:
449 changed = 'target repository'
449 changed = 'target repository'
450 elif not resp.target_changed and resp.source_changed:
450 elif not resp.target_changed and resp.source_changed:
451 changed = 'source repository'
451 changed = 'source repository'
452 else:
452 else:
453 changed = 'nothing'
453 changed = 'nothing'
454
454
455 msg = _(
455 msg = _(
456 u'Pull request updated to "{source_commit_id}" with '
456 u'Pull request updated to "{source_commit_id}" with '
457 u'{count_added} added, {count_removed} removed commits. '
457 u'{count_added} added, {count_removed} removed commits. '
458 u'Source of changes: {change_source}')
458 u'Source of changes: {change_source}')
459 msg = msg.format(
459 msg = msg.format(
460 source_commit_id=pull_request.source_ref_parts.commit_id,
460 source_commit_id=pull_request.source_ref_parts.commit_id,
461 count_added=len(resp.changes.added),
461 count_added=len(resp.changes.added),
462 count_removed=len(resp.changes.removed),
462 count_removed=len(resp.changes.removed),
463 change_source=changed)
463 change_source=changed)
464 h.flash(msg, category='success')
464 h.flash(msg, category='success')
465
465
466 registry = get_current_registry()
466 registry = get_current_registry()
467 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
467 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
468 channelstream_config = rhodecode_plugins.get('channelstream', {})
468 channelstream_config = rhodecode_plugins.get('channelstream', {})
469 if channelstream_config.get('enabled'):
469 if channelstream_config.get('enabled'):
470 message = msg + (
470 message = msg + (
471 ' - <a onclick="window.location.reload()">'
471 ' - <a onclick="window.location.reload()">'
472 '<strong>{}</strong></a>'.format(_('Reload page')))
472 '<strong>{}</strong></a>'.format(_('Reload page')))
473 channel = '/repo${}$/pr/{}'.format(
473 channel = '/repo${}$/pr/{}'.format(
474 pull_request.target_repo.repo_name,
474 pull_request.target_repo.repo_name,
475 pull_request.pull_request_id
475 pull_request.pull_request_id
476 )
476 )
477 payload = {
477 payload = {
478 'type': 'message',
478 'type': 'message',
479 'user': 'system',
479 'user': 'system',
480 'exclude_users': [request.user.username],
480 'exclude_users': [request.user.username],
481 'channel': channel,
481 'channel': channel,
482 'message': {
482 'message': {
483 'message': message,
483 'message': message,
484 'level': 'success',
484 'level': 'success',
485 'topic': '/notifications'
485 'topic': '/notifications'
486 }
486 }
487 }
487 }
488 channelstream_request(
488 channelstream_request(
489 channelstream_config, [payload], '/message',
489 channelstream_config, [payload], '/message',
490 raise_exc=False)
490 raise_exc=False)
491 else:
491 else:
492 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
492 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
493 warning_reasons = [
493 warning_reasons = [
494 UpdateFailureReason.NO_CHANGE,
494 UpdateFailureReason.NO_CHANGE,
495 UpdateFailureReason.WRONG_REF_TYPE,
495 UpdateFailureReason.WRONG_REF_TYPE,
496 ]
496 ]
497 category = 'warning' if resp.reason in warning_reasons else 'error'
497 category = 'warning' if resp.reason in warning_reasons else 'error'
498 h.flash(msg, category=category)
498 h.flash(msg, category=category)
499
499
500 @auth.CSRFRequired()
500 @auth.CSRFRequired()
501 @LoginRequired()
501 @LoginRequired()
502 @NotAnonymous()
502 @NotAnonymous()
503 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
503 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
504 'repository.admin')
504 'repository.admin')
505 def merge(self, repo_name, pull_request_id):
505 def merge(self, repo_name, pull_request_id):
506 """
506 """
507 POST /{repo_name}/pull-request/{pull_request_id}
507 POST /{repo_name}/pull-request/{pull_request_id}
508
508
509 Merge will perform a server-side merge of the specified
509 Merge will perform a server-side merge of the specified
510 pull request, if the pull request is approved and mergeable.
510 pull request, if the pull request is approved and mergeable.
511 After successful merging, the pull request is automatically
511 After successful merging, the pull request is automatically
512 closed, with a relevant comment.
512 closed, with a relevant comment.
513 """
513 """
514 pull_request_id = safe_int(pull_request_id)
514 pull_request_id = safe_int(pull_request_id)
515 pull_request = PullRequest.get_or_404(pull_request_id)
515 pull_request = PullRequest.get_or_404(pull_request_id)
516 user = c.rhodecode_user
516 user = c.rhodecode_user
517
517
518 check = MergeCheck.validate(pull_request, user)
518 check = MergeCheck.validate(pull_request, user)
519 merge_possible = not check.failed
519 merge_possible = not check.failed
520
520
521 for err_type, error_msg in check.errors:
521 for err_type, error_msg in check.errors:
522 h.flash(error_msg, category=err_type)
522 h.flash(error_msg, category=err_type)
523
523
524 if merge_possible:
524 if merge_possible:
525 log.debug("Pre-conditions checked, trying to merge.")
525 log.debug("Pre-conditions checked, trying to merge.")
526 extras = vcs_operation_context(
526 extras = vcs_operation_context(
527 request.environ, repo_name=pull_request.target_repo.repo_name,
527 request.environ, repo_name=pull_request.target_repo.repo_name,
528 username=user.username, action='push',
528 username=user.username, action='push',
529 scm=pull_request.target_repo.repo_type)
529 scm=pull_request.target_repo.repo_type)
530 self._merge_pull_request(pull_request, user, extras)
530 self._merge_pull_request(pull_request, user, extras)
531
531
532 return redirect(url(
532 return redirect(url(
533 'pullrequest_show',
533 'pullrequest_show',
534 repo_name=pull_request.target_repo.repo_name,
534 repo_name=pull_request.target_repo.repo_name,
535 pull_request_id=pull_request.pull_request_id))
535 pull_request_id=pull_request.pull_request_id))
536
536
537 def _merge_pull_request(self, pull_request, user, extras):
537 def _merge_pull_request(self, pull_request, user, extras):
538 merge_resp = PullRequestModel().merge(
538 merge_resp = PullRequestModel().merge(
539 pull_request, user, extras=extras)
539 pull_request, user, extras=extras)
540
540
541 if merge_resp.executed:
541 if merge_resp.executed:
542 log.debug("The merge was successful, closing the pull request.")
542 log.debug("The merge was successful, closing the pull request.")
543 PullRequestModel().close_pull_request(
543 PullRequestModel().close_pull_request(
544 pull_request.pull_request_id, user)
544 pull_request.pull_request_id, user)
545 Session().commit()
545 Session().commit()
546 msg = _('Pull request was successfully merged and closed.')
546 msg = _('Pull request was successfully merged and closed.')
547 h.flash(msg, category='success')
547 h.flash(msg, category='success')
548 else:
548 else:
549 log.debug(
549 log.debug(
550 "The merge was not successful. Merge response: %s",
550 "The merge was not successful. Merge response: %s",
551 merge_resp)
551 merge_resp)
552 msg = PullRequestModel().merge_status_message(
552 msg = PullRequestModel().merge_status_message(
553 merge_resp.failure_reason)
553 merge_resp.failure_reason)
554 h.flash(msg, category='error')
554 h.flash(msg, category='error')
555
555
556 def _update_reviewers(self, pull_request_id, review_members):
556 def _update_reviewers(self, pull_request_id, review_members):
557 reviewers = [
557 reviewers = [
558 (int(r['user_id']), r['reasons']) for r in review_members]
558 (int(r['user_id']), r['reasons']) for r in review_members]
559 PullRequestModel().update_reviewers(pull_request_id, reviewers)
559 PullRequestModel().update_reviewers(pull_request_id, reviewers)
560 Session().commit()
560 Session().commit()
561
561
562 def _reject_close(self, pull_request):
562 def _reject_close(self, pull_request):
563 if pull_request.is_closed():
563 if pull_request.is_closed():
564 raise HTTPForbidden()
564 raise HTTPForbidden()
565
565
566 PullRequestModel().close_pull_request_with_comment(
566 PullRequestModel().close_pull_request_with_comment(
567 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
567 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
568 Session().commit()
568 Session().commit()
569
569
570 @LoginRequired()
570 @LoginRequired()
571 @NotAnonymous()
571 @NotAnonymous()
572 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
572 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
573 'repository.admin')
573 'repository.admin')
574 @auth.CSRFRequired()
574 @auth.CSRFRequired()
575 @jsonify
575 @jsonify
576 def delete(self, repo_name, pull_request_id):
576 def delete(self, repo_name, pull_request_id):
577 pull_request_id = safe_int(pull_request_id)
577 pull_request_id = safe_int(pull_request_id)
578 pull_request = PullRequest.get_or_404(pull_request_id)
578 pull_request = PullRequest.get_or_404(pull_request_id)
579
579
580 pr_closed = pull_request.is_closed()
580 pr_closed = pull_request.is_closed()
581 allowed_to_delete = PullRequestModel().check_user_delete(
581 allowed_to_delete = PullRequestModel().check_user_delete(
582 pull_request, c.rhodecode_user) and not pr_closed
582 pull_request, c.rhodecode_user) and not pr_closed
583
583
584 # only owner can delete it !
584 # only owner can delete it !
585 if allowed_to_delete:
585 if allowed_to_delete:
586 PullRequestModel().delete(pull_request)
586 PullRequestModel().delete(pull_request)
587 Session().commit()
587 Session().commit()
588 h.flash(_('Successfully deleted pull request'),
588 h.flash(_('Successfully deleted pull request'),
589 category='success')
589 category='success')
590 return redirect(url('my_account_pullrequests'))
590 return redirect(url('my_account_pullrequests'))
591
591
592 h.flash(_('Your are not allowed to delete this pull request'),
592 h.flash(_('Your are not allowed to delete this pull request'),
593 category='error')
593 category='error')
594 raise HTTPForbidden()
594 raise HTTPForbidden()
595
595
596 def _get_pr_version(self, pull_request_id, version=None):
596 def _get_pr_version(self, pull_request_id, version=None):
597 pull_request_id = safe_int(pull_request_id)
597 pull_request_id = safe_int(pull_request_id)
598 at_version = None
598 at_version = None
599
599
600 if version and version == 'latest':
600 if version and version == 'latest':
601 pull_request_ver = PullRequest.get(pull_request_id)
601 pull_request_ver = PullRequest.get(pull_request_id)
602 pull_request_obj = pull_request_ver
602 pull_request_obj = pull_request_ver
603 _org_pull_request_obj = pull_request_obj
603 _org_pull_request_obj = pull_request_obj
604 at_version = 'latest'
604 at_version = 'latest'
605 elif version:
605 elif version:
606 pull_request_ver = PullRequestVersion.get_or_404(version)
606 pull_request_ver = PullRequestVersion.get_or_404(version)
607 pull_request_obj = pull_request_ver
607 pull_request_obj = pull_request_ver
608 _org_pull_request_obj = pull_request_ver.pull_request
608 _org_pull_request_obj = pull_request_ver.pull_request
609 at_version = pull_request_ver.pull_request_version_id
609 at_version = pull_request_ver.pull_request_version_id
610 else:
610 else:
611 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
611 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
612
612
613 pull_request_display_obj = PullRequest.get_pr_display_object(
613 pull_request_display_obj = PullRequest.get_pr_display_object(
614 pull_request_obj, _org_pull_request_obj)
614 pull_request_obj, _org_pull_request_obj)
615
615
616 return _org_pull_request_obj, pull_request_obj, \
616 return _org_pull_request_obj, pull_request_obj, \
617 pull_request_display_obj, at_version
617 pull_request_display_obj, at_version
618
618
619 def _get_diffset(
619 def _get_diffset(
620 self, source_repo, source_ref_id, target_ref_id, target_commit,
620 self, source_repo, source_ref_id, target_ref_id, target_commit,
621 source_commit, diff_limit, file_limit, display_inline_comments):
621 source_commit, diff_limit, file_limit, display_inline_comments):
622 vcs_diff = PullRequestModel().get_diff(
622 vcs_diff = PullRequestModel().get_diff(
623 source_repo, source_ref_id, target_ref_id)
623 source_repo, source_ref_id, target_ref_id)
624
624
625 diff_processor = diffs.DiffProcessor(
625 diff_processor = diffs.DiffProcessor(
626 vcs_diff, format='newdiff', diff_limit=diff_limit,
626 vcs_diff, format='newdiff', diff_limit=diff_limit,
627 file_limit=file_limit, show_full_diff=c.fulldiff)
627 file_limit=file_limit, show_full_diff=c.fulldiff)
628
628
629 _parsed = diff_processor.prepare()
629 _parsed = diff_processor.prepare()
630
630
631 def _node_getter(commit):
631 def _node_getter(commit):
632 def get_node(fname):
632 def get_node(fname):
633 try:
633 try:
634 return commit.get_node(fname)
634 return commit.get_node(fname)
635 except NodeDoesNotExistError:
635 except NodeDoesNotExistError:
636 return None
636 return None
637
637
638 return get_node
638 return get_node
639
639
640 diffset = codeblocks.DiffSet(
640 diffset = codeblocks.DiffSet(
641 repo_name=c.repo_name,
641 repo_name=c.repo_name,
642 source_repo_name=c.source_repo.repo_name,
642 source_repo_name=c.source_repo.repo_name,
643 source_node_getter=_node_getter(target_commit),
643 source_node_getter=_node_getter(target_commit),
644 target_node_getter=_node_getter(source_commit),
644 target_node_getter=_node_getter(source_commit),
645 comments=display_inline_comments
645 comments=display_inline_comments
646 )
646 )
647 diffset = diffset.render_patchset(
647 diffset = diffset.render_patchset(
648 _parsed, target_commit.raw_id, source_commit.raw_id)
648 _parsed, target_commit.raw_id, source_commit.raw_id)
649
649
650 return diffset
650 return diffset
651
651
652 @LoginRequired()
652 @LoginRequired()
653 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
653 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
654 'repository.admin')
654 'repository.admin')
655 def show(self, repo_name, pull_request_id):
655 def show(self, repo_name, pull_request_id):
656 pull_request_id = safe_int(pull_request_id)
656 pull_request_id = safe_int(pull_request_id)
657 version = request.GET.get('version')
657 version = request.GET.get('version')
658 from_version = request.GET.get('from_version') or version
658 from_version = request.GET.get('from_version') or version
659 merge_checks = request.GET.get('merge_checks')
659 merge_checks = request.GET.get('merge_checks')
660 c.fulldiff = str2bool(request.GET.get('fulldiff'))
660 c.fulldiff = str2bool(request.GET.get('fulldiff'))
661
661
662 (pull_request_latest,
662 (pull_request_latest,
663 pull_request_at_ver,
663 pull_request_at_ver,
664 pull_request_display_obj,
664 pull_request_display_obj,
665 at_version) = self._get_pr_version(
665 at_version) = self._get_pr_version(
666 pull_request_id, version=version)
666 pull_request_id, version=version)
667 pr_closed = pull_request_latest.is_closed()
667 pr_closed = pull_request_latest.is_closed()
668
668
669 if pr_closed and (version or from_version):
669 if pr_closed and (version or from_version):
670 # not allow to browse versions
670 # not allow to browse versions
671 return redirect(h.url('pullrequest_show', repo_name=repo_name,
671 return redirect(h.url('pullrequest_show', repo_name=repo_name,
672 pull_request_id=pull_request_id))
672 pull_request_id=pull_request_id))
673
673
674 versions = pull_request_display_obj.versions()
674 versions = pull_request_display_obj.versions()
675
675
676 c.at_version = at_version
676 c.at_version = at_version
677 c.at_version_num = (at_version
677 c.at_version_num = (at_version
678 if at_version and at_version != 'latest'
678 if at_version and at_version != 'latest'
679 else None)
679 else None)
680 c.at_version_pos = ChangesetComment.get_index_from_version(
680 c.at_version_pos = ChangesetComment.get_index_from_version(
681 c.at_version_num, versions)
681 c.at_version_num, versions)
682
682
683 (prev_pull_request_latest,
683 (prev_pull_request_latest,
684 prev_pull_request_at_ver,
684 prev_pull_request_at_ver,
685 prev_pull_request_display_obj,
685 prev_pull_request_display_obj,
686 prev_at_version) = self._get_pr_version(
686 prev_at_version) = self._get_pr_version(
687 pull_request_id, version=from_version)
687 pull_request_id, version=from_version)
688
688
689 c.from_version = prev_at_version
689 c.from_version = prev_at_version
690 c.from_version_num = (prev_at_version
690 c.from_version_num = (prev_at_version
691 if prev_at_version and prev_at_version != 'latest'
691 if prev_at_version and prev_at_version != 'latest'
692 else None)
692 else None)
693 c.from_version_pos = ChangesetComment.get_index_from_version(
693 c.from_version_pos = ChangesetComment.get_index_from_version(
694 c.from_version_num, versions)
694 c.from_version_num, versions)
695
695
696 # define if we're in COMPARE mode or VIEW at version mode
696 # define if we're in COMPARE mode or VIEW at version mode
697 compare = at_version != prev_at_version
697 compare = at_version != prev_at_version
698
698
699 # pull_requests repo_name we opened it against
699 # pull_requests repo_name we opened it against
700 # ie. target_repo must match
700 # ie. target_repo must match
701 if repo_name != pull_request_at_ver.target_repo.repo_name:
701 if repo_name != pull_request_at_ver.target_repo.repo_name:
702 raise HTTPNotFound
702 raise HTTPNotFound
703
703
704 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
704 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
705 pull_request_at_ver)
705 pull_request_at_ver)
706
706
707 c.pull_request = pull_request_display_obj
707 c.pull_request = pull_request_display_obj
708 c.pull_request_latest = pull_request_latest
708 c.pull_request_latest = pull_request_latest
709
709
710 if compare or (at_version and not at_version == 'latest'):
710 if compare or (at_version and not at_version == 'latest'):
711 c.allowed_to_change_status = False
711 c.allowed_to_change_status = False
712 c.allowed_to_update = False
712 c.allowed_to_update = False
713 c.allowed_to_merge = False
713 c.allowed_to_merge = False
714 c.allowed_to_delete = False
714 c.allowed_to_delete = False
715 c.allowed_to_comment = False
715 c.allowed_to_comment = False
716 c.allowed_to_close = False
716 c.allowed_to_close = False
717 else:
717 else:
718 c.allowed_to_change_status = PullRequestModel(). \
718 c.allowed_to_change_status = PullRequestModel(). \
719 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
719 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
720 and not pr_closed
720 and not pr_closed
721
721
722 c.allowed_to_update = PullRequestModel().check_user_update(
722 c.allowed_to_update = PullRequestModel().check_user_update(
723 pull_request_latest, c.rhodecode_user) and not pr_closed
723 pull_request_latest, c.rhodecode_user) and not pr_closed
724 c.allowed_to_merge = PullRequestModel().check_user_merge(
724 c.allowed_to_merge = PullRequestModel().check_user_merge(
725 pull_request_latest, c.rhodecode_user) and not pr_closed
725 pull_request_latest, c.rhodecode_user) and not pr_closed
726 c.allowed_to_delete = PullRequestModel().check_user_delete(
726 c.allowed_to_delete = PullRequestModel().check_user_delete(
727 pull_request_latest, c.rhodecode_user) and not pr_closed
727 pull_request_latest, c.rhodecode_user) and not pr_closed
728 c.allowed_to_comment = not pr_closed
728 c.allowed_to_comment = not pr_closed
729 c.allowed_to_close = c.allowed_to_merge and not pr_closed
729 c.allowed_to_close = c.allowed_to_merge and not pr_closed
730
730
731 # check merge capabilities
731 # check merge capabilities
732 _merge_check = MergeCheck.validate(
732 _merge_check = MergeCheck.validate(
733 pull_request_latest, user=c.rhodecode_user)
733 pull_request_latest, user=c.rhodecode_user)
734 c.pr_merge_errors = _merge_check.error_details
734 c.pr_merge_errors = _merge_check.error_details
735 c.pr_merge_possible = not _merge_check.failed
735 c.pr_merge_possible = not _merge_check.failed
736 c.pr_merge_message = _merge_check.merge_msg
736 c.pr_merge_message = _merge_check.merge_msg
737
737
738 c.pull_request_review_status = _merge_check.review_status
738 c.pull_request_review_status = _merge_check.review_status
739 if merge_checks:
739 if merge_checks:
740 return render('/pullrequests/pullrequest_merge_checks.mako')
740 return render('/pullrequests/pullrequest_merge_checks.mako')
741
741
742 comments_model = CommentsModel()
742 comments_model = CommentsModel()
743
743
744 # reviewers and statuses
744 # reviewers and statuses
745 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
745 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
746 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
746 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
747
747
748 # GENERAL COMMENTS with versions #
748 # GENERAL COMMENTS with versions #
749 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
749 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
750 q = q.order_by(ChangesetComment.comment_id.asc())
750 q = q.order_by(ChangesetComment.comment_id.asc())
751 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
751 general_comments = q
752
752
753 # pick comments we want to render at current version
753 # pick comments we want to render at current version
754 c.comment_versions = comments_model.aggregate_comments(
754 c.comment_versions = comments_model.aggregate_comments(
755 general_comments, versions, c.at_version_num)
755 general_comments, versions, c.at_version_num)
756 c.comments = c.comment_versions[c.at_version_num]['until']
756 c.comments = c.comment_versions[c.at_version_num]['until']
757
757
758 # INLINE COMMENTS with versions #
758 # INLINE COMMENTS with versions #
759 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
759 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
760 q = q.order_by(ChangesetComment.comment_id.asc())
760 q = q.order_by(ChangesetComment.comment_id.asc())
761 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
761 inline_comments = q
762
762 c.inline_versions = comments_model.aggregate_comments(
763 c.inline_versions = comments_model.aggregate_comments(
763 inline_comments, versions, c.at_version_num, inline=True)
764 inline_comments, versions, c.at_version_num, inline=True)
764
765
765 # inject latest version
766 # inject latest version
766 latest_ver = PullRequest.get_pr_display_object(
767 latest_ver = PullRequest.get_pr_display_object(
767 pull_request_latest, pull_request_latest)
768 pull_request_latest, pull_request_latest)
768
769
769 c.versions = versions + [latest_ver]
770 c.versions = versions + [latest_ver]
770
771
771 # if we use version, then do not show later comments
772 # if we use version, then do not show later comments
772 # than current version
773 # than current version
773 display_inline_comments = collections.defaultdict(
774 display_inline_comments = collections.defaultdict(
774 lambda: collections.defaultdict(list))
775 lambda: collections.defaultdict(list))
775 for co in inline_comments:
776 for co in inline_comments:
776 if c.at_version_num:
777 if c.at_version_num:
777 # pick comments that are at least UPTO given version, so we
778 # pick comments that are at least UPTO given version, so we
778 # don't render comments for higher version
779 # don't render comments for higher version
779 should_render = co.pull_request_version_id and \
780 should_render = co.pull_request_version_id and \
780 co.pull_request_version_id <= c.at_version_num
781 co.pull_request_version_id <= c.at_version_num
781 else:
782 else:
782 # showing all, for 'latest'
783 # showing all, for 'latest'
783 should_render = True
784 should_render = True
784
785
785 if should_render:
786 if should_render:
786 display_inline_comments[co.f_path][co.line_no].append(co)
787 display_inline_comments[co.f_path][co.line_no].append(co)
787
788
788 # load diff data into template context, if we use compare mode then
789 # load diff data into template context, if we use compare mode then
789 # diff is calculated based on changes between versions of PR
790 # diff is calculated based on changes between versions of PR
790
791
791 source_repo = pull_request_at_ver.source_repo
792 source_repo = pull_request_at_ver.source_repo
792 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
793 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
793
794
794 target_repo = pull_request_at_ver.target_repo
795 target_repo = pull_request_at_ver.target_repo
795 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
796 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
796
797
797 if compare:
798 if compare:
798 # in compare switch the diff base to latest commit from prev version
799 # in compare switch the diff base to latest commit from prev version
799 target_ref_id = prev_pull_request_display_obj.revisions[0]
800 target_ref_id = prev_pull_request_display_obj.revisions[0]
800
801
801 # despite opening commits for bookmarks/branches/tags, we always
802 # despite opening commits for bookmarks/branches/tags, we always
802 # convert this to rev to prevent changes after bookmark or branch change
803 # convert this to rev to prevent changes after bookmark or branch change
803 c.source_ref_type = 'rev'
804 c.source_ref_type = 'rev'
804 c.source_ref = source_ref_id
805 c.source_ref = source_ref_id
805
806
806 c.target_ref_type = 'rev'
807 c.target_ref_type = 'rev'
807 c.target_ref = target_ref_id
808 c.target_ref = target_ref_id
808
809
809 c.source_repo = source_repo
810 c.source_repo = source_repo
810 c.target_repo = target_repo
811 c.target_repo = target_repo
811
812
812 # diff_limit is the old behavior, will cut off the whole diff
813 # diff_limit is the old behavior, will cut off the whole diff
813 # if the limit is applied otherwise will just hide the
814 # if the limit is applied otherwise will just hide the
814 # big files from the front-end
815 # big files from the front-end
815 diff_limit = self.cut_off_limit_diff
816 diff_limit = self.cut_off_limit_diff
816 file_limit = self.cut_off_limit_file
817 file_limit = self.cut_off_limit_file
817
818
818 c.commit_ranges = []
819 c.commit_ranges = []
819 source_commit = EmptyCommit()
820 source_commit = EmptyCommit()
820 target_commit = EmptyCommit()
821 target_commit = EmptyCommit()
821 c.missing_requirements = False
822 c.missing_requirements = False
822
823
823 source_scm = source_repo.scm_instance()
824 source_scm = source_repo.scm_instance()
824 target_scm = target_repo.scm_instance()
825 target_scm = target_repo.scm_instance()
825
826
826 # try first shadow repo, fallback to regular repo
827 # try first shadow repo, fallback to regular repo
827 try:
828 try:
828 commits_source_repo = pull_request_latest.get_shadow_repo()
829 commits_source_repo = pull_request_latest.get_shadow_repo()
829 except Exception:
830 except Exception:
830 log.debug('Failed to get shadow repo', exc_info=True)
831 log.debug('Failed to get shadow repo', exc_info=True)
831 commits_source_repo = source_scm
832 commits_source_repo = source_scm
832
833
833 c.commits_source_repo = commits_source_repo
834 c.commits_source_repo = commits_source_repo
834 commit_cache = {}
835 commit_cache = {}
835 try:
836 try:
836 pre_load = ["author", "branch", "date", "message"]
837 pre_load = ["author", "branch", "date", "message"]
837 show_revs = pull_request_at_ver.revisions
838 show_revs = pull_request_at_ver.revisions
838 for rev in show_revs:
839 for rev in show_revs:
839 comm = commits_source_repo.get_commit(
840 comm = commits_source_repo.get_commit(
840 commit_id=rev, pre_load=pre_load)
841 commit_id=rev, pre_load=pre_load)
841 c.commit_ranges.append(comm)
842 c.commit_ranges.append(comm)
842 commit_cache[comm.raw_id] = comm
843 commit_cache[comm.raw_id] = comm
843
844
844 target_commit = commits_source_repo.get_commit(
845 target_commit = commits_source_repo.get_commit(
845 commit_id=safe_str(target_ref_id))
846 commit_id=safe_str(target_ref_id))
846 source_commit = commits_source_repo.get_commit(
847 source_commit = commits_source_repo.get_commit(
847 commit_id=safe_str(source_ref_id))
848 commit_id=safe_str(source_ref_id))
848 except CommitDoesNotExistError:
849 except CommitDoesNotExistError:
849 pass
850 pass
850 except RepositoryRequirementError:
851 except RepositoryRequirementError:
851 log.warning(
852 log.warning(
852 'Failed to get all required data from repo', exc_info=True)
853 'Failed to get all required data from repo', exc_info=True)
853 c.missing_requirements = True
854 c.missing_requirements = True
854
855
855 c.ancestor = None # set it to None, to hide it from PR view
856 c.ancestor = None # set it to None, to hide it from PR view
856
857
857 try:
858 try:
858 ancestor_id = source_scm.get_common_ancestor(
859 ancestor_id = source_scm.get_common_ancestor(
859 source_commit.raw_id, target_commit.raw_id, target_scm)
860 source_commit.raw_id, target_commit.raw_id, target_scm)
860 c.ancestor_commit = source_scm.get_commit(ancestor_id)
861 c.ancestor_commit = source_scm.get_commit(ancestor_id)
861 except Exception:
862 except Exception:
862 c.ancestor_commit = None
863 c.ancestor_commit = None
863
864
864 c.statuses = source_repo.statuses(
865 c.statuses = source_repo.statuses(
865 [x.raw_id for x in c.commit_ranges])
866 [x.raw_id for x in c.commit_ranges])
866
867
867 # auto collapse if we have more than limit
868 # auto collapse if we have more than limit
868 collapse_limit = diffs.DiffProcessor._collapse_commits_over
869 collapse_limit = diffs.DiffProcessor._collapse_commits_over
869 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
870 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
870 c.compare_mode = compare
871 c.compare_mode = compare
871
872
872 c.missing_commits = False
873 c.missing_commits = False
873 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
874 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
874 or source_commit == target_commit):
875 or source_commit == target_commit):
875
876
876 c.missing_commits = True
877 c.missing_commits = True
877 else:
878 else:
878
879
879 c.diffset = self._get_diffset(
880 c.diffset = self._get_diffset(
880 commits_source_repo, source_ref_id, target_ref_id,
881 commits_source_repo, source_ref_id, target_ref_id,
881 target_commit, source_commit,
882 target_commit, source_commit,
882 diff_limit, file_limit, display_inline_comments)
883 diff_limit, file_limit, display_inline_comments)
883
884
884 c.limited_diff = c.diffset.limited_diff
885 c.limited_diff = c.diffset.limited_diff
885
886
886 # calculate removed files that are bound to comments
887 # calculate removed files that are bound to comments
887 comment_deleted_files = [
888 comment_deleted_files = [
888 fname for fname in display_inline_comments
889 fname for fname in display_inline_comments
889 if fname not in c.diffset.file_stats]
890 if fname not in c.diffset.file_stats]
890
891
891 c.deleted_files_comments = collections.defaultdict(dict)
892 c.deleted_files_comments = collections.defaultdict(dict)
892 for fname, per_line_comments in display_inline_comments.items():
893 for fname, per_line_comments in display_inline_comments.items():
893 if fname in comment_deleted_files:
894 if fname in comment_deleted_files:
894 c.deleted_files_comments[fname]['stats'] = 0
895 c.deleted_files_comments[fname]['stats'] = 0
895 c.deleted_files_comments[fname]['comments'] = list()
896 c.deleted_files_comments[fname]['comments'] = list()
896 for lno, comments in per_line_comments.items():
897 for lno, comments in per_line_comments.items():
897 c.deleted_files_comments[fname]['comments'].extend(
898 c.deleted_files_comments[fname]['comments'].extend(
898 comments)
899 comments)
899
900
900 # this is a hack to properly display links, when creating PR, the
901 # this is a hack to properly display links, when creating PR, the
901 # compare view and others uses different notation, and
902 # compare view and others uses different notation, and
902 # compare_commits.mako renders links based on the target_repo.
903 # compare_commits.mako renders links based on the target_repo.
903 # We need to swap that here to generate it properly on the html side
904 # We need to swap that here to generate it properly on the html side
904 c.target_repo = c.source_repo
905 c.target_repo = c.source_repo
905
906
906 c.commit_statuses = ChangesetStatus.STATUSES
907 c.commit_statuses = ChangesetStatus.STATUSES
907
908
908 c.show_version_changes = not pr_closed
909 c.show_version_changes = not pr_closed
909 if c.show_version_changes:
910 if c.show_version_changes:
910 cur_obj = pull_request_at_ver
911 cur_obj = pull_request_at_ver
911 prev_obj = prev_pull_request_at_ver
912 prev_obj = prev_pull_request_at_ver
912
913
913 old_commit_ids = prev_obj.revisions
914 old_commit_ids = prev_obj.revisions
914 new_commit_ids = cur_obj.revisions
915 new_commit_ids = cur_obj.revisions
915 commit_changes = PullRequestModel()._calculate_commit_id_changes(
916 commit_changes = PullRequestModel()._calculate_commit_id_changes(
916 old_commit_ids, new_commit_ids)
917 old_commit_ids, new_commit_ids)
917 c.commit_changes_summary = commit_changes
918 c.commit_changes_summary = commit_changes
918
919
919 # calculate the diff for commits between versions
920 # calculate the diff for commits between versions
920 c.commit_changes = []
921 c.commit_changes = []
921 mark = lambda cs, fw: list(
922 mark = lambda cs, fw: list(
922 h.itertools.izip_longest([], cs, fillvalue=fw))
923 h.itertools.izip_longest([], cs, fillvalue=fw))
923 for c_type, raw_id in mark(commit_changes.added, 'a') \
924 for c_type, raw_id in mark(commit_changes.added, 'a') \
924 + mark(commit_changes.removed, 'r') \
925 + mark(commit_changes.removed, 'r') \
925 + mark(commit_changes.common, 'c'):
926 + mark(commit_changes.common, 'c'):
926
927
927 if raw_id in commit_cache:
928 if raw_id in commit_cache:
928 commit = commit_cache[raw_id]
929 commit = commit_cache[raw_id]
929 else:
930 else:
930 try:
931 try:
931 commit = commits_source_repo.get_commit(raw_id)
932 commit = commits_source_repo.get_commit(raw_id)
932 except CommitDoesNotExistError:
933 except CommitDoesNotExistError:
933 # in case we fail extracting still use "dummy" commit
934 # in case we fail extracting still use "dummy" commit
934 # for display in commit diff
935 # for display in commit diff
935 commit = h.AttributeDict(
936 commit = h.AttributeDict(
936 {'raw_id': raw_id,
937 {'raw_id': raw_id,
937 'message': 'EMPTY or MISSING COMMIT'})
938 'message': 'EMPTY or MISSING COMMIT'})
938 c.commit_changes.append([c_type, commit])
939 c.commit_changes.append([c_type, commit])
939
940
940 # current user review statuses for each version
941 # current user review statuses for each version
941 c.review_versions = {}
942 c.review_versions = {}
942 if c.rhodecode_user.user_id in allowed_reviewers:
943 if c.rhodecode_user.user_id in allowed_reviewers:
943 for co in general_comments:
944 for co in general_comments:
944 if co.author.user_id == c.rhodecode_user.user_id:
945 if co.author.user_id == c.rhodecode_user.user_id:
945 # each comment has a status change
946 # each comment has a status change
946 status = co.status_change
947 status = co.status_change
947 if status:
948 if status:
948 _ver_pr = status[0].comment.pull_request_version_id
949 _ver_pr = status[0].comment.pull_request_version_id
949 c.review_versions[_ver_pr] = status[0]
950 c.review_versions[_ver_pr] = status[0]
950
951
951 return render('/pullrequests/pullrequest_show.mako')
952 return render('/pullrequests/pullrequest_show.mako')
952
953
953 @LoginRequired()
954 @LoginRequired()
954 @NotAnonymous()
955 @NotAnonymous()
955 @HasRepoPermissionAnyDecorator(
956 @HasRepoPermissionAnyDecorator(
956 'repository.read', 'repository.write', 'repository.admin')
957 'repository.read', 'repository.write', 'repository.admin')
957 @auth.CSRFRequired()
958 @auth.CSRFRequired()
958 @jsonify
959 @jsonify
959 def comment(self, repo_name, pull_request_id):
960 def comment(self, repo_name, pull_request_id):
960 pull_request_id = safe_int(pull_request_id)
961 pull_request_id = safe_int(pull_request_id)
961 pull_request = PullRequest.get_or_404(pull_request_id)
962 pull_request = PullRequest.get_or_404(pull_request_id)
962 if pull_request.is_closed():
963 if pull_request.is_closed():
963 raise HTTPForbidden()
964 raise HTTPForbidden()
964
965
965 status = request.POST.get('changeset_status', None)
966 status = request.POST.get('changeset_status', None)
966 text = request.POST.get('text')
967 text = request.POST.get('text')
967 comment_type = request.POST.get('comment_type')
968 comment_type = request.POST.get('comment_type')
968 resolves_comment_id = request.POST.get('resolves_comment_id', None)
969 resolves_comment_id = request.POST.get('resolves_comment_id', None)
969 close_pull_request = request.POST.get('close_pull_request')
970 close_pull_request = request.POST.get('close_pull_request')
970
971
971 close_pr = False
972 close_pr = False
972 # only owner or admin or person with write permissions
973 # only owner or admin or person with write permissions
973 allowed_to_close = PullRequestModel().check_user_update(
974 allowed_to_close = PullRequestModel().check_user_update(
974 pull_request, c.rhodecode_user)
975 pull_request, c.rhodecode_user)
975
976
976 if close_pull_request and allowed_to_close:
977 if close_pull_request and allowed_to_close:
977 close_pr = True
978 close_pr = True
978 pull_request_review_status = pull_request.calculated_review_status()
979 pull_request_review_status = pull_request.calculated_review_status()
979 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
980 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
980 # approved only if we have voting consent
981 # approved only if we have voting consent
981 status = ChangesetStatus.STATUS_APPROVED
982 status = ChangesetStatus.STATUS_APPROVED
982 else:
983 else:
983 status = ChangesetStatus.STATUS_REJECTED
984 status = ChangesetStatus.STATUS_REJECTED
984
985
985 allowed_to_change_status = PullRequestModel().check_user_change_status(
986 allowed_to_change_status = PullRequestModel().check_user_change_status(
986 pull_request, c.rhodecode_user)
987 pull_request, c.rhodecode_user)
987
988
988 if status and allowed_to_change_status:
989 if status and allowed_to_change_status:
989 message = (_('Status change %(transition_icon)s %(status)s')
990 message = (_('Status change %(transition_icon)s %(status)s')
990 % {'transition_icon': '>',
991 % {'transition_icon': '>',
991 'status': ChangesetStatus.get_status_lbl(status)})
992 'status': ChangesetStatus.get_status_lbl(status)})
992 if close_pr:
993 if close_pr:
993 message = _('Closing with') + ' ' + message
994 message = _('Closing with') + ' ' + message
994 text = text or message
995 text = text or message
995 comm = CommentsModel().create(
996 comm = CommentsModel().create(
996 text=text,
997 text=text,
997 repo=c.rhodecode_db_repo.repo_id,
998 repo=c.rhodecode_db_repo.repo_id,
998 user=c.rhodecode_user.user_id,
999 user=c.rhodecode_user.user_id,
999 pull_request=pull_request_id,
1000 pull_request=pull_request_id,
1000 f_path=request.POST.get('f_path'),
1001 f_path=request.POST.get('f_path'),
1001 line_no=request.POST.get('line'),
1002 line_no=request.POST.get('line'),
1002 status_change=(ChangesetStatus.get_status_lbl(status)
1003 status_change=(ChangesetStatus.get_status_lbl(status)
1003 if status and allowed_to_change_status else None),
1004 if status and allowed_to_change_status else None),
1004 status_change_type=(status
1005 status_change_type=(status
1005 if status and allowed_to_change_status else None),
1006 if status and allowed_to_change_status else None),
1006 closing_pr=close_pr,
1007 closing_pr=close_pr,
1007 comment_type=comment_type,
1008 comment_type=comment_type,
1008 resolves_comment_id=resolves_comment_id
1009 resolves_comment_id=resolves_comment_id
1009 )
1010 )
1010
1011
1011 if allowed_to_change_status:
1012 if allowed_to_change_status:
1012 old_calculated_status = pull_request.calculated_review_status()
1013 old_calculated_status = pull_request.calculated_review_status()
1013 # get status if set !
1014 # get status if set !
1014 if status:
1015 if status:
1015 ChangesetStatusModel().set_status(
1016 ChangesetStatusModel().set_status(
1016 c.rhodecode_db_repo.repo_id,
1017 c.rhodecode_db_repo.repo_id,
1017 status,
1018 status,
1018 c.rhodecode_user.user_id,
1019 c.rhodecode_user.user_id,
1019 comm,
1020 comm,
1020 pull_request=pull_request_id
1021 pull_request=pull_request_id
1021 )
1022 )
1022
1023
1023 Session().flush()
1024 Session().flush()
1024 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1025 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1025 # we now calculate the status of pull request, and based on that
1026 # we now calculate the status of pull request, and based on that
1026 # calculation we set the commits status
1027 # calculation we set the commits status
1027 calculated_status = pull_request.calculated_review_status()
1028 calculated_status = pull_request.calculated_review_status()
1028 if old_calculated_status != calculated_status:
1029 if old_calculated_status != calculated_status:
1029 PullRequestModel()._trigger_pull_request_hook(
1030 PullRequestModel()._trigger_pull_request_hook(
1030 pull_request, c.rhodecode_user, 'review_status_change')
1031 pull_request, c.rhodecode_user, 'review_status_change')
1031
1032
1032 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1033 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1033 calculated_status)
1034 calculated_status)
1034
1035
1035 if close_pr:
1036 if close_pr:
1036 status_completed = (
1037 status_completed = (
1037 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1038 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1038 ChangesetStatus.STATUS_REJECTED])
1039 ChangesetStatus.STATUS_REJECTED])
1039 if close_pull_request or status_completed:
1040 if close_pull_request or status_completed:
1040 PullRequestModel().close_pull_request(
1041 PullRequestModel().close_pull_request(
1041 pull_request_id, c.rhodecode_user)
1042 pull_request_id, c.rhodecode_user)
1042 else:
1043 else:
1043 h.flash(_('Closing pull request on other statuses than '
1044 h.flash(_('Closing pull request on other statuses than '
1044 'rejected or approved is forbidden. '
1045 'rejected or approved is forbidden. '
1045 'Calculated status from all reviewers '
1046 'Calculated status from all reviewers '
1046 'is currently: %s') % calculated_status_lbl,
1047 'is currently: %s') % calculated_status_lbl,
1047 category='warning')
1048 category='warning')
1048
1049
1049 Session().commit()
1050 Session().commit()
1050
1051
1051 if not request.is_xhr:
1052 if not request.is_xhr:
1052 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1053 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1053 pull_request_id=pull_request_id))
1054 pull_request_id=pull_request_id))
1054
1055
1055 data = {
1056 data = {
1056 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1057 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1057 }
1058 }
1058 if comm:
1059 if comm:
1059 c.co = comm
1060 c.co = comm
1060 c.inline_comment = True if comm.line_no else False
1061 c.inline_comment = True if comm.line_no else False
1061 data.update(comm.get_dict())
1062 data.update(comm.get_dict())
1062 data.update({'rendered_text':
1063 data.update({'rendered_text':
1063 render('changeset/changeset_comment_block.mako')})
1064 render('changeset/changeset_comment_block.mako')})
1064
1065
1065 return data
1066 return data
1066
1067
1067 @LoginRequired()
1068 @LoginRequired()
1068 @NotAnonymous()
1069 @NotAnonymous()
1069 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1070 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1070 'repository.admin')
1071 'repository.admin')
1071 @auth.CSRFRequired()
1072 @auth.CSRFRequired()
1072 @jsonify
1073 @jsonify
1073 def delete_comment(self, repo_name, comment_id):
1074 def delete_comment(self, repo_name, comment_id):
1074 return self._delete_comment(comment_id)
1075 return self._delete_comment(comment_id)
1075
1076
1076 def _delete_comment(self, comment_id):
1077 def _delete_comment(self, comment_id):
1077 comment_id = safe_int(comment_id)
1078 comment_id = safe_int(comment_id)
1078 co = ChangesetComment.get_or_404(comment_id)
1079 co = ChangesetComment.get_or_404(comment_id)
1079 if co.pull_request.is_closed():
1080 if co.pull_request.is_closed():
1080 # don't allow deleting comments on closed pull request
1081 # don't allow deleting comments on closed pull request
1081 raise HTTPForbidden()
1082 raise HTTPForbidden()
1082
1083
1083 is_owner = co.author.user_id == c.rhodecode_user.user_id
1084 is_owner = co.author.user_id == c.rhodecode_user.user_id
1084 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1085 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1085 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1086 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1086 old_calculated_status = co.pull_request.calculated_review_status()
1087 old_calculated_status = co.pull_request.calculated_review_status()
1087 CommentsModel().delete(comment=co)
1088 CommentsModel().delete(comment=co)
1088 Session().commit()
1089 Session().commit()
1089 calculated_status = co.pull_request.calculated_review_status()
1090 calculated_status = co.pull_request.calculated_review_status()
1090 if old_calculated_status != calculated_status:
1091 if old_calculated_status != calculated_status:
1091 PullRequestModel()._trigger_pull_request_hook(
1092 PullRequestModel()._trigger_pull_request_hook(
1092 co.pull_request, c.rhodecode_user, 'review_status_change')
1093 co.pull_request, c.rhodecode_user, 'review_status_change')
1093 return True
1094 return True
1094 else:
1095 else:
1095 raise HTTPForbidden()
1096 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now