##// END OF EJS Templates
pr: Use new update response object from pr model. #3950
Martin Bornhold -
r1076:ab17456b default
parent child Browse files
Show More
@@ -1,896 +1,890 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24
24
25 import peppercorn
25 import peppercorn
26 import formencode
26 import formencode
27 import logging
27 import logging
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
35 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib import auth, diffs, helpers as h
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
41 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.utils import jsonify
46 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 from rhodecode.lib.vcs.exceptions import (
49 from rhodecode.lib.vcs.exceptions import (
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
51 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.lib.diffs import LimitedDiffContainer
52 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.comment import ChangesetCommentsModel
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
55 Repository
55 Repository
56 from rhodecode.model.forms import PullRequestForm
56 from rhodecode.model.forms import PullRequestForm
57 from rhodecode.model.meta import Session
57 from rhodecode.model.meta import Session
58 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.pull_request import PullRequestModel
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class PullrequestsController(BaseRepoController):
63 class PullrequestsController(BaseRepoController):
64 def __before__(self):
64 def __before__(self):
65 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
66
66
67 def _load_compare_data(self, pull_request, enable_comments=True):
67 def _load_compare_data(self, pull_request, enable_comments=True):
68 """
68 """
69 Load context data needed for generating compare diff
69 Load context data needed for generating compare diff
70
70
71 :param pull_request: object related to the request
71 :param pull_request: object related to the request
72 :param enable_comments: flag to determine if comments are included
72 :param enable_comments: flag to determine if comments are included
73 """
73 """
74 source_repo = pull_request.source_repo
74 source_repo = pull_request.source_repo
75 source_ref_id = pull_request.source_ref_parts.commit_id
75 source_ref_id = pull_request.source_ref_parts.commit_id
76
76
77 target_repo = pull_request.target_repo
77 target_repo = pull_request.target_repo
78 target_ref_id = pull_request.target_ref_parts.commit_id
78 target_ref_id = pull_request.target_ref_parts.commit_id
79
79
80 # despite opening commits for bookmarks/branches/tags, we always
80 # despite opening commits for bookmarks/branches/tags, we always
81 # convert this to rev to prevent changes after bookmark or branch change
81 # convert this to rev to prevent changes after bookmark or branch change
82 c.source_ref_type = 'rev'
82 c.source_ref_type = 'rev'
83 c.source_ref = source_ref_id
83 c.source_ref = source_ref_id
84
84
85 c.target_ref_type = 'rev'
85 c.target_ref_type = 'rev'
86 c.target_ref = target_ref_id
86 c.target_ref = target_ref_id
87
87
88 c.source_repo = source_repo
88 c.source_repo = source_repo
89 c.target_repo = target_repo
89 c.target_repo = target_repo
90
90
91 c.fulldiff = bool(request.GET.get('fulldiff'))
91 c.fulldiff = bool(request.GET.get('fulldiff'))
92
92
93 # diff_limit is the old behavior, will cut off the whole diff
93 # diff_limit is the old behavior, will cut off the whole diff
94 # if the limit is applied otherwise will just hide the
94 # if the limit is applied otherwise will just hide the
95 # big files from the front-end
95 # big files from the front-end
96 diff_limit = self.cut_off_limit_diff
96 diff_limit = self.cut_off_limit_diff
97 file_limit = self.cut_off_limit_file
97 file_limit = self.cut_off_limit_file
98
98
99 pre_load = ["author", "branch", "date", "message"]
99 pre_load = ["author", "branch", "date", "message"]
100
100
101 c.commit_ranges = []
101 c.commit_ranges = []
102 source_commit = EmptyCommit()
102 source_commit = EmptyCommit()
103 target_commit = EmptyCommit()
103 target_commit = EmptyCommit()
104 c.missing_requirements = False
104 c.missing_requirements = False
105 try:
105 try:
106 c.commit_ranges = [
106 c.commit_ranges = [
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
108 for rev in pull_request.revisions]
108 for rev in pull_request.revisions]
109
109
110 c.statuses = source_repo.statuses(
110 c.statuses = source_repo.statuses(
111 [x.raw_id for x in c.commit_ranges])
111 [x.raw_id for x in c.commit_ranges])
112
112
113 target_commit = source_repo.get_commit(
113 target_commit = source_repo.get_commit(
114 commit_id=safe_str(target_ref_id))
114 commit_id=safe_str(target_ref_id))
115 source_commit = source_repo.get_commit(
115 source_commit = source_repo.get_commit(
116 commit_id=safe_str(source_ref_id))
116 commit_id=safe_str(source_ref_id))
117 except RepositoryRequirementError:
117 except RepositoryRequirementError:
118 c.missing_requirements = True
118 c.missing_requirements = True
119
119
120 c.missing_commits = False
120 c.missing_commits = False
121 if (c.missing_requirements or
121 if (c.missing_requirements or
122 isinstance(source_commit, EmptyCommit) or
122 isinstance(source_commit, EmptyCommit) or
123 source_commit == target_commit):
123 source_commit == target_commit):
124 _parsed = []
124 _parsed = []
125 c.missing_commits = True
125 c.missing_commits = True
126 else:
126 else:
127 vcs_diff = PullRequestModel().get_diff(pull_request)
127 vcs_diff = PullRequestModel().get_diff(pull_request)
128 diff_processor = diffs.DiffProcessor(
128 diff_processor = diffs.DiffProcessor(
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
130 file_limit=file_limit, show_full_diff=c.fulldiff)
130 file_limit=file_limit, show_full_diff=c.fulldiff)
131 _parsed = diff_processor.prepare()
131 _parsed = diff_processor.prepare()
132
132
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
134
134
135 c.files = []
135 c.files = []
136 c.changes = {}
136 c.changes = {}
137 c.lines_added = 0
137 c.lines_added = 0
138 c.lines_deleted = 0
138 c.lines_deleted = 0
139 c.included_files = []
139 c.included_files = []
140 c.deleted_files = []
140 c.deleted_files = []
141
141
142 for f in _parsed:
142 for f in _parsed:
143 st = f['stats']
143 st = f['stats']
144 c.lines_added += st['added']
144 c.lines_added += st['added']
145 c.lines_deleted += st['deleted']
145 c.lines_deleted += st['deleted']
146
146
147 fid = h.FID('', f['filename'])
147 fid = h.FID('', f['filename'])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
149 c.included_files.append(f['filename'])
149 c.included_files.append(f['filename'])
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
151 parsed_lines=[f])
151 parsed_lines=[f])
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
153
153
154 def _extract_ordering(self, request):
154 def _extract_ordering(self, request):
155 column_index = safe_int(request.GET.get('order[0][column]'))
155 column_index = safe_int(request.GET.get('order[0][column]'))
156 order_dir = request.GET.get('order[0][dir]', 'desc')
156 order_dir = request.GET.get('order[0][dir]', 'desc')
157 order_by = request.GET.get(
157 order_by = request.GET.get(
158 'columns[%s][data][sort]' % column_index, 'name_raw')
158 'columns[%s][data][sort]' % column_index, 'name_raw')
159 return order_by, order_dir
159 return order_by, order_dir
160
160
161 @LoginRequired()
161 @LoginRequired()
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 'repository.admin')
163 'repository.admin')
164 @HasAcceptedRepoType('git', 'hg')
164 @HasAcceptedRepoType('git', 'hg')
165 def show_all(self, repo_name):
165 def show_all(self, repo_name):
166 # filter types
166 # filter types
167 c.active = 'open'
167 c.active = 'open'
168 c.source = str2bool(request.GET.get('source'))
168 c.source = str2bool(request.GET.get('source'))
169 c.closed = str2bool(request.GET.get('closed'))
169 c.closed = str2bool(request.GET.get('closed'))
170 c.my = str2bool(request.GET.get('my'))
170 c.my = str2bool(request.GET.get('my'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
173 c.repo_name = repo_name
173 c.repo_name = repo_name
174
174
175 opened_by = None
175 opened_by = None
176 if c.my:
176 if c.my:
177 c.active = 'my'
177 c.active = 'my'
178 opened_by = [c.rhodecode_user.user_id]
178 opened_by = [c.rhodecode_user.user_id]
179
179
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 if c.closed:
181 if c.closed:
182 c.active = 'closed'
182 c.active = 'closed'
183 statuses = [PullRequest.STATUS_CLOSED]
183 statuses = [PullRequest.STATUS_CLOSED]
184
184
185 if c.awaiting_review and not c.source:
185 if c.awaiting_review and not c.source:
186 c.active = 'awaiting'
186 c.active = 'awaiting'
187 if c.source and not c.awaiting_review:
187 if c.source and not c.awaiting_review:
188 c.active = 'source'
188 c.active = 'source'
189 if c.awaiting_my_review:
189 if c.awaiting_my_review:
190 c.active = 'awaiting_my'
190 c.active = 'awaiting_my'
191
191
192 data = self._get_pull_requests_list(
192 data = self._get_pull_requests_list(
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
194 if not request.is_xhr:
194 if not request.is_xhr:
195 c.data = json.dumps(data['data'])
195 c.data = json.dumps(data['data'])
196 c.records_total = data['recordsTotal']
196 c.records_total = data['recordsTotal']
197 return render('/pullrequests/pullrequests.html')
197 return render('/pullrequests/pullrequests.html')
198 else:
198 else:
199 return json.dumps(data)
199 return json.dumps(data)
200
200
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
202 # pagination
202 # pagination
203 start = safe_int(request.GET.get('start'), 0)
203 start = safe_int(request.GET.get('start'), 0)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
205 order_by, order_dir = self._extract_ordering(request)
205 order_by, order_dir = self._extract_ordering(request)
206
206
207 if c.awaiting_review:
207 if c.awaiting_review:
208 pull_requests = PullRequestModel().get_awaiting_review(
208 pull_requests = PullRequestModel().get_awaiting_review(
209 repo_name, source=c.source, opened_by=opened_by,
209 repo_name, source=c.source, opened_by=opened_by,
210 statuses=statuses, offset=start, length=length,
210 statuses=statuses, offset=start, length=length,
211 order_by=order_by, order_dir=order_dir)
211 order_by=order_by, order_dir=order_dir)
212 pull_requests_total_count = PullRequestModel(
212 pull_requests_total_count = PullRequestModel(
213 ).count_awaiting_review(
213 ).count_awaiting_review(
214 repo_name, source=c.source, statuses=statuses,
214 repo_name, source=c.source, statuses=statuses,
215 opened_by=opened_by)
215 opened_by=opened_by)
216 elif c.awaiting_my_review:
216 elif c.awaiting_my_review:
217 pull_requests = PullRequestModel().get_awaiting_my_review(
217 pull_requests = PullRequestModel().get_awaiting_my_review(
218 repo_name, source=c.source, opened_by=opened_by,
218 repo_name, source=c.source, opened_by=opened_by,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
220 offset=start, length=length, order_by=order_by,
220 offset=start, length=length, order_by=order_by,
221 order_dir=order_dir)
221 order_dir=order_dir)
222 pull_requests_total_count = PullRequestModel(
222 pull_requests_total_count = PullRequestModel(
223 ).count_awaiting_my_review(
223 ).count_awaiting_my_review(
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
225 statuses=statuses, opened_by=opened_by)
225 statuses=statuses, opened_by=opened_by)
226 else:
226 else:
227 pull_requests = PullRequestModel().get_all(
227 pull_requests = PullRequestModel().get_all(
228 repo_name, source=c.source, opened_by=opened_by,
228 repo_name, source=c.source, opened_by=opened_by,
229 statuses=statuses, offset=start, length=length,
229 statuses=statuses, offset=start, length=length,
230 order_by=order_by, order_dir=order_dir)
230 order_by=order_by, order_dir=order_dir)
231 pull_requests_total_count = PullRequestModel().count_all(
231 pull_requests_total_count = PullRequestModel().count_all(
232 repo_name, source=c.source, statuses=statuses,
232 repo_name, source=c.source, statuses=statuses,
233 opened_by=opened_by)
233 opened_by=opened_by)
234
234
235 from rhodecode.lib.utils import PartialRenderer
235 from rhodecode.lib.utils import PartialRenderer
236 _render = PartialRenderer('data_table/_dt_elements.html')
236 _render = PartialRenderer('data_table/_dt_elements.html')
237 data = []
237 data = []
238 for pr in pull_requests:
238 for pr in pull_requests:
239 comments = ChangesetCommentsModel().get_all_comments(
239 comments = ChangesetCommentsModel().get_all_comments(
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
241
241
242 data.append({
242 data.append({
243 'name': _render('pullrequest_name',
243 'name': _render('pullrequest_name',
244 pr.pull_request_id, pr.target_repo.repo_name),
244 pr.pull_request_id, pr.target_repo.repo_name),
245 'name_raw': pr.pull_request_id,
245 'name_raw': pr.pull_request_id,
246 'status': _render('pullrequest_status',
246 'status': _render('pullrequest_status',
247 pr.calculated_review_status()),
247 pr.calculated_review_status()),
248 'title': _render(
248 'title': _render(
249 'pullrequest_title', pr.title, pr.description),
249 'pullrequest_title', pr.title, pr.description),
250 'description': h.escape(pr.description),
250 'description': h.escape(pr.description),
251 'updated_on': _render('pullrequest_updated_on',
251 'updated_on': _render('pullrequest_updated_on',
252 h.datetime_to_time(pr.updated_on)),
252 h.datetime_to_time(pr.updated_on)),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
254 'created_on': _render('pullrequest_updated_on',
254 'created_on': _render('pullrequest_updated_on',
255 h.datetime_to_time(pr.created_on)),
255 h.datetime_to_time(pr.created_on)),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
257 'author': _render('pullrequest_author',
257 'author': _render('pullrequest_author',
258 pr.author.full_contact, ),
258 pr.author.full_contact, ),
259 'author_raw': pr.author.full_name,
259 'author_raw': pr.author.full_name,
260 'comments': _render('pullrequest_comments', len(comments)),
260 'comments': _render('pullrequest_comments', len(comments)),
261 'comments_raw': len(comments),
261 'comments_raw': len(comments),
262 'closed': pr.is_closed(),
262 'closed': pr.is_closed(),
263 })
263 })
264 # json used to render the grid
264 # json used to render the grid
265 data = ({
265 data = ({
266 'data': data,
266 'data': data,
267 'recordsTotal': pull_requests_total_count,
267 'recordsTotal': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
269 })
269 })
270 return data
270 return data
271
271
272 @LoginRequired()
272 @LoginRequired()
273 @NotAnonymous()
273 @NotAnonymous()
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 'repository.admin')
275 'repository.admin')
276 @HasAcceptedRepoType('git', 'hg')
276 @HasAcceptedRepoType('git', 'hg')
277 def index(self):
277 def index(self):
278 source_repo = c.rhodecode_db_repo
278 source_repo = c.rhodecode_db_repo
279
279
280 try:
280 try:
281 source_repo.scm_instance().get_commit()
281 source_repo.scm_instance().get_commit()
282 except EmptyRepositoryError:
282 except EmptyRepositoryError:
283 h.flash(h.literal(_('There are no commits yet')),
283 h.flash(h.literal(_('There are no commits yet')),
284 category='warning')
284 category='warning')
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
286
286
287 commit_id = request.GET.get('commit')
287 commit_id = request.GET.get('commit')
288 branch_ref = request.GET.get('branch')
288 branch_ref = request.GET.get('branch')
289 bookmark_ref = request.GET.get('bookmark')
289 bookmark_ref = request.GET.get('bookmark')
290
290
291 try:
291 try:
292 source_repo_data = PullRequestModel().generate_repo_data(
292 source_repo_data = PullRequestModel().generate_repo_data(
293 source_repo, commit_id=commit_id,
293 source_repo, commit_id=commit_id,
294 branch=branch_ref, bookmark=bookmark_ref)
294 branch=branch_ref, bookmark=bookmark_ref)
295 except CommitDoesNotExistError as e:
295 except CommitDoesNotExistError as e:
296 log.exception(e)
296 log.exception(e)
297 h.flash(_('Commit does not exist'), 'error')
297 h.flash(_('Commit does not exist'), 'error')
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
299
299
300 default_target_repo = source_repo
300 default_target_repo = source_repo
301
301
302 if source_repo.parent:
302 if source_repo.parent:
303 parent_vcs_obj = source_repo.parent.scm_instance()
303 parent_vcs_obj = source_repo.parent.scm_instance()
304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
305 # change default if we have a parent repo
305 # change default if we have a parent repo
306 default_target_repo = source_repo.parent
306 default_target_repo = source_repo.parent
307
307
308 target_repo_data = PullRequestModel().generate_repo_data(
308 target_repo_data = PullRequestModel().generate_repo_data(
309 default_target_repo)
309 default_target_repo)
310
310
311 selected_source_ref = source_repo_data['refs']['selected_ref']
311 selected_source_ref = source_repo_data['refs']['selected_ref']
312
312
313 title_source_ref = selected_source_ref.split(':', 2)[1]
313 title_source_ref = selected_source_ref.split(':', 2)[1]
314 c.default_title = PullRequestModel().generate_pullrequest_title(
314 c.default_title = PullRequestModel().generate_pullrequest_title(
315 source=source_repo.repo_name,
315 source=source_repo.repo_name,
316 source_ref=title_source_ref,
316 source_ref=title_source_ref,
317 target=default_target_repo.repo_name
317 target=default_target_repo.repo_name
318 )
318 )
319
319
320 c.default_repo_data = {
320 c.default_repo_data = {
321 'source_repo_name': source_repo.repo_name,
321 'source_repo_name': source_repo.repo_name,
322 'source_refs_json': json.dumps(source_repo_data),
322 'source_refs_json': json.dumps(source_repo_data),
323 'target_repo_name': default_target_repo.repo_name,
323 'target_repo_name': default_target_repo.repo_name,
324 'target_refs_json': json.dumps(target_repo_data),
324 'target_refs_json': json.dumps(target_repo_data),
325 }
325 }
326 c.default_source_ref = selected_source_ref
326 c.default_source_ref = selected_source_ref
327
327
328 return render('/pullrequests/pullrequest.html')
328 return render('/pullrequests/pullrequest.html')
329
329
330 @LoginRequired()
330 @LoginRequired()
331 @NotAnonymous()
331 @NotAnonymous()
332 @XHRRequired()
332 @XHRRequired()
333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 'repository.admin')
334 'repository.admin')
335 @jsonify
335 @jsonify
336 def get_repo_refs(self, repo_name, target_repo_name):
336 def get_repo_refs(self, repo_name, target_repo_name):
337 repo = Repository.get_by_repo_name(target_repo_name)
337 repo = Repository.get_by_repo_name(target_repo_name)
338 if not repo:
338 if not repo:
339 raise HTTPNotFound
339 raise HTTPNotFound
340 return PullRequestModel().generate_repo_data(repo)
340 return PullRequestModel().generate_repo_data(repo)
341
341
342 @LoginRequired()
342 @LoginRequired()
343 @NotAnonymous()
343 @NotAnonymous()
344 @XHRRequired()
344 @XHRRequired()
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 'repository.admin')
346 'repository.admin')
347 @jsonify
347 @jsonify
348 def get_repo_destinations(self, repo_name):
348 def get_repo_destinations(self, repo_name):
349 repo = Repository.get_by_repo_name(repo_name)
349 repo = Repository.get_by_repo_name(repo_name)
350 if not repo:
350 if not repo:
351 raise HTTPNotFound
351 raise HTTPNotFound
352 filter_query = request.GET.get('query')
352 filter_query = request.GET.get('query')
353
353
354 query = Repository.query() \
354 query = Repository.query() \
355 .order_by(func.length(Repository.repo_name)) \
355 .order_by(func.length(Repository.repo_name)) \
356 .filter(or_(
356 .filter(or_(
357 Repository.repo_name == repo.repo_name,
357 Repository.repo_name == repo.repo_name,
358 Repository.fork_id == repo.repo_id))
358 Repository.fork_id == repo.repo_id))
359
359
360 if filter_query:
360 if filter_query:
361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
362 query = query.filter(
362 query = query.filter(
363 Repository.repo_name.ilike(ilike_expression))
363 Repository.repo_name.ilike(ilike_expression))
364
364
365 add_parent = False
365 add_parent = False
366 if repo.parent:
366 if repo.parent:
367 if filter_query in repo.parent.repo_name:
367 if filter_query in repo.parent.repo_name:
368 parent_vcs_obj = repo.parent.scm_instance()
368 parent_vcs_obj = repo.parent.scm_instance()
369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
370 add_parent = True
370 add_parent = True
371
371
372 limit = 20 - 1 if add_parent else 20
372 limit = 20 - 1 if add_parent else 20
373 all_repos = query.limit(limit).all()
373 all_repos = query.limit(limit).all()
374 if add_parent:
374 if add_parent:
375 all_repos += [repo.parent]
375 all_repos += [repo.parent]
376
376
377 repos = []
377 repos = []
378 for obj in self.scm_model.get_repos(all_repos):
378 for obj in self.scm_model.get_repos(all_repos):
379 repos.append({
379 repos.append({
380 'id': obj['name'],
380 'id': obj['name'],
381 'text': obj['name'],
381 'text': obj['name'],
382 'type': 'repo',
382 'type': 'repo',
383 'obj': obj['dbrepo']
383 'obj': obj['dbrepo']
384 })
384 })
385
385
386 data = {
386 data = {
387 'more': False,
387 'more': False,
388 'results': [{
388 'results': [{
389 'text': _('Repositories'),
389 'text': _('Repositories'),
390 'children': repos
390 'children': repos
391 }] if repos else []
391 }] if repos else []
392 }
392 }
393 return data
393 return data
394
394
395 @LoginRequired()
395 @LoginRequired()
396 @NotAnonymous()
396 @NotAnonymous()
397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 'repository.admin')
398 'repository.admin')
399 @HasAcceptedRepoType('git', 'hg')
399 @HasAcceptedRepoType('git', 'hg')
400 @auth.CSRFRequired()
400 @auth.CSRFRequired()
401 def create(self, repo_name):
401 def create(self, repo_name):
402 repo = Repository.get_by_repo_name(repo_name)
402 repo = Repository.get_by_repo_name(repo_name)
403 if not repo:
403 if not repo:
404 raise HTTPNotFound
404 raise HTTPNotFound
405
405
406 controls = peppercorn.parse(request.POST.items())
406 controls = peppercorn.parse(request.POST.items())
407
407
408 try:
408 try:
409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
410 except formencode.Invalid as errors:
410 except formencode.Invalid as errors:
411 if errors.error_dict.get('revisions'):
411 if errors.error_dict.get('revisions'):
412 msg = 'Revisions: %s' % errors.error_dict['revisions']
412 msg = 'Revisions: %s' % errors.error_dict['revisions']
413 elif errors.error_dict.get('pullrequest_title'):
413 elif errors.error_dict.get('pullrequest_title'):
414 msg = _('Pull request requires a title with min. 3 chars')
414 msg = _('Pull request requires a title with min. 3 chars')
415 else:
415 else:
416 msg = _('Error creating pull request: {}').format(errors)
416 msg = _('Error creating pull request: {}').format(errors)
417 log.exception(msg)
417 log.exception(msg)
418 h.flash(msg, 'error')
418 h.flash(msg, 'error')
419
419
420 # would rather just go back to form ...
420 # would rather just go back to form ...
421 return redirect(url('pullrequest_home', repo_name=repo_name))
421 return redirect(url('pullrequest_home', repo_name=repo_name))
422
422
423 source_repo = _form['source_repo']
423 source_repo = _form['source_repo']
424 source_ref = _form['source_ref']
424 source_ref = _form['source_ref']
425 target_repo = _form['target_repo']
425 target_repo = _form['target_repo']
426 target_ref = _form['target_ref']
426 target_ref = _form['target_ref']
427 commit_ids = _form['revisions'][::-1]
427 commit_ids = _form['revisions'][::-1]
428 reviewers = [
428 reviewers = [
429 (r['user_id'], r['reasons']) for r in _form['review_members']]
429 (r['user_id'], r['reasons']) for r in _form['review_members']]
430
430
431 # find the ancestor for this pr
431 # find the ancestor for this pr
432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
434
434
435 source_scm = source_db_repo.scm_instance()
435 source_scm = source_db_repo.scm_instance()
436 target_scm = target_db_repo.scm_instance()
436 target_scm = target_db_repo.scm_instance()
437
437
438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
440
440
441 ancestor = source_scm.get_common_ancestor(
441 ancestor = source_scm.get_common_ancestor(
442 source_commit.raw_id, target_commit.raw_id, target_scm)
442 source_commit.raw_id, target_commit.raw_id, target_scm)
443
443
444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
446
446
447 pullrequest_title = _form['pullrequest_title']
447 pullrequest_title = _form['pullrequest_title']
448 title_source_ref = source_ref.split(':', 2)[1]
448 title_source_ref = source_ref.split(':', 2)[1]
449 if not pullrequest_title:
449 if not pullrequest_title:
450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
451 source=source_repo,
451 source=source_repo,
452 source_ref=title_source_ref,
452 source_ref=title_source_ref,
453 target=target_repo
453 target=target_repo
454 )
454 )
455
455
456 description = _form['pullrequest_desc']
456 description = _form['pullrequest_desc']
457 try:
457 try:
458 pull_request = PullRequestModel().create(
458 pull_request = PullRequestModel().create(
459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
460 target_ref, commit_ids, reviewers, pullrequest_title,
460 target_ref, commit_ids, reviewers, pullrequest_title,
461 description
461 description
462 )
462 )
463 Session().commit()
463 Session().commit()
464 h.flash(_('Successfully opened new pull request'),
464 h.flash(_('Successfully opened new pull request'),
465 category='success')
465 category='success')
466 except Exception as e:
466 except Exception as e:
467 msg = _('Error occurred during sending pull request')
467 msg = _('Error occurred during sending pull request')
468 log.exception(msg)
468 log.exception(msg)
469 h.flash(msg, category='error')
469 h.flash(msg, category='error')
470 return redirect(url('pullrequest_home', repo_name=repo_name))
470 return redirect(url('pullrequest_home', repo_name=repo_name))
471
471
472 return redirect(url('pullrequest_show', repo_name=target_repo,
472 return redirect(url('pullrequest_show', repo_name=target_repo,
473 pull_request_id=pull_request.pull_request_id))
473 pull_request_id=pull_request.pull_request_id))
474
474
475 @LoginRequired()
475 @LoginRequired()
476 @NotAnonymous()
476 @NotAnonymous()
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
478 'repository.admin')
478 'repository.admin')
479 @auth.CSRFRequired()
479 @auth.CSRFRequired()
480 @jsonify
480 @jsonify
481 def update(self, repo_name, pull_request_id):
481 def update(self, repo_name, pull_request_id):
482 pull_request_id = safe_int(pull_request_id)
482 pull_request_id = safe_int(pull_request_id)
483 pull_request = PullRequest.get_or_404(pull_request_id)
483 pull_request = PullRequest.get_or_404(pull_request_id)
484 # only owner or admin can update it
484 # only owner or admin can update it
485 allowed_to_update = PullRequestModel().check_user_update(
485 allowed_to_update = PullRequestModel().check_user_update(
486 pull_request, c.rhodecode_user)
486 pull_request, c.rhodecode_user)
487 if allowed_to_update:
487 if allowed_to_update:
488 controls = peppercorn.parse(request.POST.items())
488 controls = peppercorn.parse(request.POST.items())
489
489
490 if 'review_members' in controls:
490 if 'review_members' in controls:
491 self._update_reviewers(
491 self._update_reviewers(
492 pull_request_id, controls['review_members'])
492 pull_request_id, controls['review_members'])
493 elif str2bool(request.POST.get('update_commits', 'false')):
493 elif str2bool(request.POST.get('update_commits', 'false')):
494 self._update_commits(pull_request)
494 self._update_commits(pull_request)
495 elif str2bool(request.POST.get('close_pull_request', 'false')):
495 elif str2bool(request.POST.get('close_pull_request', 'false')):
496 self._reject_close(pull_request)
496 self._reject_close(pull_request)
497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
498 self._edit_pull_request(pull_request)
498 self._edit_pull_request(pull_request)
499 else:
499 else:
500 raise HTTPBadRequest()
500 raise HTTPBadRequest()
501 return True
501 return True
502 raise HTTPForbidden()
502 raise HTTPForbidden()
503
503
504 def _edit_pull_request(self, pull_request):
504 def _edit_pull_request(self, pull_request):
505 try:
505 try:
506 PullRequestModel().edit(
506 PullRequestModel().edit(
507 pull_request, request.POST.get('title'),
507 pull_request, request.POST.get('title'),
508 request.POST.get('description'))
508 request.POST.get('description'))
509 except ValueError:
509 except ValueError:
510 msg = _(u'Cannot update closed pull requests.')
510 msg = _(u'Cannot update closed pull requests.')
511 h.flash(msg, category='error')
511 h.flash(msg, category='error')
512 return
512 return
513 else:
513 else:
514 Session().commit()
514 Session().commit()
515
515
516 msg = _(u'Pull request title & description updated.')
516 msg = _(u'Pull request title & description updated.')
517 h.flash(msg, category='success')
517 h.flash(msg, category='success')
518 return
518 return
519
519
520 def _update_commits(self, pull_request):
520 def _update_commits(self, pull_request):
521 try:
521 resp = PullRequestModel().update_commits(pull_request)
522 if PullRequestModel().has_valid_update_type(pull_request):
522 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
523 updated_version, changes = PullRequestModel().update_commits(
523
524 pull_request)
524 # Abort if pull request update failed.
525 if updated_version:
525 if not resp.success:
526 msg = _(
526 h.flash(msg, category='error')
527 u'Pull request updated to "{source_commit_id}" with '
527 return
528 u'{count_added} added, {count_removed} removed '
528
529 u'commits.'
529 if resp.reason == UpdateFailureReason.NONE:
530 ).format(
530 msg = _(
531 source_commit_id=pull_request.source_ref_parts.commit_id,
531 u'Pull request updated to "{source_commit_id}" with '
532 count_added=len(changes.added),
532 u'{count_added} added, {count_removed} removed commits.')
533 count_removed=len(changes.removed))
533 msg = msg.format(
534 h.flash(msg, category='success')
534 source_commit_id=pull_request.source_ref_parts.commit_id,
535 registry = get_current_registry()
535 count_added=len(resp.changes.added),
536 rhodecode_plugins = getattr(registry,
536 count_removed=len(resp.changes.removed))
537 'rhodecode_plugins', {})
537 h.flash(msg, category='success')
538 channelstream_config = rhodecode_plugins.get(
538
539 'channelstream', {})
539 registry = get_current_registry()
540 if channelstream_config.get('enabled'):
540 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
541 message = msg + ' - <a onclick="' \
541 channelstream_config = rhodecode_plugins.get('channelstream', {})
542 'window.location.reload()">' \
542 if channelstream_config.get('enabled'):
543 '<strong>{}</strong></a>'.format(
543 message = msg + (
544 _('Reload page')
544 ' - <a onclick="window.location.reload()">'
545 )
545 '<strong>{}</strong></a>'.format(_('Reload page')))
546 channel = '/repo${}$/pr/{}'.format(
546 channel = '/repo${}$/pr/{}'.format(
547 pull_request.target_repo.repo_name,
547 pull_request.target_repo.repo_name,
548 pull_request.pull_request_id
548 pull_request.pull_request_id
549 )
549 )
550 payload = {
550 payload = {
551 'type': 'message',
551 'type': 'message',
552 'user': 'system',
552 'user': 'system',
553 'exclude_users': [request.user.username],
553 'exclude_users': [request.user.username],
554 'channel': channel,
554 'channel': channel,
555 'message': {
555 'message': {
556 'message': message,
556 'message': message,
557 'level': 'success',
557 'level': 'success',
558 'topic': '/notifications'
558 'topic': '/notifications'
559 }
559 }
560 }
560 }
561 channelstream_request(channelstream_config, [payload],
561 channelstream_request(
562 '/message', raise_exc=False)
562 channelstream_config, [payload], '/message',
563 else:
563 raise_exc=False)
564 h.flash(_("Nothing changed in pull request."),
564 elif resp.reason == UpdateFailureReason.NO_CHANGE:
565 category='warning')
565 # Display a warning if no update is needed.
566 else:
566 h.flash(msg, category='warning')
567 msg = _(
567 else:
568 u"Skipping update of pull request due to reference "
568 h.flash(msg, category='error')
569 u"type: {reference_type}"
570 ).format(reference_type=pull_request.source_ref_parts.type)
571 h.flash(msg, category='warning')
572 except CommitDoesNotExistError:
573 h.flash(
574 _(u'Update failed due to missing commits.'), category='error')
575
569
576 @auth.CSRFRequired()
570 @auth.CSRFRequired()
577 @LoginRequired()
571 @LoginRequired()
578 @NotAnonymous()
572 @NotAnonymous()
579 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
573 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
580 'repository.admin')
574 'repository.admin')
581 def merge(self, repo_name, pull_request_id):
575 def merge(self, repo_name, pull_request_id):
582 """
576 """
583 POST /{repo_name}/pull-request/{pull_request_id}
577 POST /{repo_name}/pull-request/{pull_request_id}
584
578
585 Merge will perform a server-side merge of the specified
579 Merge will perform a server-side merge of the specified
586 pull request, if the pull request is approved and mergeable.
580 pull request, if the pull request is approved and mergeable.
587 After succesfull merging, the pull request is automatically
581 After succesfull merging, the pull request is automatically
588 closed, with a relevant comment.
582 closed, with a relevant comment.
589 """
583 """
590 pull_request_id = safe_int(pull_request_id)
584 pull_request_id = safe_int(pull_request_id)
591 pull_request = PullRequest.get_or_404(pull_request_id)
585 pull_request = PullRequest.get_or_404(pull_request_id)
592 user = c.rhodecode_user
586 user = c.rhodecode_user
593
587
594 if self._meets_merge_pre_conditions(pull_request, user):
588 if self._meets_merge_pre_conditions(pull_request, user):
595 log.debug("Pre-conditions checked, trying to merge.")
589 log.debug("Pre-conditions checked, trying to merge.")
596 extras = vcs_operation_context(
590 extras = vcs_operation_context(
597 request.environ, repo_name=pull_request.target_repo.repo_name,
591 request.environ, repo_name=pull_request.target_repo.repo_name,
598 username=user.username, action='push',
592 username=user.username, action='push',
599 scm=pull_request.target_repo.repo_type)
593 scm=pull_request.target_repo.repo_type)
600 self._merge_pull_request(pull_request, user, extras)
594 self._merge_pull_request(pull_request, user, extras)
601
595
602 return redirect(url(
596 return redirect(url(
603 'pullrequest_show',
597 'pullrequest_show',
604 repo_name=pull_request.target_repo.repo_name,
598 repo_name=pull_request.target_repo.repo_name,
605 pull_request_id=pull_request.pull_request_id))
599 pull_request_id=pull_request.pull_request_id))
606
600
607 def _meets_merge_pre_conditions(self, pull_request, user):
601 def _meets_merge_pre_conditions(self, pull_request, user):
608 if not PullRequestModel().check_user_merge(pull_request, user):
602 if not PullRequestModel().check_user_merge(pull_request, user):
609 raise HTTPForbidden()
603 raise HTTPForbidden()
610
604
611 merge_status, msg = PullRequestModel().merge_status(pull_request)
605 merge_status, msg = PullRequestModel().merge_status(pull_request)
612 if not merge_status:
606 if not merge_status:
613 log.debug("Cannot merge, not mergeable.")
607 log.debug("Cannot merge, not mergeable.")
614 h.flash(msg, category='error')
608 h.flash(msg, category='error')
615 return False
609 return False
616
610
617 if (pull_request.calculated_review_status()
611 if (pull_request.calculated_review_status()
618 is not ChangesetStatus.STATUS_APPROVED):
612 is not ChangesetStatus.STATUS_APPROVED):
619 log.debug("Cannot merge, approval is pending.")
613 log.debug("Cannot merge, approval is pending.")
620 msg = _('Pull request reviewer approval is pending.')
614 msg = _('Pull request reviewer approval is pending.')
621 h.flash(msg, category='error')
615 h.flash(msg, category='error')
622 return False
616 return False
623 return True
617 return True
624
618
625 def _merge_pull_request(self, pull_request, user, extras):
619 def _merge_pull_request(self, pull_request, user, extras):
626 merge_resp = PullRequestModel().merge(
620 merge_resp = PullRequestModel().merge(
627 pull_request, user, extras=extras)
621 pull_request, user, extras=extras)
628
622
629 if merge_resp.executed:
623 if merge_resp.executed:
630 log.debug("The merge was successful, closing the pull request.")
624 log.debug("The merge was successful, closing the pull request.")
631 PullRequestModel().close_pull_request(
625 PullRequestModel().close_pull_request(
632 pull_request.pull_request_id, user)
626 pull_request.pull_request_id, user)
633 Session().commit()
627 Session().commit()
634 msg = _('Pull request was successfully merged and closed.')
628 msg = _('Pull request was successfully merged and closed.')
635 h.flash(msg, category='success')
629 h.flash(msg, category='success')
636 else:
630 else:
637 log.debug(
631 log.debug(
638 "The merge was not successful. Merge response: %s",
632 "The merge was not successful. Merge response: %s",
639 merge_resp)
633 merge_resp)
640 msg = PullRequestModel().merge_status_message(
634 msg = PullRequestModel().merge_status_message(
641 merge_resp.failure_reason)
635 merge_resp.failure_reason)
642 h.flash(msg, category='error')
636 h.flash(msg, category='error')
643
637
644 def _update_reviewers(self, pull_request_id, review_members):
638 def _update_reviewers(self, pull_request_id, review_members):
645 reviewers = [
639 reviewers = [
646 (int(r['user_id']), r['reasons']) for r in review_members]
640 (int(r['user_id']), r['reasons']) for r in review_members]
647 PullRequestModel().update_reviewers(pull_request_id, reviewers)
641 PullRequestModel().update_reviewers(pull_request_id, reviewers)
648 Session().commit()
642 Session().commit()
649
643
650 def _reject_close(self, pull_request):
644 def _reject_close(self, pull_request):
651 if pull_request.is_closed():
645 if pull_request.is_closed():
652 raise HTTPForbidden()
646 raise HTTPForbidden()
653
647
654 PullRequestModel().close_pull_request_with_comment(
648 PullRequestModel().close_pull_request_with_comment(
655 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
649 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
656 Session().commit()
650 Session().commit()
657
651
658 @LoginRequired()
652 @LoginRequired()
659 @NotAnonymous()
653 @NotAnonymous()
660 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
654 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
661 'repository.admin')
655 'repository.admin')
662 @auth.CSRFRequired()
656 @auth.CSRFRequired()
663 @jsonify
657 @jsonify
664 def delete(self, repo_name, pull_request_id):
658 def delete(self, repo_name, pull_request_id):
665 pull_request_id = safe_int(pull_request_id)
659 pull_request_id = safe_int(pull_request_id)
666 pull_request = PullRequest.get_or_404(pull_request_id)
660 pull_request = PullRequest.get_or_404(pull_request_id)
667 # only owner can delete it !
661 # only owner can delete it !
668 if pull_request.author.user_id == c.rhodecode_user.user_id:
662 if pull_request.author.user_id == c.rhodecode_user.user_id:
669 PullRequestModel().delete(pull_request)
663 PullRequestModel().delete(pull_request)
670 Session().commit()
664 Session().commit()
671 h.flash(_('Successfully deleted pull request'),
665 h.flash(_('Successfully deleted pull request'),
672 category='success')
666 category='success')
673 return redirect(url('my_account_pullrequests'))
667 return redirect(url('my_account_pullrequests'))
674 raise HTTPForbidden()
668 raise HTTPForbidden()
675
669
676 @LoginRequired()
670 @LoginRequired()
677 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
671 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
678 'repository.admin')
672 'repository.admin')
679 def show(self, repo_name, pull_request_id):
673 def show(self, repo_name, pull_request_id):
680 pull_request_id = safe_int(pull_request_id)
674 pull_request_id = safe_int(pull_request_id)
681 c.pull_request = PullRequest.get_or_404(pull_request_id)
675 c.pull_request = PullRequest.get_or_404(pull_request_id)
682
676
683 c.template_context['pull_request_data']['pull_request_id'] = \
677 c.template_context['pull_request_data']['pull_request_id'] = \
684 pull_request_id
678 pull_request_id
685
679
686 # pull_requests repo_name we opened it against
680 # pull_requests repo_name we opened it against
687 # ie. target_repo must match
681 # ie. target_repo must match
688 if repo_name != c.pull_request.target_repo.repo_name:
682 if repo_name != c.pull_request.target_repo.repo_name:
689 raise HTTPNotFound
683 raise HTTPNotFound
690
684
691 c.allowed_to_change_status = PullRequestModel(). \
685 c.allowed_to_change_status = PullRequestModel(). \
692 check_user_change_status(c.pull_request, c.rhodecode_user)
686 check_user_change_status(c.pull_request, c.rhodecode_user)
693 c.allowed_to_update = PullRequestModel().check_user_update(
687 c.allowed_to_update = PullRequestModel().check_user_update(
694 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
688 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
695 c.allowed_to_merge = PullRequestModel().check_user_merge(
689 c.allowed_to_merge = PullRequestModel().check_user_merge(
696 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
690 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
697 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
691 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
698 c.pull_request)
692 c.pull_request)
699
693
700 cc_model = ChangesetCommentsModel()
694 cc_model = ChangesetCommentsModel()
701
695
702 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
696 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
703
697
704 c.pull_request_review_status = c.pull_request.calculated_review_status()
698 c.pull_request_review_status = c.pull_request.calculated_review_status()
705 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
699 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
706 c.pull_request)
700 c.pull_request)
707 c.approval_msg = None
701 c.approval_msg = None
708 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
702 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
709 c.approval_msg = _('Reviewer approval is pending.')
703 c.approval_msg = _('Reviewer approval is pending.')
710 c.pr_merge_status = False
704 c.pr_merge_status = False
711 # load compare data into template context
705 # load compare data into template context
712 enable_comments = not c.pull_request.is_closed()
706 enable_comments = not c.pull_request.is_closed()
713 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
707 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
714
708
715 # this is a hack to properly display links, when creating PR, the
709 # this is a hack to properly display links, when creating PR, the
716 # compare view and others uses different notation, and
710 # compare view and others uses different notation, and
717 # compare_commits.html renders links based on the target_repo.
711 # compare_commits.html renders links based on the target_repo.
718 # We need to swap that here to generate it properly on the html side
712 # We need to swap that here to generate it properly on the html side
719 c.target_repo = c.source_repo
713 c.target_repo = c.source_repo
720
714
721 # inline comments
715 # inline comments
722 c.inline_cnt = 0
716 c.inline_cnt = 0
723 c.inline_comments = cc_model.get_inline_comments(
717 c.inline_comments = cc_model.get_inline_comments(
724 c.rhodecode_db_repo.repo_id,
718 c.rhodecode_db_repo.repo_id,
725 pull_request=pull_request_id).items()
719 pull_request=pull_request_id).items()
726 # count inline comments
720 # count inline comments
727 for __, lines in c.inline_comments:
721 for __, lines in c.inline_comments:
728 for comments in lines.values():
722 for comments in lines.values():
729 c.inline_cnt += len(comments)
723 c.inline_cnt += len(comments)
730
724
731 # outdated comments
725 # outdated comments
732 c.outdated_cnt = 0
726 c.outdated_cnt = 0
733 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
727 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
734 c.outdated_comments = cc_model.get_outdated_comments(
728 c.outdated_comments = cc_model.get_outdated_comments(
735 c.rhodecode_db_repo.repo_id,
729 c.rhodecode_db_repo.repo_id,
736 pull_request=c.pull_request)
730 pull_request=c.pull_request)
737 # Count outdated comments and check for deleted files
731 # Count outdated comments and check for deleted files
738 for file_name, lines in c.outdated_comments.iteritems():
732 for file_name, lines in c.outdated_comments.iteritems():
739 for comments in lines.values():
733 for comments in lines.values():
740 c.outdated_cnt += len(comments)
734 c.outdated_cnt += len(comments)
741 if file_name not in c.included_files:
735 if file_name not in c.included_files:
742 c.deleted_files.append(file_name)
736 c.deleted_files.append(file_name)
743 else:
737 else:
744 c.outdated_comments = {}
738 c.outdated_comments = {}
745
739
746 # comments
740 # comments
747 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
741 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
748 pull_request=pull_request_id)
742 pull_request=pull_request_id)
749
743
750 if c.allowed_to_update:
744 if c.allowed_to_update:
751 force_close = ('forced_closed', _('Close Pull Request'))
745 force_close = ('forced_closed', _('Close Pull Request'))
752 statuses = ChangesetStatus.STATUSES + [force_close]
746 statuses = ChangesetStatus.STATUSES + [force_close]
753 else:
747 else:
754 statuses = ChangesetStatus.STATUSES
748 statuses = ChangesetStatus.STATUSES
755 c.commit_statuses = statuses
749 c.commit_statuses = statuses
756
750
757 c.ancestor = None # TODO: add ancestor here
751 c.ancestor = None # TODO: add ancestor here
758
752
759 return render('/pullrequests/pullrequest_show.html')
753 return render('/pullrequests/pullrequest_show.html')
760
754
761 @LoginRequired()
755 @LoginRequired()
762 @NotAnonymous()
756 @NotAnonymous()
763 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
757 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
764 'repository.admin')
758 'repository.admin')
765 @auth.CSRFRequired()
759 @auth.CSRFRequired()
766 @jsonify
760 @jsonify
767 def comment(self, repo_name, pull_request_id):
761 def comment(self, repo_name, pull_request_id):
768 pull_request_id = safe_int(pull_request_id)
762 pull_request_id = safe_int(pull_request_id)
769 pull_request = PullRequest.get_or_404(pull_request_id)
763 pull_request = PullRequest.get_or_404(pull_request_id)
770 if pull_request.is_closed():
764 if pull_request.is_closed():
771 raise HTTPForbidden()
765 raise HTTPForbidden()
772
766
773 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
767 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
774 # as a changeset status, still we want to send it in one value.
768 # as a changeset status, still we want to send it in one value.
775 status = request.POST.get('changeset_status', None)
769 status = request.POST.get('changeset_status', None)
776 text = request.POST.get('text')
770 text = request.POST.get('text')
777 if status and '_closed' in status:
771 if status and '_closed' in status:
778 close_pr = True
772 close_pr = True
779 status = status.replace('_closed', '')
773 status = status.replace('_closed', '')
780 else:
774 else:
781 close_pr = False
775 close_pr = False
782
776
783 forced = (status == 'forced')
777 forced = (status == 'forced')
784 if forced:
778 if forced:
785 status = 'rejected'
779 status = 'rejected'
786
780
787 allowed_to_change_status = PullRequestModel().check_user_change_status(
781 allowed_to_change_status = PullRequestModel().check_user_change_status(
788 pull_request, c.rhodecode_user)
782 pull_request, c.rhodecode_user)
789
783
790 if status and allowed_to_change_status:
784 if status and allowed_to_change_status:
791 message = (_('Status change %(transition_icon)s %(status)s')
785 message = (_('Status change %(transition_icon)s %(status)s')
792 % {'transition_icon': '>',
786 % {'transition_icon': '>',
793 'status': ChangesetStatus.get_status_lbl(status)})
787 'status': ChangesetStatus.get_status_lbl(status)})
794 if close_pr:
788 if close_pr:
795 message = _('Closing with') + ' ' + message
789 message = _('Closing with') + ' ' + message
796 text = text or message
790 text = text or message
797 comm = ChangesetCommentsModel().create(
791 comm = ChangesetCommentsModel().create(
798 text=text,
792 text=text,
799 repo=c.rhodecode_db_repo.repo_id,
793 repo=c.rhodecode_db_repo.repo_id,
800 user=c.rhodecode_user.user_id,
794 user=c.rhodecode_user.user_id,
801 pull_request=pull_request_id,
795 pull_request=pull_request_id,
802 f_path=request.POST.get('f_path'),
796 f_path=request.POST.get('f_path'),
803 line_no=request.POST.get('line'),
797 line_no=request.POST.get('line'),
804 status_change=(ChangesetStatus.get_status_lbl(status)
798 status_change=(ChangesetStatus.get_status_lbl(status)
805 if status and allowed_to_change_status else None),
799 if status and allowed_to_change_status else None),
806 status_change_type=(status
800 status_change_type=(status
807 if status and allowed_to_change_status else None),
801 if status and allowed_to_change_status else None),
808 closing_pr=close_pr
802 closing_pr=close_pr
809 )
803 )
810
804
811
805
812
806
813 if allowed_to_change_status:
807 if allowed_to_change_status:
814 old_calculated_status = pull_request.calculated_review_status()
808 old_calculated_status = pull_request.calculated_review_status()
815 # get status if set !
809 # get status if set !
816 if status:
810 if status:
817 ChangesetStatusModel().set_status(
811 ChangesetStatusModel().set_status(
818 c.rhodecode_db_repo.repo_id,
812 c.rhodecode_db_repo.repo_id,
819 status,
813 status,
820 c.rhodecode_user.user_id,
814 c.rhodecode_user.user_id,
821 comm,
815 comm,
822 pull_request=pull_request_id
816 pull_request=pull_request_id
823 )
817 )
824
818
825 Session().flush()
819 Session().flush()
826 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
820 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
827 # we now calculate the status of pull request, and based on that
821 # we now calculate the status of pull request, and based on that
828 # calculation we set the commits status
822 # calculation we set the commits status
829 calculated_status = pull_request.calculated_review_status()
823 calculated_status = pull_request.calculated_review_status()
830 if old_calculated_status != calculated_status:
824 if old_calculated_status != calculated_status:
831 PullRequestModel()._trigger_pull_request_hook(
825 PullRequestModel()._trigger_pull_request_hook(
832 pull_request, c.rhodecode_user, 'review_status_change')
826 pull_request, c.rhodecode_user, 'review_status_change')
833
827
834 calculated_status_lbl = ChangesetStatus.get_status_lbl(
828 calculated_status_lbl = ChangesetStatus.get_status_lbl(
835 calculated_status)
829 calculated_status)
836
830
837 if close_pr:
831 if close_pr:
838 status_completed = (
832 status_completed = (
839 calculated_status in [ChangesetStatus.STATUS_APPROVED,
833 calculated_status in [ChangesetStatus.STATUS_APPROVED,
840 ChangesetStatus.STATUS_REJECTED])
834 ChangesetStatus.STATUS_REJECTED])
841 if forced or status_completed:
835 if forced or status_completed:
842 PullRequestModel().close_pull_request(
836 PullRequestModel().close_pull_request(
843 pull_request_id, c.rhodecode_user)
837 pull_request_id, c.rhodecode_user)
844 else:
838 else:
845 h.flash(_('Closing pull request on other statuses than '
839 h.flash(_('Closing pull request on other statuses than '
846 'rejected or approved is forbidden. '
840 'rejected or approved is forbidden. '
847 'Calculated status from all reviewers '
841 'Calculated status from all reviewers '
848 'is currently: %s') % calculated_status_lbl,
842 'is currently: %s') % calculated_status_lbl,
849 category='warning')
843 category='warning')
850
844
851 Session().commit()
845 Session().commit()
852
846
853 if not request.is_xhr:
847 if not request.is_xhr:
854 return redirect(h.url('pullrequest_show', repo_name=repo_name,
848 return redirect(h.url('pullrequest_show', repo_name=repo_name,
855 pull_request_id=pull_request_id))
849 pull_request_id=pull_request_id))
856
850
857 data = {
851 data = {
858 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
852 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
859 }
853 }
860 if comm:
854 if comm:
861 c.co = comm
855 c.co = comm
862 data.update(comm.get_dict())
856 data.update(comm.get_dict())
863 data.update({'rendered_text':
857 data.update({'rendered_text':
864 render('changeset/changeset_comment_block.html')})
858 render('changeset/changeset_comment_block.html')})
865
859
866 return data
860 return data
867
861
868 @LoginRequired()
862 @LoginRequired()
869 @NotAnonymous()
863 @NotAnonymous()
870 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
864 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
871 'repository.admin')
865 'repository.admin')
872 @auth.CSRFRequired()
866 @auth.CSRFRequired()
873 @jsonify
867 @jsonify
874 def delete_comment(self, repo_name, comment_id):
868 def delete_comment(self, repo_name, comment_id):
875 return self._delete_comment(comment_id)
869 return self._delete_comment(comment_id)
876
870
877 def _delete_comment(self, comment_id):
871 def _delete_comment(self, comment_id):
878 comment_id = safe_int(comment_id)
872 comment_id = safe_int(comment_id)
879 co = ChangesetComment.get_or_404(comment_id)
873 co = ChangesetComment.get_or_404(comment_id)
880 if co.pull_request.is_closed():
874 if co.pull_request.is_closed():
881 # don't allow deleting comments on closed pull request
875 # don't allow deleting comments on closed pull request
882 raise HTTPForbidden()
876 raise HTTPForbidden()
883
877
884 is_owner = co.author.user_id == c.rhodecode_user.user_id
878 is_owner = co.author.user_id == c.rhodecode_user.user_id
885 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
879 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
886 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
880 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
887 old_calculated_status = co.pull_request.calculated_review_status()
881 old_calculated_status = co.pull_request.calculated_review_status()
888 ChangesetCommentsModel().delete(comment=co)
882 ChangesetCommentsModel().delete(comment=co)
889 Session().commit()
883 Session().commit()
890 calculated_status = co.pull_request.calculated_review_status()
884 calculated_status = co.pull_request.calculated_review_status()
891 if old_calculated_status != calculated_status:
885 if old_calculated_status != calculated_status:
892 PullRequestModel()._trigger_pull_request_hook(
886 PullRequestModel()._trigger_pull_request_hook(
893 co.pull_request, c.rhodecode_user, 'review_status_change')
887 co.pull_request, c.rhodecode_user, 'review_status_change')
894 return True
888 return True
895 else:
889 else:
896 raise HTTPForbidden()
890 raise HTTPForbidden()
General Comments 0
You need to be logged in to leave comments. Login now