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