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