##// END OF EJS Templates
pull-reqests: workflow, change who can close a PR, it's only super-admin...
marcink -
r1686:f0d2d511 default
parent child Browse files
Show More
@@ -1,1091 +1,1095 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_TPYE,
495 UpdateFailureReason.WRONG_REF_TPYE,
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_change_status 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.order_by(ChangesetComment.pull_request_version_id.asc())
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.order_by(ChangesetComment.pull_request_version_id.asc())
762 c.inline_versions = comments_model.aggregate_comments(
762 c.inline_versions = comments_model.aggregate_comments(
763 inline_comments, versions, c.at_version_num, inline=True)
763 inline_comments, versions, c.at_version_num, inline=True)
764
764
765 # inject latest version
765 # inject latest version
766 latest_ver = PullRequest.get_pr_display_object(
766 latest_ver = PullRequest.get_pr_display_object(
767 pull_request_latest, pull_request_latest)
767 pull_request_latest, pull_request_latest)
768
768
769 c.versions = versions + [latest_ver]
769 c.versions = versions + [latest_ver]
770
770
771 # if we use version, then do not show later comments
771 # if we use version, then do not show later comments
772 # than current version
772 # than current version
773 display_inline_comments = collections.defaultdict(
773 display_inline_comments = collections.defaultdict(
774 lambda: collections.defaultdict(list))
774 lambda: collections.defaultdict(list))
775 for co in inline_comments:
775 for co in inline_comments:
776 if c.at_version_num:
776 if c.at_version_num:
777 # pick comments that are at least UPTO given version, so we
777 # pick comments that are at least UPTO given version, so we
778 # don't render comments for higher version
778 # don't render comments for higher version
779 should_render = co.pull_request_version_id and \
779 should_render = co.pull_request_version_id and \
780 co.pull_request_version_id <= c.at_version_num
780 co.pull_request_version_id <= c.at_version_num
781 else:
781 else:
782 # showing all, for 'latest'
782 # showing all, for 'latest'
783 should_render = True
783 should_render = True
784
784
785 if should_render:
785 if should_render:
786 display_inline_comments[co.f_path][co.line_no].append(co)
786 display_inline_comments[co.f_path][co.line_no].append(co)
787
787
788 # load diff data into template context, if we use compare mode then
788 # load diff data into template context, if we use compare mode then
789 # diff is calculated based on changes between versions of PR
789 # diff is calculated based on changes between versions of PR
790
790
791 source_repo = pull_request_at_ver.source_repo
791 source_repo = pull_request_at_ver.source_repo
792 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
792 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
793
793
794 target_repo = pull_request_at_ver.target_repo
794 target_repo = pull_request_at_ver.target_repo
795 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
795 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
796
796
797 if compare:
797 if compare:
798 # in compare switch the diff base to latest commit from prev version
798 # in compare switch the diff base to latest commit from prev version
799 target_ref_id = prev_pull_request_display_obj.revisions[0]
799 target_ref_id = prev_pull_request_display_obj.revisions[0]
800
800
801 # despite opening commits for bookmarks/branches/tags, we always
801 # despite opening commits for bookmarks/branches/tags, we always
802 # convert this to rev to prevent changes after bookmark or branch change
802 # convert this to rev to prevent changes after bookmark or branch change
803 c.source_ref_type = 'rev'
803 c.source_ref_type = 'rev'
804 c.source_ref = source_ref_id
804 c.source_ref = source_ref_id
805
805
806 c.target_ref_type = 'rev'
806 c.target_ref_type = 'rev'
807 c.target_ref = target_ref_id
807 c.target_ref = target_ref_id
808
808
809 c.source_repo = source_repo
809 c.source_repo = source_repo
810 c.target_repo = target_repo
810 c.target_repo = target_repo
811
811
812 # diff_limit is the old behavior, will cut off the whole diff
812 # diff_limit is the old behavior, will cut off the whole diff
813 # if the limit is applied otherwise will just hide the
813 # if the limit is applied otherwise will just hide the
814 # big files from the front-end
814 # big files from the front-end
815 diff_limit = self.cut_off_limit_diff
815 diff_limit = self.cut_off_limit_diff
816 file_limit = self.cut_off_limit_file
816 file_limit = self.cut_off_limit_file
817
817
818 c.commit_ranges = []
818 c.commit_ranges = []
819 source_commit = EmptyCommit()
819 source_commit = EmptyCommit()
820 target_commit = EmptyCommit()
820 target_commit = EmptyCommit()
821 c.missing_requirements = False
821 c.missing_requirements = False
822
822
823 source_scm = source_repo.scm_instance()
823 source_scm = source_repo.scm_instance()
824 target_scm = target_repo.scm_instance()
824 target_scm = target_repo.scm_instance()
825
825
826 # try first shadow repo, fallback to regular repo
826 # try first shadow repo, fallback to regular repo
827 try:
827 try:
828 commits_source_repo = pull_request_latest.get_shadow_repo()
828 commits_source_repo = pull_request_latest.get_shadow_repo()
829 except Exception:
829 except Exception:
830 log.debug('Failed to get shadow repo', exc_info=True)
830 log.debug('Failed to get shadow repo', exc_info=True)
831 commits_source_repo = source_scm
831 commits_source_repo = source_scm
832
832
833 c.commits_source_repo = commits_source_repo
833 c.commits_source_repo = commits_source_repo
834 commit_cache = {}
834 commit_cache = {}
835 try:
835 try:
836 pre_load = ["author", "branch", "date", "message"]
836 pre_load = ["author", "branch", "date", "message"]
837 show_revs = pull_request_at_ver.revisions
837 show_revs = pull_request_at_ver.revisions
838 for rev in show_revs:
838 for rev in show_revs:
839 comm = commits_source_repo.get_commit(
839 comm = commits_source_repo.get_commit(
840 commit_id=rev, pre_load=pre_load)
840 commit_id=rev, pre_load=pre_load)
841 c.commit_ranges.append(comm)
841 c.commit_ranges.append(comm)
842 commit_cache[comm.raw_id] = comm
842 commit_cache[comm.raw_id] = comm
843
843
844 target_commit = commits_source_repo.get_commit(
844 target_commit = commits_source_repo.get_commit(
845 commit_id=safe_str(target_ref_id))
845 commit_id=safe_str(target_ref_id))
846 source_commit = commits_source_repo.get_commit(
846 source_commit = commits_source_repo.get_commit(
847 commit_id=safe_str(source_ref_id))
847 commit_id=safe_str(source_ref_id))
848 except CommitDoesNotExistError:
848 except CommitDoesNotExistError:
849 pass
849 pass
850 except RepositoryRequirementError:
850 except RepositoryRequirementError:
851 log.warning(
851 log.warning(
852 'Failed to get all required data from repo', exc_info=True)
852 'Failed to get all required data from repo', exc_info=True)
853 c.missing_requirements = True
853 c.missing_requirements = True
854
854
855 c.ancestor = None # set it to None, to hide it from PR view
855 c.ancestor = None # set it to None, to hide it from PR view
856
856
857 try:
857 try:
858 ancestor_id = source_scm.get_common_ancestor(
858 ancestor_id = source_scm.get_common_ancestor(
859 source_commit.raw_id, target_commit.raw_id, target_scm)
859 source_commit.raw_id, target_commit.raw_id, target_scm)
860 c.ancestor_commit = source_scm.get_commit(ancestor_id)
860 c.ancestor_commit = source_scm.get_commit(ancestor_id)
861 except Exception:
861 except Exception:
862 c.ancestor_commit = None
862 c.ancestor_commit = None
863
863
864 c.statuses = source_repo.statuses(
864 c.statuses = source_repo.statuses(
865 [x.raw_id for x in c.commit_ranges])
865 [x.raw_id for x in c.commit_ranges])
866
866
867 # auto collapse if we have more than limit
867 # auto collapse if we have more than limit
868 collapse_limit = diffs.DiffProcessor._collapse_commits_over
868 collapse_limit = diffs.DiffProcessor._collapse_commits_over
869 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
869 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
870 c.compare_mode = compare
870 c.compare_mode = compare
871
871
872 c.missing_commits = False
872 c.missing_commits = False
873 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
873 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
874 or source_commit == target_commit):
874 or source_commit == target_commit):
875
875
876 c.missing_commits = True
876 c.missing_commits = True
877 else:
877 else:
878
878
879 c.diffset = self._get_diffset(
879 c.diffset = self._get_diffset(
880 commits_source_repo, source_ref_id, target_ref_id,
880 commits_source_repo, source_ref_id, target_ref_id,
881 target_commit, source_commit,
881 target_commit, source_commit,
882 diff_limit, file_limit, display_inline_comments)
882 diff_limit, file_limit, display_inline_comments)
883
883
884 c.limited_diff = c.diffset.limited_diff
884 c.limited_diff = c.diffset.limited_diff
885
885
886 # calculate removed files that are bound to comments
886 # calculate removed files that are bound to comments
887 comment_deleted_files = [
887 comment_deleted_files = [
888 fname for fname in display_inline_comments
888 fname for fname in display_inline_comments
889 if fname not in c.diffset.file_stats]
889 if fname not in c.diffset.file_stats]
890
890
891 c.deleted_files_comments = collections.defaultdict(dict)
891 c.deleted_files_comments = collections.defaultdict(dict)
892 for fname, per_line_comments in display_inline_comments.items():
892 for fname, per_line_comments in display_inline_comments.items():
893 if fname in comment_deleted_files:
893 if fname in comment_deleted_files:
894 c.deleted_files_comments[fname]['stats'] = 0
894 c.deleted_files_comments[fname]['stats'] = 0
895 c.deleted_files_comments[fname]['comments'] = list()
895 c.deleted_files_comments[fname]['comments'] = list()
896 for lno, comments in per_line_comments.items():
896 for lno, comments in per_line_comments.items():
897 c.deleted_files_comments[fname]['comments'].extend(
897 c.deleted_files_comments[fname]['comments'].extend(
898 comments)
898 comments)
899
899
900 # this is a hack to properly display links, when creating PR, the
900 # this is a hack to properly display links, when creating PR, the
901 # compare view and others uses different notation, and
901 # compare view and others uses different notation, and
902 # compare_commits.mako renders links based on the target_repo.
902 # 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
903 # We need to swap that here to generate it properly on the html side
904 c.target_repo = c.source_repo
904 c.target_repo = c.source_repo
905
905
906 c.commit_statuses = ChangesetStatus.STATUSES
906 c.commit_statuses = ChangesetStatus.STATUSES
907
907
908 c.show_version_changes = not pr_closed
908 c.show_version_changes = not pr_closed
909 if c.show_version_changes:
909 if c.show_version_changes:
910 cur_obj = pull_request_at_ver
910 cur_obj = pull_request_at_ver
911 prev_obj = prev_pull_request_at_ver
911 prev_obj = prev_pull_request_at_ver
912
912
913 old_commit_ids = prev_obj.revisions
913 old_commit_ids = prev_obj.revisions
914 new_commit_ids = cur_obj.revisions
914 new_commit_ids = cur_obj.revisions
915 commit_changes = PullRequestModel()._calculate_commit_id_changes(
915 commit_changes = PullRequestModel()._calculate_commit_id_changes(
916 old_commit_ids, new_commit_ids)
916 old_commit_ids, new_commit_ids)
917 c.commit_changes_summary = commit_changes
917 c.commit_changes_summary = commit_changes
918
918
919 # calculate the diff for commits between versions
919 # calculate the diff for commits between versions
920 c.commit_changes = []
920 c.commit_changes = []
921 mark = lambda cs, fw: list(
921 mark = lambda cs, fw: list(
922 h.itertools.izip_longest([], cs, fillvalue=fw))
922 h.itertools.izip_longest([], cs, fillvalue=fw))
923 for c_type, raw_id in mark(commit_changes.added, 'a') \
923 for c_type, raw_id in mark(commit_changes.added, 'a') \
924 + mark(commit_changes.removed, 'r') \
924 + mark(commit_changes.removed, 'r') \
925 + mark(commit_changes.common, 'c'):
925 + mark(commit_changes.common, 'c'):
926
926
927 if raw_id in commit_cache:
927 if raw_id in commit_cache:
928 commit = commit_cache[raw_id]
928 commit = commit_cache[raw_id]
929 else:
929 else:
930 try:
930 try:
931 commit = commits_source_repo.get_commit(raw_id)
931 commit = commits_source_repo.get_commit(raw_id)
932 except CommitDoesNotExistError:
932 except CommitDoesNotExistError:
933 # in case we fail extracting still use "dummy" commit
933 # in case we fail extracting still use "dummy" commit
934 # for display in commit diff
934 # for display in commit diff
935 commit = h.AttributeDict(
935 commit = h.AttributeDict(
936 {'raw_id': raw_id,
936 {'raw_id': raw_id,
937 'message': 'EMPTY or MISSING COMMIT'})
937 'message': 'EMPTY or MISSING COMMIT'})
938 c.commit_changes.append([c_type, commit])
938 c.commit_changes.append([c_type, commit])
939
939
940 # current user review statuses for each version
940 # current user review statuses for each version
941 c.review_versions = {}
941 c.review_versions = {}
942 if c.rhodecode_user.user_id in allowed_reviewers:
942 if c.rhodecode_user.user_id in allowed_reviewers:
943 for co in general_comments:
943 for co in general_comments:
944 if co.author.user_id == c.rhodecode_user.user_id:
944 if co.author.user_id == c.rhodecode_user.user_id:
945 # each comment has a status change
945 # each comment has a status change
946 status = co.status_change
946 status = co.status_change
947 if status:
947 if status:
948 _ver_pr = status[0].comment.pull_request_version_id
948 _ver_pr = status[0].comment.pull_request_version_id
949 c.review_versions[_ver_pr] = status[0]
949 c.review_versions[_ver_pr] = status[0]
950
950
951 return render('/pullrequests/pullrequest_show.mako')
951 return render('/pullrequests/pullrequest_show.mako')
952
952
953 @LoginRequired()
953 @LoginRequired()
954 @NotAnonymous()
954 @NotAnonymous()
955 @HasRepoPermissionAnyDecorator(
955 @HasRepoPermissionAnyDecorator(
956 'repository.read', 'repository.write', 'repository.admin')
956 'repository.read', 'repository.write', 'repository.admin')
957 @auth.CSRFRequired()
957 @auth.CSRFRequired()
958 @jsonify
958 @jsonify
959 def comment(self, repo_name, pull_request_id):
959 def comment(self, repo_name, pull_request_id):
960 pull_request_id = safe_int(pull_request_id)
960 pull_request_id = safe_int(pull_request_id)
961 pull_request = PullRequest.get_or_404(pull_request_id)
961 pull_request = PullRequest.get_or_404(pull_request_id)
962 if pull_request.is_closed():
962 if pull_request.is_closed():
963 raise HTTPForbidden()
963 raise HTTPForbidden()
964
964
965 status = request.POST.get('changeset_status', None)
965 status = request.POST.get('changeset_status', None)
966 text = request.POST.get('text')
966 text = request.POST.get('text')
967 comment_type = request.POST.get('comment_type')
967 comment_type = request.POST.get('comment_type')
968 resolves_comment_id = request.POST.get('resolves_comment_id', None)
968 resolves_comment_id = request.POST.get('resolves_comment_id', None)
969 close_pull_request = request.POST.get('close_pull_request')
969 close_pull_request = request.POST.get('close_pull_request')
970
970
971 close_pr = False
971 close_pr = False
972 if close_pull_request:
972 # only owner or admin or person with write permissions
973 allowed_to_close = PullRequestModel().check_user_update(
974 pull_request, c.rhodecode_user)
975
976 if close_pull_request and allowed_to_close:
973 close_pr = True
977 close_pr = True
974 pull_request_review_status = pull_request.calculated_review_status()
978 pull_request_review_status = pull_request.calculated_review_status()
975 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
979 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
976 # approved only if we have voting consent
980 # approved only if we have voting consent
977 status = ChangesetStatus.STATUS_APPROVED
981 status = ChangesetStatus.STATUS_APPROVED
978 else:
982 else:
979 status = ChangesetStatus.STATUS_REJECTED
983 status = ChangesetStatus.STATUS_REJECTED
980
984
981 allowed_to_change_status = PullRequestModel().check_user_change_status(
985 allowed_to_change_status = PullRequestModel().check_user_change_status(
982 pull_request, c.rhodecode_user)
986 pull_request, c.rhodecode_user)
983
987
984 if status and allowed_to_change_status:
988 if status and allowed_to_change_status:
985 message = (_('Status change %(transition_icon)s %(status)s')
989 message = (_('Status change %(transition_icon)s %(status)s')
986 % {'transition_icon': '>',
990 % {'transition_icon': '>',
987 'status': ChangesetStatus.get_status_lbl(status)})
991 'status': ChangesetStatus.get_status_lbl(status)})
988 if close_pr:
992 if close_pr:
989 message = _('Closing with') + ' ' + message
993 message = _('Closing with') + ' ' + message
990 text = text or message
994 text = text or message
991 comm = CommentsModel().create(
995 comm = CommentsModel().create(
992 text=text,
996 text=text,
993 repo=c.rhodecode_db_repo.repo_id,
997 repo=c.rhodecode_db_repo.repo_id,
994 user=c.rhodecode_user.user_id,
998 user=c.rhodecode_user.user_id,
995 pull_request=pull_request_id,
999 pull_request=pull_request_id,
996 f_path=request.POST.get('f_path'),
1000 f_path=request.POST.get('f_path'),
997 line_no=request.POST.get('line'),
1001 line_no=request.POST.get('line'),
998 status_change=(ChangesetStatus.get_status_lbl(status)
1002 status_change=(ChangesetStatus.get_status_lbl(status)
999 if status and allowed_to_change_status else None),
1003 if status and allowed_to_change_status else None),
1000 status_change_type=(status
1004 status_change_type=(status
1001 if status and allowed_to_change_status else None),
1005 if status and allowed_to_change_status else None),
1002 closing_pr=close_pr,
1006 closing_pr=close_pr,
1003 comment_type=comment_type,
1007 comment_type=comment_type,
1004 resolves_comment_id=resolves_comment_id
1008 resolves_comment_id=resolves_comment_id
1005 )
1009 )
1006
1010
1007 if allowed_to_change_status:
1011 if allowed_to_change_status:
1008 old_calculated_status = pull_request.calculated_review_status()
1012 old_calculated_status = pull_request.calculated_review_status()
1009 # get status if set !
1013 # get status if set !
1010 if status:
1014 if status:
1011 ChangesetStatusModel().set_status(
1015 ChangesetStatusModel().set_status(
1012 c.rhodecode_db_repo.repo_id,
1016 c.rhodecode_db_repo.repo_id,
1013 status,
1017 status,
1014 c.rhodecode_user.user_id,
1018 c.rhodecode_user.user_id,
1015 comm,
1019 comm,
1016 pull_request=pull_request_id
1020 pull_request=pull_request_id
1017 )
1021 )
1018
1022
1019 Session().flush()
1023 Session().flush()
1020 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1024 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
1021 # we now calculate the status of pull request, and based on that
1025 # we now calculate the status of pull request, and based on that
1022 # calculation we set the commits status
1026 # calculation we set the commits status
1023 calculated_status = pull_request.calculated_review_status()
1027 calculated_status = pull_request.calculated_review_status()
1024 if old_calculated_status != calculated_status:
1028 if old_calculated_status != calculated_status:
1025 PullRequestModel()._trigger_pull_request_hook(
1029 PullRequestModel()._trigger_pull_request_hook(
1026 pull_request, c.rhodecode_user, 'review_status_change')
1030 pull_request, c.rhodecode_user, 'review_status_change')
1027
1031
1028 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1032 calculated_status_lbl = ChangesetStatus.get_status_lbl(
1029 calculated_status)
1033 calculated_status)
1030
1034
1031 if close_pr:
1035 if close_pr:
1032 status_completed = (
1036 status_completed = (
1033 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1037 calculated_status in [ChangesetStatus.STATUS_APPROVED,
1034 ChangesetStatus.STATUS_REJECTED])
1038 ChangesetStatus.STATUS_REJECTED])
1035 if close_pull_request or status_completed:
1039 if close_pull_request or status_completed:
1036 PullRequestModel().close_pull_request(
1040 PullRequestModel().close_pull_request(
1037 pull_request_id, c.rhodecode_user)
1041 pull_request_id, c.rhodecode_user)
1038 else:
1042 else:
1039 h.flash(_('Closing pull request on other statuses than '
1043 h.flash(_('Closing pull request on other statuses than '
1040 'rejected or approved is forbidden. '
1044 'rejected or approved is forbidden. '
1041 'Calculated status from all reviewers '
1045 'Calculated status from all reviewers '
1042 'is currently: %s') % calculated_status_lbl,
1046 'is currently: %s') % calculated_status_lbl,
1043 category='warning')
1047 category='warning')
1044
1048
1045 Session().commit()
1049 Session().commit()
1046
1050
1047 if not request.is_xhr:
1051 if not request.is_xhr:
1048 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1052 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1049 pull_request_id=pull_request_id))
1053 pull_request_id=pull_request_id))
1050
1054
1051 data = {
1055 data = {
1052 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1056 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1053 }
1057 }
1054 if comm:
1058 if comm:
1055 c.co = comm
1059 c.co = comm
1056 c.inline_comment = True if comm.line_no else False
1060 c.inline_comment = True if comm.line_no else False
1057 data.update(comm.get_dict())
1061 data.update(comm.get_dict())
1058 data.update({'rendered_text':
1062 data.update({'rendered_text':
1059 render('changeset/changeset_comment_block.mako')})
1063 render('changeset/changeset_comment_block.mako')})
1060
1064
1061 return data
1065 return data
1062
1066
1063 @LoginRequired()
1067 @LoginRequired()
1064 @NotAnonymous()
1068 @NotAnonymous()
1065 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1069 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1066 'repository.admin')
1070 'repository.admin')
1067 @auth.CSRFRequired()
1071 @auth.CSRFRequired()
1068 @jsonify
1072 @jsonify
1069 def delete_comment(self, repo_name, comment_id):
1073 def delete_comment(self, repo_name, comment_id):
1070 return self._delete_comment(comment_id)
1074 return self._delete_comment(comment_id)
1071
1075
1072 def _delete_comment(self, comment_id):
1076 def _delete_comment(self, comment_id):
1073 comment_id = safe_int(comment_id)
1077 comment_id = safe_int(comment_id)
1074 co = ChangesetComment.get_or_404(comment_id)
1078 co = ChangesetComment.get_or_404(comment_id)
1075 if co.pull_request.is_closed():
1079 if co.pull_request.is_closed():
1076 # don't allow deleting comments on closed pull request
1080 # don't allow deleting comments on closed pull request
1077 raise HTTPForbidden()
1081 raise HTTPForbidden()
1078
1082
1079 is_owner = co.author.user_id == c.rhodecode_user.user_id
1083 is_owner = co.author.user_id == c.rhodecode_user.user_id
1080 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1084 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1081 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1085 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1082 old_calculated_status = co.pull_request.calculated_review_status()
1086 old_calculated_status = co.pull_request.calculated_review_status()
1083 CommentsModel().delete(comment=co)
1087 CommentsModel().delete(comment=co)
1084 Session().commit()
1088 Session().commit()
1085 calculated_status = co.pull_request.calculated_review_status()
1089 calculated_status = co.pull_request.calculated_review_status()
1086 if old_calculated_status != calculated_status:
1090 if old_calculated_status != calculated_status:
1087 PullRequestModel()._trigger_pull_request_hook(
1091 PullRequestModel()._trigger_pull_request_hook(
1088 co.pull_request, c.rhodecode_user, 'review_status_change')
1092 co.pull_request, c.rhodecode_user, 'review_status_change')
1089 return True
1093 return True
1090 else:
1094 else:
1091 raise HTTPForbidden()
1095 raise HTTPForbidden()
@@ -1,1077 +1,1095 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification)
29 PullRequest, ChangesetStatus, UserLog, Notification)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
34 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
34 from rhodecode.tests import (
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 from rhodecode.tests.utils import AssertResponse
36 from rhodecode.tests.utils import AssertResponse
36
37
37
38
38 @pytest.mark.usefixtures('app', 'autologin_user')
39 @pytest.mark.usefixtures('app', 'autologin_user')
39 @pytest.mark.backends("git", "hg")
40 @pytest.mark.backends("git", "hg")
40 class TestPullrequestsController:
41 class TestPullrequestsController:
41
42
42 def test_index(self, backend):
43 def test_index(self, backend):
43 self.app.get(url(
44 self.app.get(url(
44 controller='pullrequests', action='index',
45 controller='pullrequests', action='index',
45 repo_name=backend.repo_name))
46 repo_name=backend.repo_name))
46
47
47 def test_option_menu_create_pull_request_exists(self, backend):
48 def test_option_menu_create_pull_request_exists(self, backend):
48 repo_name = backend.repo_name
49 repo_name = backend.repo_name
49 response = self.app.get(url('summary_home', repo_name=repo_name))
50 response = self.app.get(url('summary_home', repo_name=repo_name))
50
51
51 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
52 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
52 'pullrequest', repo_name=repo_name)
53 'pullrequest', repo_name=repo_name)
53 response.mustcontain(create_pr_link)
54 response.mustcontain(create_pr_link)
54
55
55 def test_global_redirect_of_pr(self, backend, pr_util):
56 def test_global_redirect_of_pr(self, backend, pr_util):
56 pull_request = pr_util.create_pull_request()
57 pull_request = pr_util.create_pull_request()
57
58
58 response = self.app.get(
59 response = self.app.get(
59 url('pull_requests_global',
60 url('pull_requests_global',
60 pull_request_id=pull_request.pull_request_id))
61 pull_request_id=pull_request.pull_request_id))
61
62
62 repo_name = pull_request.target_repo.repo_name
63 repo_name = pull_request.target_repo.repo_name
63 redirect_url = url('pullrequest_show', repo_name=repo_name,
64 redirect_url = url('pullrequest_show', repo_name=repo_name,
64 pull_request_id=pull_request.pull_request_id)
65 pull_request_id=pull_request.pull_request_id)
65 assert response.status == '302 Found'
66 assert response.status == '302 Found'
66 assert redirect_url in response.location
67 assert redirect_url in response.location
67
68
68 def test_create_pr_form_with_raw_commit_id(self, backend):
69 def test_create_pr_form_with_raw_commit_id(self, backend):
69 repo = backend.repo
70 repo = backend.repo
70
71
71 self.app.get(
72 self.app.get(
72 url(controller='pullrequests', action='index',
73 url(controller='pullrequests', action='index',
73 repo_name=repo.repo_name,
74 repo_name=repo.repo_name,
74 commit=repo.get_commit().raw_id),
75 commit=repo.get_commit().raw_id),
75 status=200)
76 status=200)
76
77
77 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
78 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
78 def test_show(self, pr_util, pr_merge_enabled):
79 def test_show(self, pr_util, pr_merge_enabled):
79 pull_request = pr_util.create_pull_request(
80 pull_request = pr_util.create_pull_request(
80 mergeable=pr_merge_enabled, enable_notifications=False)
81 mergeable=pr_merge_enabled, enable_notifications=False)
81
82
82 response = self.app.get(url(
83 response = self.app.get(url(
83 controller='pullrequests', action='show',
84 controller='pullrequests', action='show',
84 repo_name=pull_request.target_repo.scm_instance().name,
85 repo_name=pull_request.target_repo.scm_instance().name,
85 pull_request_id=str(pull_request.pull_request_id)))
86 pull_request_id=str(pull_request.pull_request_id)))
86
87
87 for commit_id in pull_request.revisions:
88 for commit_id in pull_request.revisions:
88 response.mustcontain(commit_id)
89 response.mustcontain(commit_id)
89
90
90 assert pull_request.target_ref_parts.type in response
91 assert pull_request.target_ref_parts.type in response
91 assert pull_request.target_ref_parts.name in response
92 assert pull_request.target_ref_parts.name in response
92 target_clone_url = pull_request.target_repo.clone_url()
93 target_clone_url = pull_request.target_repo.clone_url()
93 assert target_clone_url in response
94 assert target_clone_url in response
94
95
95 assert 'class="pull-request-merge"' in response
96 assert 'class="pull-request-merge"' in response
96 assert (
97 assert (
97 'Server-side pull request merging is disabled.'
98 'Server-side pull request merging is disabled.'
98 in response) != pr_merge_enabled
99 in response) != pr_merge_enabled
99
100
100 def test_close_status_visibility(self, pr_util, csrf_token):
101 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
101 from rhodecode.tests.functional.test_login import login_url, logut_url
102 from rhodecode.tests.functional.test_login import login_url, logut_url
102 # Logout
103 # Logout
103 response = self.app.post(
104 response = self.app.post(
104 logut_url,
105 logut_url,
105 params={'csrf_token': csrf_token})
106 params={'csrf_token': csrf_token})
106 # Login as regular user
107 # Login as regular user
107 response = self.app.post(login_url,
108 response = self.app.post(login_url,
108 {'username': 'test_regular',
109 {'username': TEST_USER_REGULAR_LOGIN,
109 'password': 'test12'})
110 'password': 'test12'})
110
111
111 pull_request = pr_util.create_pull_request(author='test_regular')
112 pull_request = pr_util.create_pull_request(
113 author=TEST_USER_REGULAR_LOGIN)
112
114
113 response = self.app.get(url(
115 response = self.app.get(url(
114 controller='pullrequests', action='show',
116 controller='pullrequests', action='show',
115 repo_name=pull_request.target_repo.scm_instance().name,
117 repo_name=pull_request.target_repo.scm_instance().name,
116 pull_request_id=str(pull_request.pull_request_id)))
118 pull_request_id=str(pull_request.pull_request_id)))
117
119
118 response.mustcontain('Server-side pull request merging is disabled.')
120 response.mustcontain('Server-side pull request merging is disabled.')
119
121
120 assert_response = response.assert_response()
122 assert_response = response.assert_response()
123 # for regular user without a merge permissions, we don't see it
124 assert_response.no_element_exists('#close-pull-request-action')
125
126 user_util.grant_user_permission_to_repo(
127 pull_request.target_repo,
128 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
129 'repository.write')
130 response = self.app.get(url(
131 controller='pullrequests', action='show',
132 repo_name=pull_request.target_repo.scm_instance().name,
133 pull_request_id=str(pull_request.pull_request_id)))
134
135 response.mustcontain('Server-side pull request merging is disabled.')
136
137 assert_response = response.assert_response()
138 # now regular user has a merge permissions, we have CLOSE button
121 assert_response.one_element_exists('#close-pull-request-action')
139 assert_response.one_element_exists('#close-pull-request-action')
122
140
123 def test_show_invalid_commit_id(self, pr_util):
141 def test_show_invalid_commit_id(self, pr_util):
124 # Simulating invalid revisions which will cause a lookup error
142 # Simulating invalid revisions which will cause a lookup error
125 pull_request = pr_util.create_pull_request()
143 pull_request = pr_util.create_pull_request()
126 pull_request.revisions = ['invalid']
144 pull_request.revisions = ['invalid']
127 Session().add(pull_request)
145 Session().add(pull_request)
128 Session().commit()
146 Session().commit()
129
147
130 response = self.app.get(url(
148 response = self.app.get(url(
131 controller='pullrequests', action='show',
149 controller='pullrequests', action='show',
132 repo_name=pull_request.target_repo.scm_instance().name,
150 repo_name=pull_request.target_repo.scm_instance().name,
133 pull_request_id=str(pull_request.pull_request_id)))
151 pull_request_id=str(pull_request.pull_request_id)))
134
152
135 for commit_id in pull_request.revisions:
153 for commit_id in pull_request.revisions:
136 response.mustcontain(commit_id)
154 response.mustcontain(commit_id)
137
155
138 def test_show_invalid_source_reference(self, pr_util):
156 def test_show_invalid_source_reference(self, pr_util):
139 pull_request = pr_util.create_pull_request()
157 pull_request = pr_util.create_pull_request()
140 pull_request.source_ref = 'branch:b:invalid'
158 pull_request.source_ref = 'branch:b:invalid'
141 Session().add(pull_request)
159 Session().add(pull_request)
142 Session().commit()
160 Session().commit()
143
161
144 self.app.get(url(
162 self.app.get(url(
145 controller='pullrequests', action='show',
163 controller='pullrequests', action='show',
146 repo_name=pull_request.target_repo.scm_instance().name,
164 repo_name=pull_request.target_repo.scm_instance().name,
147 pull_request_id=str(pull_request.pull_request_id)))
165 pull_request_id=str(pull_request.pull_request_id)))
148
166
149 def test_edit_title_description(self, pr_util, csrf_token):
167 def test_edit_title_description(self, pr_util, csrf_token):
150 pull_request = pr_util.create_pull_request()
168 pull_request = pr_util.create_pull_request()
151 pull_request_id = pull_request.pull_request_id
169 pull_request_id = pull_request.pull_request_id
152
170
153 response = self.app.post(
171 response = self.app.post(
154 url(controller='pullrequests', action='update',
172 url(controller='pullrequests', action='update',
155 repo_name=pull_request.target_repo.repo_name,
173 repo_name=pull_request.target_repo.repo_name,
156 pull_request_id=str(pull_request_id)),
174 pull_request_id=str(pull_request_id)),
157 params={
175 params={
158 'edit_pull_request': 'true',
176 'edit_pull_request': 'true',
159 '_method': 'put',
177 '_method': 'put',
160 'title': 'New title',
178 'title': 'New title',
161 'description': 'New description',
179 'description': 'New description',
162 'csrf_token': csrf_token})
180 'csrf_token': csrf_token})
163
181
164 assert_session_flash(
182 assert_session_flash(
165 response, u'Pull request title & description updated.',
183 response, u'Pull request title & description updated.',
166 category='success')
184 category='success')
167
185
168 pull_request = PullRequest.get(pull_request_id)
186 pull_request = PullRequest.get(pull_request_id)
169 assert pull_request.title == 'New title'
187 assert pull_request.title == 'New title'
170 assert pull_request.description == 'New description'
188 assert pull_request.description == 'New description'
171
189
172 def test_edit_title_description_closed(self, pr_util, csrf_token):
190 def test_edit_title_description_closed(self, pr_util, csrf_token):
173 pull_request = pr_util.create_pull_request()
191 pull_request = pr_util.create_pull_request()
174 pull_request_id = pull_request.pull_request_id
192 pull_request_id = pull_request.pull_request_id
175 pr_util.close()
193 pr_util.close()
176
194
177 response = self.app.post(
195 response = self.app.post(
178 url(controller='pullrequests', action='update',
196 url(controller='pullrequests', action='update',
179 repo_name=pull_request.target_repo.repo_name,
197 repo_name=pull_request.target_repo.repo_name,
180 pull_request_id=str(pull_request_id)),
198 pull_request_id=str(pull_request_id)),
181 params={
199 params={
182 'edit_pull_request': 'true',
200 'edit_pull_request': 'true',
183 '_method': 'put',
201 '_method': 'put',
184 'title': 'New title',
202 'title': 'New title',
185 'description': 'New description',
203 'description': 'New description',
186 'csrf_token': csrf_token})
204 'csrf_token': csrf_token})
187
205
188 assert_session_flash(
206 assert_session_flash(
189 response, u'Cannot update closed pull requests.',
207 response, u'Cannot update closed pull requests.',
190 category='error')
208 category='error')
191
209
192 def test_update_invalid_source_reference(self, pr_util, csrf_token):
210 def test_update_invalid_source_reference(self, pr_util, csrf_token):
193 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
211 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
194
212
195 pull_request = pr_util.create_pull_request()
213 pull_request = pr_util.create_pull_request()
196 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
214 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
197 Session().add(pull_request)
215 Session().add(pull_request)
198 Session().commit()
216 Session().commit()
199
217
200 pull_request_id = pull_request.pull_request_id
218 pull_request_id = pull_request.pull_request_id
201
219
202 response = self.app.post(
220 response = self.app.post(
203 url(controller='pullrequests', action='update',
221 url(controller='pullrequests', action='update',
204 repo_name=pull_request.target_repo.repo_name,
222 repo_name=pull_request.target_repo.repo_name,
205 pull_request_id=str(pull_request_id)),
223 pull_request_id=str(pull_request_id)),
206 params={'update_commits': 'true', '_method': 'put',
224 params={'update_commits': 'true', '_method': 'put',
207 'csrf_token': csrf_token})
225 'csrf_token': csrf_token})
208
226
209 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
227 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
210 UpdateFailureReason.MISSING_SOURCE_REF]
228 UpdateFailureReason.MISSING_SOURCE_REF]
211 assert_session_flash(response, expected_msg, category='error')
229 assert_session_flash(response, expected_msg, category='error')
212
230
213 def test_missing_target_reference(self, pr_util, csrf_token):
231 def test_missing_target_reference(self, pr_util, csrf_token):
214 from rhodecode.lib.vcs.backends.base import MergeFailureReason
232 from rhodecode.lib.vcs.backends.base import MergeFailureReason
215 pull_request = pr_util.create_pull_request(
233 pull_request = pr_util.create_pull_request(
216 approved=True, mergeable=True)
234 approved=True, mergeable=True)
217 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
235 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
218 Session().add(pull_request)
236 Session().add(pull_request)
219 Session().commit()
237 Session().commit()
220
238
221 pull_request_id = pull_request.pull_request_id
239 pull_request_id = pull_request.pull_request_id
222 pull_request_url = url(
240 pull_request_url = url(
223 controller='pullrequests', action='show',
241 controller='pullrequests', action='show',
224 repo_name=pull_request.target_repo.repo_name,
242 repo_name=pull_request.target_repo.repo_name,
225 pull_request_id=str(pull_request_id))
243 pull_request_id=str(pull_request_id))
226
244
227 response = self.app.get(pull_request_url)
245 response = self.app.get(pull_request_url)
228
246
229 assertr = AssertResponse(response)
247 assertr = AssertResponse(response)
230 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
248 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
231 MergeFailureReason.MISSING_TARGET_REF]
249 MergeFailureReason.MISSING_TARGET_REF]
232 assertr.element_contains(
250 assertr.element_contains(
233 'span[data-role="merge-message"]', str(expected_msg))
251 'span[data-role="merge-message"]', str(expected_msg))
234
252
235 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
253 def test_comment_and_close_pull_request(self, pr_util, csrf_token):
236 pull_request = pr_util.create_pull_request(approved=True)
254 pull_request = pr_util.create_pull_request(approved=True)
237 pull_request_id = pull_request.pull_request_id
255 pull_request_id = pull_request.pull_request_id
238 author = pull_request.user_id
256 author = pull_request.user_id
239 repo = pull_request.target_repo.repo_id
257 repo = pull_request.target_repo.repo_id
240
258
241 self.app.post(
259 self.app.post(
242 url(controller='pullrequests',
260 url(controller='pullrequests',
243 action='comment',
261 action='comment',
244 repo_name=pull_request.target_repo.scm_instance().name,
262 repo_name=pull_request.target_repo.scm_instance().name,
245 pull_request_id=str(pull_request_id)),
263 pull_request_id=str(pull_request_id)),
246 params={
264 params={
247 'changeset_status': ChangesetStatus.STATUS_APPROVED,
265 'changeset_status': ChangesetStatus.STATUS_APPROVED,
248 'close_pull_request': '1',
266 'close_pull_request': '1',
249 'text': 'Closing a PR',
267 'text': 'Closing a PR',
250 'csrf_token': csrf_token},
268 'csrf_token': csrf_token},
251 status=302)
269 status=302)
252
270
253 action = 'user_closed_pull_request:%d' % pull_request_id
271 action = 'user_closed_pull_request:%d' % pull_request_id
254 journal = UserLog.query()\
272 journal = UserLog.query()\
255 .filter(UserLog.user_id == author)\
273 .filter(UserLog.user_id == author)\
256 .filter(UserLog.repository_id == repo)\
274 .filter(UserLog.repository_id == repo)\
257 .filter(UserLog.action == action)\
275 .filter(UserLog.action == action)\
258 .all()
276 .all()
259 assert len(journal) == 1
277 assert len(journal) == 1
260
278
261 pull_request = PullRequest.get(pull_request_id)
279 pull_request = PullRequest.get(pull_request_id)
262 assert pull_request.is_closed()
280 assert pull_request.is_closed()
263
281
264 # check only the latest status, not the review status
282 # check only the latest status, not the review status
265 status = ChangesetStatusModel().get_status(
283 status = ChangesetStatusModel().get_status(
266 pull_request.source_repo, pull_request=pull_request)
284 pull_request.source_repo, pull_request=pull_request)
267 assert status == ChangesetStatus.STATUS_APPROVED
285 assert status == ChangesetStatus.STATUS_APPROVED
268
286
269 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
287 def test_reject_and_close_pull_request(self, pr_util, csrf_token):
270 pull_request = pr_util.create_pull_request()
288 pull_request = pr_util.create_pull_request()
271 pull_request_id = pull_request.pull_request_id
289 pull_request_id = pull_request.pull_request_id
272 response = self.app.post(
290 response = self.app.post(
273 url(controller='pullrequests',
291 url(controller='pullrequests',
274 action='update',
292 action='update',
275 repo_name=pull_request.target_repo.scm_instance().name,
293 repo_name=pull_request.target_repo.scm_instance().name,
276 pull_request_id=str(pull_request.pull_request_id)),
294 pull_request_id=str(pull_request.pull_request_id)),
277 params={'close_pull_request': 'true', '_method': 'put',
295 params={'close_pull_request': 'true', '_method': 'put',
278 'csrf_token': csrf_token})
296 'csrf_token': csrf_token})
279
297
280 pull_request = PullRequest.get(pull_request_id)
298 pull_request = PullRequest.get(pull_request_id)
281
299
282 assert response.json is True
300 assert response.json is True
283 assert pull_request.is_closed()
301 assert pull_request.is_closed()
284
302
285 # check only the latest status, not the review status
303 # check only the latest status, not the review status
286 status = ChangesetStatusModel().get_status(
304 status = ChangesetStatusModel().get_status(
287 pull_request.source_repo, pull_request=pull_request)
305 pull_request.source_repo, pull_request=pull_request)
288 assert status == ChangesetStatus.STATUS_REJECTED
306 assert status == ChangesetStatus.STATUS_REJECTED
289
307
290 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
308 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
291 pull_request = pr_util.create_pull_request()
309 pull_request = pr_util.create_pull_request()
292 pull_request_id = pull_request.pull_request_id
310 pull_request_id = pull_request.pull_request_id
293 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
311 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
294 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
312 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
295 author = pull_request.user_id
313 author = pull_request.user_id
296 repo = pull_request.target_repo.repo_id
314 repo = pull_request.target_repo.repo_id
297 self.app.post(
315 self.app.post(
298 url(controller='pullrequests',
316 url(controller='pullrequests',
299 action='comment',
317 action='comment',
300 repo_name=pull_request.target_repo.scm_instance().name,
318 repo_name=pull_request.target_repo.scm_instance().name,
301 pull_request_id=str(pull_request_id)),
319 pull_request_id=str(pull_request_id)),
302 params={
320 params={
303 'changeset_status': 'rejected',
321 'changeset_status': 'rejected',
304 'close_pull_request': '1',
322 'close_pull_request': '1',
305 'csrf_token': csrf_token},
323 'csrf_token': csrf_token},
306 status=302)
324 status=302)
307
325
308 pull_request = PullRequest.get(pull_request_id)
326 pull_request = PullRequest.get(pull_request_id)
309
327
310 action = 'user_closed_pull_request:%d' % pull_request_id
328 action = 'user_closed_pull_request:%d' % pull_request_id
311 journal = UserLog.query().filter(
329 journal = UserLog.query().filter(
312 UserLog.user_id == author,
330 UserLog.user_id == author,
313 UserLog.repository_id == repo,
331 UserLog.repository_id == repo,
314 UserLog.action == action).all()
332 UserLog.action == action).all()
315 assert len(journal) == 1
333 assert len(journal) == 1
316
334
317 # check only the latest status, not the review status
335 # check only the latest status, not the review status
318 status = ChangesetStatusModel().get_status(
336 status = ChangesetStatusModel().get_status(
319 pull_request.source_repo, pull_request=pull_request)
337 pull_request.source_repo, pull_request=pull_request)
320 assert status == ChangesetStatus.STATUS_REJECTED
338 assert status == ChangesetStatus.STATUS_REJECTED
321
339
322 def test_create_pull_request(self, backend, csrf_token):
340 def test_create_pull_request(self, backend, csrf_token):
323 commits = [
341 commits = [
324 {'message': 'ancestor'},
342 {'message': 'ancestor'},
325 {'message': 'change'},
343 {'message': 'change'},
326 {'message': 'change2'},
344 {'message': 'change2'},
327 ]
345 ]
328 commit_ids = backend.create_master_repo(commits)
346 commit_ids = backend.create_master_repo(commits)
329 target = backend.create_repo(heads=['ancestor'])
347 target = backend.create_repo(heads=['ancestor'])
330 source = backend.create_repo(heads=['change2'])
348 source = backend.create_repo(heads=['change2'])
331
349
332 response = self.app.post(
350 response = self.app.post(
333 url(
351 url(
334 controller='pullrequests',
352 controller='pullrequests',
335 action='create',
353 action='create',
336 repo_name=source.repo_name
354 repo_name=source.repo_name
337 ),
355 ),
338 [
356 [
339 ('source_repo', source.repo_name),
357 ('source_repo', source.repo_name),
340 ('source_ref', 'branch:default:' + commit_ids['change2']),
358 ('source_ref', 'branch:default:' + commit_ids['change2']),
341 ('target_repo', target.repo_name),
359 ('target_repo', target.repo_name),
342 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
360 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
343 ('pullrequest_desc', 'Description'),
361 ('pullrequest_desc', 'Description'),
344 ('pullrequest_title', 'Title'),
362 ('pullrequest_title', 'Title'),
345 ('__start__', 'review_members:sequence'),
363 ('__start__', 'review_members:sequence'),
346 ('__start__', 'reviewer:mapping'),
364 ('__start__', 'reviewer:mapping'),
347 ('user_id', '1'),
365 ('user_id', '1'),
348 ('__start__', 'reasons:sequence'),
366 ('__start__', 'reasons:sequence'),
349 ('reason', 'Some reason'),
367 ('reason', 'Some reason'),
350 ('__end__', 'reasons:sequence'),
368 ('__end__', 'reasons:sequence'),
351 ('__end__', 'reviewer:mapping'),
369 ('__end__', 'reviewer:mapping'),
352 ('__end__', 'review_members:sequence'),
370 ('__end__', 'review_members:sequence'),
353 ('__start__', 'revisions:sequence'),
371 ('__start__', 'revisions:sequence'),
354 ('revisions', commit_ids['change']),
372 ('revisions', commit_ids['change']),
355 ('revisions', commit_ids['change2']),
373 ('revisions', commit_ids['change2']),
356 ('__end__', 'revisions:sequence'),
374 ('__end__', 'revisions:sequence'),
357 ('user', ''),
375 ('user', ''),
358 ('csrf_token', csrf_token),
376 ('csrf_token', csrf_token),
359 ],
377 ],
360 status=302)
378 status=302)
361
379
362 location = response.headers['Location']
380 location = response.headers['Location']
363 pull_request_id = int(location.rsplit('/', 1)[1])
381 pull_request_id = int(location.rsplit('/', 1)[1])
364 pull_request = PullRequest.get(pull_request_id)
382 pull_request = PullRequest.get(pull_request_id)
365
383
366 # check that we have now both revisions
384 # check that we have now both revisions
367 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
385 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
368 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
386 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
369 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
387 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
370 assert pull_request.target_ref == expected_target_ref
388 assert pull_request.target_ref == expected_target_ref
371
389
372 def test_reviewer_notifications(self, backend, csrf_token):
390 def test_reviewer_notifications(self, backend, csrf_token):
373 # We have to use the app.post for this test so it will create the
391 # We have to use the app.post for this test so it will create the
374 # notifications properly with the new PR
392 # notifications properly with the new PR
375 commits = [
393 commits = [
376 {'message': 'ancestor',
394 {'message': 'ancestor',
377 'added': [FileNode('file_A', content='content_of_ancestor')]},
395 'added': [FileNode('file_A', content='content_of_ancestor')]},
378 {'message': 'change',
396 {'message': 'change',
379 'added': [FileNode('file_a', content='content_of_change')]},
397 'added': [FileNode('file_a', content='content_of_change')]},
380 {'message': 'change-child'},
398 {'message': 'change-child'},
381 {'message': 'ancestor-child', 'parents': ['ancestor'],
399 {'message': 'ancestor-child', 'parents': ['ancestor'],
382 'added': [
400 'added': [
383 FileNode('file_B', content='content_of_ancestor_child')]},
401 FileNode('file_B', content='content_of_ancestor_child')]},
384 {'message': 'ancestor-child-2'},
402 {'message': 'ancestor-child-2'},
385 ]
403 ]
386 commit_ids = backend.create_master_repo(commits)
404 commit_ids = backend.create_master_repo(commits)
387 target = backend.create_repo(heads=['ancestor-child'])
405 target = backend.create_repo(heads=['ancestor-child'])
388 source = backend.create_repo(heads=['change'])
406 source = backend.create_repo(heads=['change'])
389
407
390 response = self.app.post(
408 response = self.app.post(
391 url(
409 url(
392 controller='pullrequests',
410 controller='pullrequests',
393 action='create',
411 action='create',
394 repo_name=source.repo_name
412 repo_name=source.repo_name
395 ),
413 ),
396 [
414 [
397 ('source_repo', source.repo_name),
415 ('source_repo', source.repo_name),
398 ('source_ref', 'branch:default:' + commit_ids['change']),
416 ('source_ref', 'branch:default:' + commit_ids['change']),
399 ('target_repo', target.repo_name),
417 ('target_repo', target.repo_name),
400 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
418 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
401 ('pullrequest_desc', 'Description'),
419 ('pullrequest_desc', 'Description'),
402 ('pullrequest_title', 'Title'),
420 ('pullrequest_title', 'Title'),
403 ('__start__', 'review_members:sequence'),
421 ('__start__', 'review_members:sequence'),
404 ('__start__', 'reviewer:mapping'),
422 ('__start__', 'reviewer:mapping'),
405 ('user_id', '2'),
423 ('user_id', '2'),
406 ('__start__', 'reasons:sequence'),
424 ('__start__', 'reasons:sequence'),
407 ('reason', 'Some reason'),
425 ('reason', 'Some reason'),
408 ('__end__', 'reasons:sequence'),
426 ('__end__', 'reasons:sequence'),
409 ('__end__', 'reviewer:mapping'),
427 ('__end__', 'reviewer:mapping'),
410 ('__end__', 'review_members:sequence'),
428 ('__end__', 'review_members:sequence'),
411 ('__start__', 'revisions:sequence'),
429 ('__start__', 'revisions:sequence'),
412 ('revisions', commit_ids['change']),
430 ('revisions', commit_ids['change']),
413 ('__end__', 'revisions:sequence'),
431 ('__end__', 'revisions:sequence'),
414 ('user', ''),
432 ('user', ''),
415 ('csrf_token', csrf_token),
433 ('csrf_token', csrf_token),
416 ],
434 ],
417 status=302)
435 status=302)
418
436
419 location = response.headers['Location']
437 location = response.headers['Location']
420 pull_request_id = int(location.rsplit('/', 1)[1])
438 pull_request_id = int(location.rsplit('/', 1)[1])
421 pull_request = PullRequest.get(pull_request_id)
439 pull_request = PullRequest.get(pull_request_id)
422
440
423 # Check that a notification was made
441 # Check that a notification was made
424 notifications = Notification.query()\
442 notifications = Notification.query()\
425 .filter(Notification.created_by == pull_request.author.user_id,
443 .filter(Notification.created_by == pull_request.author.user_id,
426 Notification.type_ == Notification.TYPE_PULL_REQUEST,
444 Notification.type_ == Notification.TYPE_PULL_REQUEST,
427 Notification.subject.contains("wants you to review "
445 Notification.subject.contains("wants you to review "
428 "pull request #%d"
446 "pull request #%d"
429 % pull_request_id))
447 % pull_request_id))
430 assert len(notifications.all()) == 1
448 assert len(notifications.all()) == 1
431
449
432 # Change reviewers and check that a notification was made
450 # Change reviewers and check that a notification was made
433 PullRequestModel().update_reviewers(
451 PullRequestModel().update_reviewers(
434 pull_request.pull_request_id, [(1, [])])
452 pull_request.pull_request_id, [(1, [])])
435 assert len(notifications.all()) == 2
453 assert len(notifications.all()) == 2
436
454
437 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
455 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
438 csrf_token):
456 csrf_token):
439 commits = [
457 commits = [
440 {'message': 'ancestor',
458 {'message': 'ancestor',
441 'added': [FileNode('file_A', content='content_of_ancestor')]},
459 'added': [FileNode('file_A', content='content_of_ancestor')]},
442 {'message': 'change',
460 {'message': 'change',
443 'added': [FileNode('file_a', content='content_of_change')]},
461 'added': [FileNode('file_a', content='content_of_change')]},
444 {'message': 'change-child'},
462 {'message': 'change-child'},
445 {'message': 'ancestor-child', 'parents': ['ancestor'],
463 {'message': 'ancestor-child', 'parents': ['ancestor'],
446 'added': [
464 'added': [
447 FileNode('file_B', content='content_of_ancestor_child')]},
465 FileNode('file_B', content='content_of_ancestor_child')]},
448 {'message': 'ancestor-child-2'},
466 {'message': 'ancestor-child-2'},
449 ]
467 ]
450 commit_ids = backend.create_master_repo(commits)
468 commit_ids = backend.create_master_repo(commits)
451 target = backend.create_repo(heads=['ancestor-child'])
469 target = backend.create_repo(heads=['ancestor-child'])
452 source = backend.create_repo(heads=['change'])
470 source = backend.create_repo(heads=['change'])
453
471
454 response = self.app.post(
472 response = self.app.post(
455 url(
473 url(
456 controller='pullrequests',
474 controller='pullrequests',
457 action='create',
475 action='create',
458 repo_name=source.repo_name
476 repo_name=source.repo_name
459 ),
477 ),
460 [
478 [
461 ('source_repo', source.repo_name),
479 ('source_repo', source.repo_name),
462 ('source_ref', 'branch:default:' + commit_ids['change']),
480 ('source_ref', 'branch:default:' + commit_ids['change']),
463 ('target_repo', target.repo_name),
481 ('target_repo', target.repo_name),
464 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
482 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
465 ('pullrequest_desc', 'Description'),
483 ('pullrequest_desc', 'Description'),
466 ('pullrequest_title', 'Title'),
484 ('pullrequest_title', 'Title'),
467 ('__start__', 'review_members:sequence'),
485 ('__start__', 'review_members:sequence'),
468 ('__start__', 'reviewer:mapping'),
486 ('__start__', 'reviewer:mapping'),
469 ('user_id', '1'),
487 ('user_id', '1'),
470 ('__start__', 'reasons:sequence'),
488 ('__start__', 'reasons:sequence'),
471 ('reason', 'Some reason'),
489 ('reason', 'Some reason'),
472 ('__end__', 'reasons:sequence'),
490 ('__end__', 'reasons:sequence'),
473 ('__end__', 'reviewer:mapping'),
491 ('__end__', 'reviewer:mapping'),
474 ('__end__', 'review_members:sequence'),
492 ('__end__', 'review_members:sequence'),
475 ('__start__', 'revisions:sequence'),
493 ('__start__', 'revisions:sequence'),
476 ('revisions', commit_ids['change']),
494 ('revisions', commit_ids['change']),
477 ('__end__', 'revisions:sequence'),
495 ('__end__', 'revisions:sequence'),
478 ('user', ''),
496 ('user', ''),
479 ('csrf_token', csrf_token),
497 ('csrf_token', csrf_token),
480 ],
498 ],
481 status=302)
499 status=302)
482
500
483 location = response.headers['Location']
501 location = response.headers['Location']
484 pull_request_id = int(location.rsplit('/', 1)[1])
502 pull_request_id = int(location.rsplit('/', 1)[1])
485 pull_request = PullRequest.get(pull_request_id)
503 pull_request = PullRequest.get(pull_request_id)
486
504
487 # target_ref has to point to the ancestor's commit_id in order to
505 # target_ref has to point to the ancestor's commit_id in order to
488 # show the correct diff
506 # show the correct diff
489 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
507 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
490 assert pull_request.target_ref == expected_target_ref
508 assert pull_request.target_ref == expected_target_ref
491
509
492 # Check generated diff contents
510 # Check generated diff contents
493 response = response.follow()
511 response = response.follow()
494 assert 'content_of_ancestor' not in response.body
512 assert 'content_of_ancestor' not in response.body
495 assert 'content_of_ancestor-child' not in response.body
513 assert 'content_of_ancestor-child' not in response.body
496 assert 'content_of_change' in response.body
514 assert 'content_of_change' in response.body
497
515
498 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
516 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
499 # Clear any previous calls to rcextensions
517 # Clear any previous calls to rcextensions
500 rhodecode.EXTENSIONS.calls.clear()
518 rhodecode.EXTENSIONS.calls.clear()
501
519
502 pull_request = pr_util.create_pull_request(
520 pull_request = pr_util.create_pull_request(
503 approved=True, mergeable=True)
521 approved=True, mergeable=True)
504 pull_request_id = pull_request.pull_request_id
522 pull_request_id = pull_request.pull_request_id
505 repo_name = pull_request.target_repo.scm_instance().name,
523 repo_name = pull_request.target_repo.scm_instance().name,
506
524
507 response = self.app.post(
525 response = self.app.post(
508 url(controller='pullrequests',
526 url(controller='pullrequests',
509 action='merge',
527 action='merge',
510 repo_name=str(repo_name[0]),
528 repo_name=str(repo_name[0]),
511 pull_request_id=str(pull_request_id)),
529 pull_request_id=str(pull_request_id)),
512 params={'csrf_token': csrf_token}).follow()
530 params={'csrf_token': csrf_token}).follow()
513
531
514 pull_request = PullRequest.get(pull_request_id)
532 pull_request = PullRequest.get(pull_request_id)
515
533
516 assert response.status_int == 200
534 assert response.status_int == 200
517 assert pull_request.is_closed()
535 assert pull_request.is_closed()
518 assert_pull_request_status(
536 assert_pull_request_status(
519 pull_request, ChangesetStatus.STATUS_APPROVED)
537 pull_request, ChangesetStatus.STATUS_APPROVED)
520
538
521 # Check the relevant log entries were added
539 # Check the relevant log entries were added
522 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
540 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
523 actions = [log.action for log in user_logs]
541 actions = [log.action for log in user_logs]
524 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
542 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
525 expected_actions = [
543 expected_actions = [
526 u'user_closed_pull_request:%d' % pull_request_id,
544 u'user_closed_pull_request:%d' % pull_request_id,
527 u'user_merged_pull_request:%d' % pull_request_id,
545 u'user_merged_pull_request:%d' % pull_request_id,
528 # The action below reflect that the post push actions were executed
546 # The action below reflect that the post push actions were executed
529 u'user_commented_pull_request:%d' % pull_request_id,
547 u'user_commented_pull_request:%d' % pull_request_id,
530 u'push:%s' % ','.join(pr_commit_ids),
548 u'push:%s' % ','.join(pr_commit_ids),
531 ]
549 ]
532 assert actions == expected_actions
550 assert actions == expected_actions
533
551
534 # Check post_push rcextension was really executed
552 # Check post_push rcextension was really executed
535 push_calls = rhodecode.EXTENSIONS.calls['post_push']
553 push_calls = rhodecode.EXTENSIONS.calls['post_push']
536 assert len(push_calls) == 1
554 assert len(push_calls) == 1
537 unused_last_call_args, last_call_kwargs = push_calls[0]
555 unused_last_call_args, last_call_kwargs = push_calls[0]
538 assert last_call_kwargs['action'] == 'push'
556 assert last_call_kwargs['action'] == 'push'
539 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
557 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
540
558
541 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
559 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
542 pull_request = pr_util.create_pull_request(mergeable=False)
560 pull_request = pr_util.create_pull_request(mergeable=False)
543 pull_request_id = pull_request.pull_request_id
561 pull_request_id = pull_request.pull_request_id
544 pull_request = PullRequest.get(pull_request_id)
562 pull_request = PullRequest.get(pull_request_id)
545
563
546 response = self.app.post(
564 response = self.app.post(
547 url(controller='pullrequests',
565 url(controller='pullrequests',
548 action='merge',
566 action='merge',
549 repo_name=pull_request.target_repo.scm_instance().name,
567 repo_name=pull_request.target_repo.scm_instance().name,
550 pull_request_id=str(pull_request.pull_request_id)),
568 pull_request_id=str(pull_request.pull_request_id)),
551 params={'csrf_token': csrf_token}).follow()
569 params={'csrf_token': csrf_token}).follow()
552
570
553 assert response.status_int == 200
571 assert response.status_int == 200
554 response.mustcontain(
572 response.mustcontain(
555 'Merge is not currently possible because of below failed checks.')
573 'Merge is not currently possible because of below failed checks.')
556 response.mustcontain('Server-side pull request merging is disabled.')
574 response.mustcontain('Server-side pull request merging is disabled.')
557
575
558 @pytest.mark.skip_backends('svn')
576 @pytest.mark.skip_backends('svn')
559 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
577 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
560 pull_request = pr_util.create_pull_request(mergeable=True)
578 pull_request = pr_util.create_pull_request(mergeable=True)
561 pull_request_id = pull_request.pull_request_id
579 pull_request_id = pull_request.pull_request_id
562 repo_name = pull_request.target_repo.scm_instance().name,
580 repo_name = pull_request.target_repo.scm_instance().name,
563
581
564 response = self.app.post(
582 response = self.app.post(
565 url(controller='pullrequests',
583 url(controller='pullrequests',
566 action='merge',
584 action='merge',
567 repo_name=str(repo_name[0]),
585 repo_name=str(repo_name[0]),
568 pull_request_id=str(pull_request_id)),
586 pull_request_id=str(pull_request_id)),
569 params={'csrf_token': csrf_token}).follow()
587 params={'csrf_token': csrf_token}).follow()
570
588
571 assert response.status_int == 200
589 assert response.status_int == 200
572
590
573 response.mustcontain(
591 response.mustcontain(
574 'Merge is not currently possible because of below failed checks.')
592 'Merge is not currently possible because of below failed checks.')
575 response.mustcontain('Pull request reviewer approval is pending.')
593 response.mustcontain('Pull request reviewer approval is pending.')
576
594
577 def test_update_source_revision(self, backend, csrf_token):
595 def test_update_source_revision(self, backend, csrf_token):
578 commits = [
596 commits = [
579 {'message': 'ancestor'},
597 {'message': 'ancestor'},
580 {'message': 'change'},
598 {'message': 'change'},
581 {'message': 'change-2'},
599 {'message': 'change-2'},
582 ]
600 ]
583 commit_ids = backend.create_master_repo(commits)
601 commit_ids = backend.create_master_repo(commits)
584 target = backend.create_repo(heads=['ancestor'])
602 target = backend.create_repo(heads=['ancestor'])
585 source = backend.create_repo(heads=['change'])
603 source = backend.create_repo(heads=['change'])
586
604
587 # create pr from a in source to A in target
605 # create pr from a in source to A in target
588 pull_request = PullRequest()
606 pull_request = PullRequest()
589 pull_request.source_repo = source
607 pull_request.source_repo = source
590 # TODO: johbo: Make sure that we write the source ref this way!
608 # TODO: johbo: Make sure that we write the source ref this way!
591 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
609 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
592 branch=backend.default_branch_name, commit_id=commit_ids['change'])
610 branch=backend.default_branch_name, commit_id=commit_ids['change'])
593 pull_request.target_repo = target
611 pull_request.target_repo = target
594
612
595 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
613 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
596 branch=backend.default_branch_name,
614 branch=backend.default_branch_name,
597 commit_id=commit_ids['ancestor'])
615 commit_id=commit_ids['ancestor'])
598 pull_request.revisions = [commit_ids['change']]
616 pull_request.revisions = [commit_ids['change']]
599 pull_request.title = u"Test"
617 pull_request.title = u"Test"
600 pull_request.description = u"Description"
618 pull_request.description = u"Description"
601 pull_request.author = UserModel().get_by_username(
619 pull_request.author = UserModel().get_by_username(
602 TEST_USER_ADMIN_LOGIN)
620 TEST_USER_ADMIN_LOGIN)
603 Session().add(pull_request)
621 Session().add(pull_request)
604 Session().commit()
622 Session().commit()
605 pull_request_id = pull_request.pull_request_id
623 pull_request_id = pull_request.pull_request_id
606
624
607 # source has ancestor - change - change-2
625 # source has ancestor - change - change-2
608 backend.pull_heads(source, heads=['change-2'])
626 backend.pull_heads(source, heads=['change-2'])
609
627
610 # update PR
628 # update PR
611 self.app.post(
629 self.app.post(
612 url(controller='pullrequests', action='update',
630 url(controller='pullrequests', action='update',
613 repo_name=target.repo_name,
631 repo_name=target.repo_name,
614 pull_request_id=str(pull_request_id)),
632 pull_request_id=str(pull_request_id)),
615 params={'update_commits': 'true', '_method': 'put',
633 params={'update_commits': 'true', '_method': 'put',
616 'csrf_token': csrf_token})
634 'csrf_token': csrf_token})
617
635
618 # check that we have now both revisions
636 # check that we have now both revisions
619 pull_request = PullRequest.get(pull_request_id)
637 pull_request = PullRequest.get(pull_request_id)
620 assert pull_request.revisions == [
638 assert pull_request.revisions == [
621 commit_ids['change-2'], commit_ids['change']]
639 commit_ids['change-2'], commit_ids['change']]
622
640
623 # TODO: johbo: this should be a test on its own
641 # TODO: johbo: this should be a test on its own
624 response = self.app.get(url(
642 response = self.app.get(url(
625 controller='pullrequests', action='index',
643 controller='pullrequests', action='index',
626 repo_name=target.repo_name))
644 repo_name=target.repo_name))
627 assert response.status_int == 200
645 assert response.status_int == 200
628 assert 'Pull request updated to' in response.body
646 assert 'Pull request updated to' in response.body
629 assert 'with 1 added, 0 removed commits.' in response.body
647 assert 'with 1 added, 0 removed commits.' in response.body
630
648
631 def test_update_target_revision(self, backend, csrf_token):
649 def test_update_target_revision(self, backend, csrf_token):
632 commits = [
650 commits = [
633 {'message': 'ancestor'},
651 {'message': 'ancestor'},
634 {'message': 'change'},
652 {'message': 'change'},
635 {'message': 'ancestor-new', 'parents': ['ancestor']},
653 {'message': 'ancestor-new', 'parents': ['ancestor']},
636 {'message': 'change-rebased'},
654 {'message': 'change-rebased'},
637 ]
655 ]
638 commit_ids = backend.create_master_repo(commits)
656 commit_ids = backend.create_master_repo(commits)
639 target = backend.create_repo(heads=['ancestor'])
657 target = backend.create_repo(heads=['ancestor'])
640 source = backend.create_repo(heads=['change'])
658 source = backend.create_repo(heads=['change'])
641
659
642 # create pr from a in source to A in target
660 # create pr from a in source to A in target
643 pull_request = PullRequest()
661 pull_request = PullRequest()
644 pull_request.source_repo = source
662 pull_request.source_repo = source
645 # TODO: johbo: Make sure that we write the source ref this way!
663 # TODO: johbo: Make sure that we write the source ref this way!
646 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
664 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
647 branch=backend.default_branch_name, commit_id=commit_ids['change'])
665 branch=backend.default_branch_name, commit_id=commit_ids['change'])
648 pull_request.target_repo = target
666 pull_request.target_repo = target
649 # TODO: johbo: Target ref should be branch based, since tip can jump
667 # TODO: johbo: Target ref should be branch based, since tip can jump
650 # from branch to branch
668 # from branch to branch
651 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
669 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
652 branch=backend.default_branch_name,
670 branch=backend.default_branch_name,
653 commit_id=commit_ids['ancestor'])
671 commit_id=commit_ids['ancestor'])
654 pull_request.revisions = [commit_ids['change']]
672 pull_request.revisions = [commit_ids['change']]
655 pull_request.title = u"Test"
673 pull_request.title = u"Test"
656 pull_request.description = u"Description"
674 pull_request.description = u"Description"
657 pull_request.author = UserModel().get_by_username(
675 pull_request.author = UserModel().get_by_username(
658 TEST_USER_ADMIN_LOGIN)
676 TEST_USER_ADMIN_LOGIN)
659 Session().add(pull_request)
677 Session().add(pull_request)
660 Session().commit()
678 Session().commit()
661 pull_request_id = pull_request.pull_request_id
679 pull_request_id = pull_request.pull_request_id
662
680
663 # target has ancestor - ancestor-new
681 # target has ancestor - ancestor-new
664 # source has ancestor - ancestor-new - change-rebased
682 # source has ancestor - ancestor-new - change-rebased
665 backend.pull_heads(target, heads=['ancestor-new'])
683 backend.pull_heads(target, heads=['ancestor-new'])
666 backend.pull_heads(source, heads=['change-rebased'])
684 backend.pull_heads(source, heads=['change-rebased'])
667
685
668 # update PR
686 # update PR
669 self.app.post(
687 self.app.post(
670 url(controller='pullrequests', action='update',
688 url(controller='pullrequests', action='update',
671 repo_name=target.repo_name,
689 repo_name=target.repo_name,
672 pull_request_id=str(pull_request_id)),
690 pull_request_id=str(pull_request_id)),
673 params={'update_commits': 'true', '_method': 'put',
691 params={'update_commits': 'true', '_method': 'put',
674 'csrf_token': csrf_token},
692 'csrf_token': csrf_token},
675 status=200)
693 status=200)
676
694
677 # check that we have now both revisions
695 # check that we have now both revisions
678 pull_request = PullRequest.get(pull_request_id)
696 pull_request = PullRequest.get(pull_request_id)
679 assert pull_request.revisions == [commit_ids['change-rebased']]
697 assert pull_request.revisions == [commit_ids['change-rebased']]
680 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
698 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
681 branch=backend.default_branch_name,
699 branch=backend.default_branch_name,
682 commit_id=commit_ids['ancestor-new'])
700 commit_id=commit_ids['ancestor-new'])
683
701
684 # TODO: johbo: This should be a test on its own
702 # TODO: johbo: This should be a test on its own
685 response = self.app.get(url(
703 response = self.app.get(url(
686 controller='pullrequests', action='index',
704 controller='pullrequests', action='index',
687 repo_name=target.repo_name))
705 repo_name=target.repo_name))
688 assert response.status_int == 200
706 assert response.status_int == 200
689 assert 'Pull request updated to' in response.body
707 assert 'Pull request updated to' in response.body
690 assert 'with 1 added, 1 removed commits.' in response.body
708 assert 'with 1 added, 1 removed commits.' in response.body
691
709
692 def test_update_of_ancestor_reference(self, backend, csrf_token):
710 def test_update_of_ancestor_reference(self, backend, csrf_token):
693 commits = [
711 commits = [
694 {'message': 'ancestor'},
712 {'message': 'ancestor'},
695 {'message': 'change'},
713 {'message': 'change'},
696 {'message': 'change-2'},
714 {'message': 'change-2'},
697 {'message': 'ancestor-new', 'parents': ['ancestor']},
715 {'message': 'ancestor-new', 'parents': ['ancestor']},
698 {'message': 'change-rebased'},
716 {'message': 'change-rebased'},
699 ]
717 ]
700 commit_ids = backend.create_master_repo(commits)
718 commit_ids = backend.create_master_repo(commits)
701 target = backend.create_repo(heads=['ancestor'])
719 target = backend.create_repo(heads=['ancestor'])
702 source = backend.create_repo(heads=['change'])
720 source = backend.create_repo(heads=['change'])
703
721
704 # create pr from a in source to A in target
722 # create pr from a in source to A in target
705 pull_request = PullRequest()
723 pull_request = PullRequest()
706 pull_request.source_repo = source
724 pull_request.source_repo = source
707 # TODO: johbo: Make sure that we write the source ref this way!
725 # TODO: johbo: Make sure that we write the source ref this way!
708 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
726 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
709 branch=backend.default_branch_name,
727 branch=backend.default_branch_name,
710 commit_id=commit_ids['change'])
728 commit_id=commit_ids['change'])
711 pull_request.target_repo = target
729 pull_request.target_repo = target
712 # TODO: johbo: Target ref should be branch based, since tip can jump
730 # TODO: johbo: Target ref should be branch based, since tip can jump
713 # from branch to branch
731 # from branch to branch
714 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
732 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
715 branch=backend.default_branch_name,
733 branch=backend.default_branch_name,
716 commit_id=commit_ids['ancestor'])
734 commit_id=commit_ids['ancestor'])
717 pull_request.revisions = [commit_ids['change']]
735 pull_request.revisions = [commit_ids['change']]
718 pull_request.title = u"Test"
736 pull_request.title = u"Test"
719 pull_request.description = u"Description"
737 pull_request.description = u"Description"
720 pull_request.author = UserModel().get_by_username(
738 pull_request.author = UserModel().get_by_username(
721 TEST_USER_ADMIN_LOGIN)
739 TEST_USER_ADMIN_LOGIN)
722 Session().add(pull_request)
740 Session().add(pull_request)
723 Session().commit()
741 Session().commit()
724 pull_request_id = pull_request.pull_request_id
742 pull_request_id = pull_request.pull_request_id
725
743
726 # target has ancestor - ancestor-new
744 # target has ancestor - ancestor-new
727 # source has ancestor - ancestor-new - change-rebased
745 # source has ancestor - ancestor-new - change-rebased
728 backend.pull_heads(target, heads=['ancestor-new'])
746 backend.pull_heads(target, heads=['ancestor-new'])
729 backend.pull_heads(source, heads=['change-rebased'])
747 backend.pull_heads(source, heads=['change-rebased'])
730
748
731 # update PR
749 # update PR
732 self.app.post(
750 self.app.post(
733 url(controller='pullrequests', action='update',
751 url(controller='pullrequests', action='update',
734 repo_name=target.repo_name,
752 repo_name=target.repo_name,
735 pull_request_id=str(pull_request_id)),
753 pull_request_id=str(pull_request_id)),
736 params={'update_commits': 'true', '_method': 'put',
754 params={'update_commits': 'true', '_method': 'put',
737 'csrf_token': csrf_token},
755 'csrf_token': csrf_token},
738 status=200)
756 status=200)
739
757
740 # Expect the target reference to be updated correctly
758 # Expect the target reference to be updated correctly
741 pull_request = PullRequest.get(pull_request_id)
759 pull_request = PullRequest.get(pull_request_id)
742 assert pull_request.revisions == [commit_ids['change-rebased']]
760 assert pull_request.revisions == [commit_ids['change-rebased']]
743 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
761 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
744 branch=backend.default_branch_name,
762 branch=backend.default_branch_name,
745 commit_id=commit_ids['ancestor-new'])
763 commit_id=commit_ids['ancestor-new'])
746 assert pull_request.target_ref == expected_target_ref
764 assert pull_request.target_ref == expected_target_ref
747
765
748 def test_remove_pull_request_branch(self, backend_git, csrf_token):
766 def test_remove_pull_request_branch(self, backend_git, csrf_token):
749 branch_name = 'development'
767 branch_name = 'development'
750 commits = [
768 commits = [
751 {'message': 'initial-commit'},
769 {'message': 'initial-commit'},
752 {'message': 'old-feature'},
770 {'message': 'old-feature'},
753 {'message': 'new-feature', 'branch': branch_name},
771 {'message': 'new-feature', 'branch': branch_name},
754 ]
772 ]
755 repo = backend_git.create_repo(commits)
773 repo = backend_git.create_repo(commits)
756 commit_ids = backend_git.commit_ids
774 commit_ids = backend_git.commit_ids
757
775
758 pull_request = PullRequest()
776 pull_request = PullRequest()
759 pull_request.source_repo = repo
777 pull_request.source_repo = repo
760 pull_request.target_repo = repo
778 pull_request.target_repo = repo
761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
779 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
762 branch=branch_name, commit_id=commit_ids['new-feature'])
780 branch=branch_name, commit_id=commit_ids['new-feature'])
763 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
781 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
764 branch=backend_git.default_branch_name,
782 branch=backend_git.default_branch_name,
765 commit_id=commit_ids['old-feature'])
783 commit_id=commit_ids['old-feature'])
766 pull_request.revisions = [commit_ids['new-feature']]
784 pull_request.revisions = [commit_ids['new-feature']]
767 pull_request.title = u"Test"
785 pull_request.title = u"Test"
768 pull_request.description = u"Description"
786 pull_request.description = u"Description"
769 pull_request.author = UserModel().get_by_username(
787 pull_request.author = UserModel().get_by_username(
770 TEST_USER_ADMIN_LOGIN)
788 TEST_USER_ADMIN_LOGIN)
771 Session().add(pull_request)
789 Session().add(pull_request)
772 Session().commit()
790 Session().commit()
773
791
774 vcs = repo.scm_instance()
792 vcs = repo.scm_instance()
775 vcs.remove_ref('refs/heads/{}'.format(branch_name))
793 vcs.remove_ref('refs/heads/{}'.format(branch_name))
776
794
777 response = self.app.get(url(
795 response = self.app.get(url(
778 controller='pullrequests', action='show',
796 controller='pullrequests', action='show',
779 repo_name=repo.repo_name,
797 repo_name=repo.repo_name,
780 pull_request_id=str(pull_request.pull_request_id)))
798 pull_request_id=str(pull_request.pull_request_id)))
781
799
782 assert response.status_int == 200
800 assert response.status_int == 200
783 assert_response = AssertResponse(response)
801 assert_response = AssertResponse(response)
784 assert_response.element_contains(
802 assert_response.element_contains(
785 '#changeset_compare_view_content .alert strong',
803 '#changeset_compare_view_content .alert strong',
786 'Missing commits')
804 'Missing commits')
787 assert_response.element_contains(
805 assert_response.element_contains(
788 '#changeset_compare_view_content .alert',
806 '#changeset_compare_view_content .alert',
789 'This pull request cannot be displayed, because one or more'
807 'This pull request cannot be displayed, because one or more'
790 ' commits no longer exist in the source repository.')
808 ' commits no longer exist in the source repository.')
791
809
792 def test_strip_commits_from_pull_request(
810 def test_strip_commits_from_pull_request(
793 self, backend, pr_util, csrf_token):
811 self, backend, pr_util, csrf_token):
794 commits = [
812 commits = [
795 {'message': 'initial-commit'},
813 {'message': 'initial-commit'},
796 {'message': 'old-feature'},
814 {'message': 'old-feature'},
797 {'message': 'new-feature', 'parents': ['initial-commit']},
815 {'message': 'new-feature', 'parents': ['initial-commit']},
798 ]
816 ]
799 pull_request = pr_util.create_pull_request(
817 pull_request = pr_util.create_pull_request(
800 commits, target_head='initial-commit', source_head='new-feature',
818 commits, target_head='initial-commit', source_head='new-feature',
801 revisions=['new-feature'])
819 revisions=['new-feature'])
802
820
803 vcs = pr_util.source_repository.scm_instance()
821 vcs = pr_util.source_repository.scm_instance()
804 if backend.alias == 'git':
822 if backend.alias == 'git':
805 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
823 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
806 else:
824 else:
807 vcs.strip(pr_util.commit_ids['new-feature'])
825 vcs.strip(pr_util.commit_ids['new-feature'])
808
826
809 response = self.app.get(url(
827 response = self.app.get(url(
810 controller='pullrequests', action='show',
828 controller='pullrequests', action='show',
811 repo_name=pr_util.target_repository.repo_name,
829 repo_name=pr_util.target_repository.repo_name,
812 pull_request_id=str(pull_request.pull_request_id)))
830 pull_request_id=str(pull_request.pull_request_id)))
813
831
814 assert response.status_int == 200
832 assert response.status_int == 200
815 assert_response = AssertResponse(response)
833 assert_response = AssertResponse(response)
816 assert_response.element_contains(
834 assert_response.element_contains(
817 '#changeset_compare_view_content .alert strong',
835 '#changeset_compare_view_content .alert strong',
818 'Missing commits')
836 'Missing commits')
819 assert_response.element_contains(
837 assert_response.element_contains(
820 '#changeset_compare_view_content .alert',
838 '#changeset_compare_view_content .alert',
821 'This pull request cannot be displayed, because one or more'
839 'This pull request cannot be displayed, because one or more'
822 ' commits no longer exist in the source repository.')
840 ' commits no longer exist in the source repository.')
823 assert_response.element_contains(
841 assert_response.element_contains(
824 '#update_commits',
842 '#update_commits',
825 'Update commits')
843 'Update commits')
826
844
827 def test_strip_commits_and_update(
845 def test_strip_commits_and_update(
828 self, backend, pr_util, csrf_token):
846 self, backend, pr_util, csrf_token):
829 commits = [
847 commits = [
830 {'message': 'initial-commit'},
848 {'message': 'initial-commit'},
831 {'message': 'old-feature'},
849 {'message': 'old-feature'},
832 {'message': 'new-feature', 'parents': ['old-feature']},
850 {'message': 'new-feature', 'parents': ['old-feature']},
833 ]
851 ]
834 pull_request = pr_util.create_pull_request(
852 pull_request = pr_util.create_pull_request(
835 commits, target_head='old-feature', source_head='new-feature',
853 commits, target_head='old-feature', source_head='new-feature',
836 revisions=['new-feature'], mergeable=True)
854 revisions=['new-feature'], mergeable=True)
837
855
838 vcs = pr_util.source_repository.scm_instance()
856 vcs = pr_util.source_repository.scm_instance()
839 if backend.alias == 'git':
857 if backend.alias == 'git':
840 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
841 else:
859 else:
842 vcs.strip(pr_util.commit_ids['new-feature'])
860 vcs.strip(pr_util.commit_ids['new-feature'])
843
861
844 response = self.app.post(
862 response = self.app.post(
845 url(controller='pullrequests', action='update',
863 url(controller='pullrequests', action='update',
846 repo_name=pull_request.target_repo.repo_name,
864 repo_name=pull_request.target_repo.repo_name,
847 pull_request_id=str(pull_request.pull_request_id)),
865 pull_request_id=str(pull_request.pull_request_id)),
848 params={'update_commits': 'true', '_method': 'put',
866 params={'update_commits': 'true', '_method': 'put',
849 'csrf_token': csrf_token})
867 'csrf_token': csrf_token})
850
868
851 assert response.status_int == 200
869 assert response.status_int == 200
852 assert response.body == 'true'
870 assert response.body == 'true'
853
871
854 # Make sure that after update, it won't raise 500 errors
872 # Make sure that after update, it won't raise 500 errors
855 response = self.app.get(url(
873 response = self.app.get(url(
856 controller='pullrequests', action='show',
874 controller='pullrequests', action='show',
857 repo_name=pr_util.target_repository.repo_name,
875 repo_name=pr_util.target_repository.repo_name,
858 pull_request_id=str(pull_request.pull_request_id)))
876 pull_request_id=str(pull_request.pull_request_id)))
859
877
860 assert response.status_int == 200
878 assert response.status_int == 200
861 assert_response = AssertResponse(response)
879 assert_response = AssertResponse(response)
862 assert_response.element_contains(
880 assert_response.element_contains(
863 '#changeset_compare_view_content .alert strong',
881 '#changeset_compare_view_content .alert strong',
864 'Missing commits')
882 'Missing commits')
865
883
866 def test_branch_is_a_link(self, pr_util):
884 def test_branch_is_a_link(self, pr_util):
867 pull_request = pr_util.create_pull_request()
885 pull_request = pr_util.create_pull_request()
868 pull_request.source_ref = 'branch:origin:1234567890abcdef'
886 pull_request.source_ref = 'branch:origin:1234567890abcdef'
869 pull_request.target_ref = 'branch:target:abcdef1234567890'
887 pull_request.target_ref = 'branch:target:abcdef1234567890'
870 Session().add(pull_request)
888 Session().add(pull_request)
871 Session().commit()
889 Session().commit()
872
890
873 response = self.app.get(url(
891 response = self.app.get(url(
874 controller='pullrequests', action='show',
892 controller='pullrequests', action='show',
875 repo_name=pull_request.target_repo.scm_instance().name,
893 repo_name=pull_request.target_repo.scm_instance().name,
876 pull_request_id=str(pull_request.pull_request_id)))
894 pull_request_id=str(pull_request.pull_request_id)))
877 assert response.status_int == 200
895 assert response.status_int == 200
878 assert_response = AssertResponse(response)
896 assert_response = AssertResponse(response)
879
897
880 origin = assert_response.get_element('.pr-origininfo .tag')
898 origin = assert_response.get_element('.pr-origininfo .tag')
881 origin_children = origin.getchildren()
899 origin_children = origin.getchildren()
882 assert len(origin_children) == 1
900 assert len(origin_children) == 1
883 target = assert_response.get_element('.pr-targetinfo .tag')
901 target = assert_response.get_element('.pr-targetinfo .tag')
884 target_children = target.getchildren()
902 target_children = target.getchildren()
885 assert len(target_children) == 1
903 assert len(target_children) == 1
886
904
887 expected_origin_link = url(
905 expected_origin_link = url(
888 'changelog_home',
906 'changelog_home',
889 repo_name=pull_request.source_repo.scm_instance().name,
907 repo_name=pull_request.source_repo.scm_instance().name,
890 branch='origin')
908 branch='origin')
891 expected_target_link = url(
909 expected_target_link = url(
892 'changelog_home',
910 'changelog_home',
893 repo_name=pull_request.target_repo.scm_instance().name,
911 repo_name=pull_request.target_repo.scm_instance().name,
894 branch='target')
912 branch='target')
895 assert origin_children[0].attrib['href'] == expected_origin_link
913 assert origin_children[0].attrib['href'] == expected_origin_link
896 assert origin_children[0].text == 'branch: origin'
914 assert origin_children[0].text == 'branch: origin'
897 assert target_children[0].attrib['href'] == expected_target_link
915 assert target_children[0].attrib['href'] == expected_target_link
898 assert target_children[0].text == 'branch: target'
916 assert target_children[0].text == 'branch: target'
899
917
900 def test_bookmark_is_not_a_link(self, pr_util):
918 def test_bookmark_is_not_a_link(self, pr_util):
901 pull_request = pr_util.create_pull_request()
919 pull_request = pr_util.create_pull_request()
902 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
920 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
903 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
921 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
904 Session().add(pull_request)
922 Session().add(pull_request)
905 Session().commit()
923 Session().commit()
906
924
907 response = self.app.get(url(
925 response = self.app.get(url(
908 controller='pullrequests', action='show',
926 controller='pullrequests', action='show',
909 repo_name=pull_request.target_repo.scm_instance().name,
927 repo_name=pull_request.target_repo.scm_instance().name,
910 pull_request_id=str(pull_request.pull_request_id)))
928 pull_request_id=str(pull_request.pull_request_id)))
911 assert response.status_int == 200
929 assert response.status_int == 200
912 assert_response = AssertResponse(response)
930 assert_response = AssertResponse(response)
913
931
914 origin = assert_response.get_element('.pr-origininfo .tag')
932 origin = assert_response.get_element('.pr-origininfo .tag')
915 assert origin.text.strip() == 'bookmark: origin'
933 assert origin.text.strip() == 'bookmark: origin'
916 assert origin.getchildren() == []
934 assert origin.getchildren() == []
917
935
918 target = assert_response.get_element('.pr-targetinfo .tag')
936 target = assert_response.get_element('.pr-targetinfo .tag')
919 assert target.text.strip() == 'bookmark: target'
937 assert target.text.strip() == 'bookmark: target'
920 assert target.getchildren() == []
938 assert target.getchildren() == []
921
939
922 def test_tag_is_not_a_link(self, pr_util):
940 def test_tag_is_not_a_link(self, pr_util):
923 pull_request = pr_util.create_pull_request()
941 pull_request = pr_util.create_pull_request()
924 pull_request.source_ref = 'tag:origin:1234567890abcdef'
942 pull_request.source_ref = 'tag:origin:1234567890abcdef'
925 pull_request.target_ref = 'tag:target:abcdef1234567890'
943 pull_request.target_ref = 'tag:target:abcdef1234567890'
926 Session().add(pull_request)
944 Session().add(pull_request)
927 Session().commit()
945 Session().commit()
928
946
929 response = self.app.get(url(
947 response = self.app.get(url(
930 controller='pullrequests', action='show',
948 controller='pullrequests', action='show',
931 repo_name=pull_request.target_repo.scm_instance().name,
949 repo_name=pull_request.target_repo.scm_instance().name,
932 pull_request_id=str(pull_request.pull_request_id)))
950 pull_request_id=str(pull_request.pull_request_id)))
933 assert response.status_int == 200
951 assert response.status_int == 200
934 assert_response = AssertResponse(response)
952 assert_response = AssertResponse(response)
935
953
936 origin = assert_response.get_element('.pr-origininfo .tag')
954 origin = assert_response.get_element('.pr-origininfo .tag')
937 assert origin.text.strip() == 'tag: origin'
955 assert origin.text.strip() == 'tag: origin'
938 assert origin.getchildren() == []
956 assert origin.getchildren() == []
939
957
940 target = assert_response.get_element('.pr-targetinfo .tag')
958 target = assert_response.get_element('.pr-targetinfo .tag')
941 assert target.text.strip() == 'tag: target'
959 assert target.text.strip() == 'tag: target'
942 assert target.getchildren() == []
960 assert target.getchildren() == []
943
961
944 def test_description_is_escaped_on_index_page(self, backend, pr_util):
962 def test_description_is_escaped_on_index_page(self, backend, pr_util):
945 xss_description = "<script>alert('Hi!')</script>"
963 xss_description = "<script>alert('Hi!')</script>"
946 pull_request = pr_util.create_pull_request(description=xss_description)
964 pull_request = pr_util.create_pull_request(description=xss_description)
947 response = self.app.get(url(
965 response = self.app.get(url(
948 controller='pullrequests', action='show_all',
966 controller='pullrequests', action='show_all',
949 repo_name=pull_request.target_repo.repo_name))
967 repo_name=pull_request.target_repo.repo_name))
950 response.mustcontain(
968 response.mustcontain(
951 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
969 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
952
970
953 @pytest.mark.parametrize('mergeable', [True, False])
971 @pytest.mark.parametrize('mergeable', [True, False])
954 def test_shadow_repository_link(
972 def test_shadow_repository_link(
955 self, mergeable, pr_util, http_host_stub):
973 self, mergeable, pr_util, http_host_stub):
956 """
974 """
957 Check that the pull request summary page displays a link to the shadow
975 Check that the pull request summary page displays a link to the shadow
958 repository if the pull request is mergeable. If it is not mergeable
976 repository if the pull request is mergeable. If it is not mergeable
959 the link should not be displayed.
977 the link should not be displayed.
960 """
978 """
961 pull_request = pr_util.create_pull_request(
979 pull_request = pr_util.create_pull_request(
962 mergeable=mergeable, enable_notifications=False)
980 mergeable=mergeable, enable_notifications=False)
963 target_repo = pull_request.target_repo.scm_instance()
981 target_repo = pull_request.target_repo.scm_instance()
964 pr_id = pull_request.pull_request_id
982 pr_id = pull_request.pull_request_id
965 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
983 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
966 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
984 host=http_host_stub, repo=target_repo.name, pr_id=pr_id)
967
985
968 response = self.app.get(url(
986 response = self.app.get(url(
969 controller='pullrequests', action='show',
987 controller='pullrequests', action='show',
970 repo_name=target_repo.name,
988 repo_name=target_repo.name,
971 pull_request_id=str(pr_id)))
989 pull_request_id=str(pr_id)))
972
990
973 assertr = AssertResponse(response)
991 assertr = AssertResponse(response)
974 if mergeable:
992 if mergeable:
975 assertr.element_value_contains(
993 assertr.element_value_contains(
976 'div.pr-mergeinfo input', shadow_url)
994 'div.pr-mergeinfo input', shadow_url)
977 assertr.element_value_contains(
995 assertr.element_value_contains(
978 'div.pr-mergeinfo input', 'pr-merge')
996 'div.pr-mergeinfo input', 'pr-merge')
979 else:
997 else:
980 assertr.no_element_exists('div.pr-mergeinfo')
998 assertr.no_element_exists('div.pr-mergeinfo')
981
999
982
1000
983 @pytest.mark.usefixtures('app')
1001 @pytest.mark.usefixtures('app')
984 @pytest.mark.backends("git", "hg")
1002 @pytest.mark.backends("git", "hg")
985 class TestPullrequestsControllerDelete(object):
1003 class TestPullrequestsControllerDelete(object):
986 def test_pull_request_delete_button_permissions_admin(
1004 def test_pull_request_delete_button_permissions_admin(
987 self, autologin_user, user_admin, pr_util):
1005 self, autologin_user, user_admin, pr_util):
988 pull_request = pr_util.create_pull_request(
1006 pull_request = pr_util.create_pull_request(
989 author=user_admin.username, enable_notifications=False)
1007 author=user_admin.username, enable_notifications=False)
990
1008
991 response = self.app.get(url(
1009 response = self.app.get(url(
992 controller='pullrequests', action='show',
1010 controller='pullrequests', action='show',
993 repo_name=pull_request.target_repo.scm_instance().name,
1011 repo_name=pull_request.target_repo.scm_instance().name,
994 pull_request_id=str(pull_request.pull_request_id)))
1012 pull_request_id=str(pull_request.pull_request_id)))
995
1013
996 response.mustcontain('id="delete_pullrequest"')
1014 response.mustcontain('id="delete_pullrequest"')
997 response.mustcontain('Confirm to delete this pull request')
1015 response.mustcontain('Confirm to delete this pull request')
998
1016
999 def test_pull_request_delete_button_permissions_owner(
1017 def test_pull_request_delete_button_permissions_owner(
1000 self, autologin_regular_user, user_regular, pr_util):
1018 self, autologin_regular_user, user_regular, pr_util):
1001 pull_request = pr_util.create_pull_request(
1019 pull_request = pr_util.create_pull_request(
1002 author=user_regular.username, enable_notifications=False)
1020 author=user_regular.username, enable_notifications=False)
1003
1021
1004 response = self.app.get(url(
1022 response = self.app.get(url(
1005 controller='pullrequests', action='show',
1023 controller='pullrequests', action='show',
1006 repo_name=pull_request.target_repo.scm_instance().name,
1024 repo_name=pull_request.target_repo.scm_instance().name,
1007 pull_request_id=str(pull_request.pull_request_id)))
1025 pull_request_id=str(pull_request.pull_request_id)))
1008
1026
1009 response.mustcontain('id="delete_pullrequest"')
1027 response.mustcontain('id="delete_pullrequest"')
1010 response.mustcontain('Confirm to delete this pull request')
1028 response.mustcontain('Confirm to delete this pull request')
1011
1029
1012 def test_pull_request_delete_button_permissions_forbidden(
1030 def test_pull_request_delete_button_permissions_forbidden(
1013 self, autologin_regular_user, user_regular, user_admin, pr_util):
1031 self, autologin_regular_user, user_regular, user_admin, pr_util):
1014 pull_request = pr_util.create_pull_request(
1032 pull_request = pr_util.create_pull_request(
1015 author=user_admin.username, enable_notifications=False)
1033 author=user_admin.username, enable_notifications=False)
1016
1034
1017 response = self.app.get(url(
1035 response = self.app.get(url(
1018 controller='pullrequests', action='show',
1036 controller='pullrequests', action='show',
1019 repo_name=pull_request.target_repo.scm_instance().name,
1037 repo_name=pull_request.target_repo.scm_instance().name,
1020 pull_request_id=str(pull_request.pull_request_id)))
1038 pull_request_id=str(pull_request.pull_request_id)))
1021 response.mustcontain(no=['id="delete_pullrequest"'])
1039 response.mustcontain(no=['id="delete_pullrequest"'])
1022 response.mustcontain(no=['Confirm to delete this pull request'])
1040 response.mustcontain(no=['Confirm to delete this pull request'])
1023
1041
1024 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1042 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1025 self, autologin_regular_user, user_regular, user_admin, pr_util,
1043 self, autologin_regular_user, user_regular, user_admin, pr_util,
1026 user_util):
1044 user_util):
1027
1045
1028 pull_request = pr_util.create_pull_request(
1046 pull_request = pr_util.create_pull_request(
1029 author=user_admin.username, enable_notifications=False)
1047 author=user_admin.username, enable_notifications=False)
1030
1048
1031 user_util.grant_user_permission_to_repo(
1049 user_util.grant_user_permission_to_repo(
1032 pull_request.target_repo, user_regular,
1050 pull_request.target_repo, user_regular,
1033 'repository.write')
1051 'repository.write')
1034
1052
1035 response = self.app.get(url(
1053 response = self.app.get(url(
1036 controller='pullrequests', action='show',
1054 controller='pullrequests', action='show',
1037 repo_name=pull_request.target_repo.scm_instance().name,
1055 repo_name=pull_request.target_repo.scm_instance().name,
1038 pull_request_id=str(pull_request.pull_request_id)))
1056 pull_request_id=str(pull_request.pull_request_id)))
1039
1057
1040 response.mustcontain('id="open_edit_pullrequest"')
1058 response.mustcontain('id="open_edit_pullrequest"')
1041 response.mustcontain('id="delete_pullrequest"')
1059 response.mustcontain('id="delete_pullrequest"')
1042 response.mustcontain(no=['Confirm to delete this pull request'])
1060 response.mustcontain(no=['Confirm to delete this pull request'])
1043
1061
1044
1062
1045 def assert_pull_request_status(pull_request, expected_status):
1063 def assert_pull_request_status(pull_request, expected_status):
1046 status = ChangesetStatusModel().calculated_review_status(
1064 status = ChangesetStatusModel().calculated_review_status(
1047 pull_request=pull_request)
1065 pull_request=pull_request)
1048 assert status == expected_status
1066 assert status == expected_status
1049
1067
1050
1068
1051 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1069 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1052 @pytest.mark.usefixtures("autologin_user")
1070 @pytest.mark.usefixtures("autologin_user")
1053 def test_redirects_to_repo_summary_for_svn_repositories(
1071 def test_redirects_to_repo_summary_for_svn_repositories(
1054 backend_svn, app, action):
1072 backend_svn, app, action):
1055 denied_actions = ['show_all', 'index', 'create']
1073 denied_actions = ['show_all', 'index', 'create']
1056 for action in denied_actions:
1074 for action in denied_actions:
1057 response = app.get(url(
1075 response = app.get(url(
1058 controller='pullrequests', action=action,
1076 controller='pullrequests', action=action,
1059 repo_name=backend_svn.repo_name))
1077 repo_name=backend_svn.repo_name))
1060 assert response.status_int == 302
1078 assert response.status_int == 302
1061
1079
1062 # Not allowed, redirect to the summary
1080 # Not allowed, redirect to the summary
1063 redirected = response.follow()
1081 redirected = response.follow()
1064 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1082 summary_url = url('summary_home', repo_name=backend_svn.repo_name)
1065
1083
1066 # URL adds leading slash and path doesn't have it
1084 # URL adds leading slash and path doesn't have it
1067 assert redirected.req.path == summary_url
1085 assert redirected.req.path == summary_url
1068
1086
1069
1087
1070 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1088 def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp):
1071 # TODO: johbo: Global import not possible because models.forms blows up
1089 # TODO: johbo: Global import not possible because models.forms blows up
1072 from rhodecode.controllers.pullrequests import PullrequestsController
1090 from rhodecode.controllers.pullrequests import PullrequestsController
1073 controller = PullrequestsController()
1091 controller = PullrequestsController()
1074 patcher = mock.patch(
1092 patcher = mock.patch(
1075 'rhodecode.model.db.BaseModel.get', return_value=None)
1093 'rhodecode.model.db.BaseModel.get', return_value=None)
1076 with pytest.raises(HTTPNotFound), patcher:
1094 with pytest.raises(HTTPNotFound), patcher:
1077 controller._delete_comment(1)
1095 controller._delete_comment(1)
General Comments 0
You need to be logged in to leave comments. Login now