##// END OF EJS Templates
pull-requests: security, prevent from injecting comments to other pull requests users...
ergo -
r2181:0bf8e4db default
parent child Browse files
Show More
@@ -1,1227 +1,1235 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 self._register_global_c(c)
63 self._register_global_c(c)
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'data_table/_dt_elements.mako')
72 'data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176
176
177 # additional filters
177 # additional filters
178 req_get = self.request.GET
178 req_get = self.request.GET
179 source = str2bool(req_get.get('source'))
179 source = str2bool(req_get.get('source'))
180 closed = str2bool(req_get.get('closed'))
180 closed = str2bool(req_get.get('closed'))
181 my = str2bool(req_get.get('my'))
181 my = str2bool(req_get.get('my'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184
184
185 filter_type = 'awaiting_review' if awaiting_review \
185 filter_type = 'awaiting_review' if awaiting_review \
186 else 'awaiting_my_review' if awaiting_my_review \
186 else 'awaiting_my_review' if awaiting_my_review \
187 else None
187 else None
188
188
189 opened_by = None
189 opened_by = None
190 if my:
190 if my:
191 opened_by = [self._rhodecode_user.user_id]
191 opened_by = [self._rhodecode_user.user_id]
192
192
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 if closed:
194 if closed:
195 statuses = [PullRequest.STATUS_CLOSED]
195 statuses = [PullRequest.STATUS_CLOSED]
196
196
197 data = self._get_pull_requests_list(
197 data = self._get_pull_requests_list(
198 repo_name=self.db_repo_name, source=source,
198 repo_name=self.db_repo_name, source=source,
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200
200
201 return data
201 return data
202
202
203 def _get_pr_version(self, pull_request_id, version=None):
203 def _get_pr_version(self, pull_request_id, version=None):
204 at_version = None
204 at_version = None
205
205
206 if version and version == 'latest':
206 if version and version == 'latest':
207 pull_request_ver = PullRequest.get(pull_request_id)
207 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_obj = pull_request_ver
208 pull_request_obj = pull_request_ver
209 _org_pull_request_obj = pull_request_obj
209 _org_pull_request_obj = pull_request_obj
210 at_version = 'latest'
210 at_version = 'latest'
211 elif version:
211 elif version:
212 pull_request_ver = PullRequestVersion.get_or_404(version)
212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_obj = pull_request_ver
213 pull_request_obj = pull_request_ver
214 _org_pull_request_obj = pull_request_ver.pull_request
214 _org_pull_request_obj = pull_request_ver.pull_request
215 at_version = pull_request_ver.pull_request_version_id
215 at_version = pull_request_ver.pull_request_version_id
216 else:
216 else:
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 pull_request_id)
218 pull_request_id)
219
219
220 pull_request_display_obj = PullRequest.get_pr_display_object(
220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_obj, _org_pull_request_obj)
221 pull_request_obj, _org_pull_request_obj)
222
222
223 return _org_pull_request_obj, pull_request_obj, \
223 return _org_pull_request_obj, pull_request_obj, \
224 pull_request_display_obj, at_version
224 pull_request_display_obj, at_version
225
225
226 def _get_diffset(self, source_repo_name, source_repo,
226 def _get_diffset(self, source_repo_name, source_repo,
227 source_ref_id, target_ref_id,
227 source_ref_id, target_ref_id,
228 target_commit, source_commit, diff_limit, fulldiff,
228 target_commit, source_commit, diff_limit, fulldiff,
229 file_limit, display_inline_comments):
229 file_limit, display_inline_comments):
230
230
231 vcs_diff = PullRequestModel().get_diff(
231 vcs_diff = PullRequestModel().get_diff(
232 source_repo, source_ref_id, target_ref_id)
232 source_repo, source_ref_id, target_ref_id)
233
233
234 diff_processor = diffs.DiffProcessor(
234 diff_processor = diffs.DiffProcessor(
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 file_limit=file_limit, show_full_diff=fulldiff)
236 file_limit=file_limit, show_full_diff=fulldiff)
237
237
238 _parsed = diff_processor.prepare()
238 _parsed = diff_processor.prepare()
239
239
240 def _node_getter(commit):
240 def _node_getter(commit):
241 def get_node(fname):
241 def get_node(fname):
242 try:
242 try:
243 return commit.get_node(fname)
243 return commit.get_node(fname)
244 except NodeDoesNotExistError:
244 except NodeDoesNotExistError:
245 return None
245 return None
246
246
247 return get_node
247 return get_node
248
248
249 diffset = codeblocks.DiffSet(
249 diffset = codeblocks.DiffSet(
250 repo_name=self.db_repo_name,
250 repo_name=self.db_repo_name,
251 source_repo_name=source_repo_name,
251 source_repo_name=source_repo_name,
252 source_node_getter=_node_getter(target_commit),
252 source_node_getter=_node_getter(target_commit),
253 target_node_getter=_node_getter(source_commit),
253 target_node_getter=_node_getter(source_commit),
254 comments=display_inline_comments
254 comments=display_inline_comments
255 )
255 )
256 diffset = diffset.render_patchset(
256 diffset = diffset.render_patchset(
257 _parsed, target_commit.raw_id, source_commit.raw_id)
257 _parsed, target_commit.raw_id, source_commit.raw_id)
258
258
259 return diffset
259 return diffset
260
260
261 @LoginRequired()
261 @LoginRequired()
262 @HasRepoPermissionAnyDecorator(
262 @HasRepoPermissionAnyDecorator(
263 'repository.read', 'repository.write', 'repository.admin')
263 'repository.read', 'repository.write', 'repository.admin')
264 @view_config(
264 @view_config(
265 route_name='pullrequest_show', request_method='GET',
265 route_name='pullrequest_show', request_method='GET',
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 def pull_request_show(self):
267 def pull_request_show(self):
268 pull_request_id = self.request.matchdict['pull_request_id']
268 pull_request_id = self.request.matchdict['pull_request_id']
269
269
270 c = self.load_default_context()
270 c = self.load_default_context()
271
271
272 version = self.request.GET.get('version')
272 version = self.request.GET.get('version')
273 from_version = self.request.GET.get('from_version') or version
273 from_version = self.request.GET.get('from_version') or version
274 merge_checks = self.request.GET.get('merge_checks')
274 merge_checks = self.request.GET.get('merge_checks')
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276
276
277 (pull_request_latest,
277 (pull_request_latest,
278 pull_request_at_ver,
278 pull_request_at_ver,
279 pull_request_display_obj,
279 pull_request_display_obj,
280 at_version) = self._get_pr_version(
280 at_version) = self._get_pr_version(
281 pull_request_id, version=version)
281 pull_request_id, version=version)
282 pr_closed = pull_request_latest.is_closed()
282 pr_closed = pull_request_latest.is_closed()
283
283
284 if pr_closed and (version or from_version):
284 if pr_closed and (version or from_version):
285 # not allow to browse versions
285 # not allow to browse versions
286 raise HTTPFound(h.route_path(
286 raise HTTPFound(h.route_path(
287 'pullrequest_show', repo_name=self.db_repo_name,
287 'pullrequest_show', repo_name=self.db_repo_name,
288 pull_request_id=pull_request_id))
288 pull_request_id=pull_request_id))
289
289
290 versions = pull_request_display_obj.versions()
290 versions = pull_request_display_obj.versions()
291
291
292 c.at_version = at_version
292 c.at_version = at_version
293 c.at_version_num = (at_version
293 c.at_version_num = (at_version
294 if at_version and at_version != 'latest'
294 if at_version and at_version != 'latest'
295 else None)
295 else None)
296 c.at_version_pos = ChangesetComment.get_index_from_version(
296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_num, versions)
297 c.at_version_num, versions)
298
298
299 (prev_pull_request_latest,
299 (prev_pull_request_latest,
300 prev_pull_request_at_ver,
300 prev_pull_request_at_ver,
301 prev_pull_request_display_obj,
301 prev_pull_request_display_obj,
302 prev_at_version) = self._get_pr_version(
302 prev_at_version) = self._get_pr_version(
303 pull_request_id, version=from_version)
303 pull_request_id, version=from_version)
304
304
305 c.from_version = prev_at_version
305 c.from_version = prev_at_version
306 c.from_version_num = (prev_at_version
306 c.from_version_num = (prev_at_version
307 if prev_at_version and prev_at_version != 'latest'
307 if prev_at_version and prev_at_version != 'latest'
308 else None)
308 else None)
309 c.from_version_pos = ChangesetComment.get_index_from_version(
309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_num, versions)
310 c.from_version_num, versions)
311
311
312 # define if we're in COMPARE mode or VIEW at version mode
312 # define if we're in COMPARE mode or VIEW at version mode
313 compare = at_version != prev_at_version
313 compare = at_version != prev_at_version
314
314
315 # pull_requests repo_name we opened it against
315 # pull_requests repo_name we opened it against
316 # ie. target_repo must match
316 # ie. target_repo must match
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 raise HTTPNotFound()
318 raise HTTPNotFound()
319
319
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 pull_request_at_ver)
321 pull_request_at_ver)
322
322
323 c.pull_request = pull_request_display_obj
323 c.pull_request = pull_request_display_obj
324 c.pull_request_latest = pull_request_latest
324 c.pull_request_latest = pull_request_latest
325
325
326 if compare or (at_version and not at_version == 'latest'):
326 if compare or (at_version and not at_version == 'latest'):
327 c.allowed_to_change_status = False
327 c.allowed_to_change_status = False
328 c.allowed_to_update = False
328 c.allowed_to_update = False
329 c.allowed_to_merge = False
329 c.allowed_to_merge = False
330 c.allowed_to_delete = False
330 c.allowed_to_delete = False
331 c.allowed_to_comment = False
331 c.allowed_to_comment = False
332 c.allowed_to_close = False
332 c.allowed_to_close = False
333 else:
333 else:
334 can_change_status = PullRequestModel().check_user_change_status(
334 can_change_status = PullRequestModel().check_user_change_status(
335 pull_request_at_ver, self._rhodecode_user)
335 pull_request_at_ver, self._rhodecode_user)
336 c.allowed_to_change_status = can_change_status and not pr_closed
336 c.allowed_to_change_status = can_change_status and not pr_closed
337
337
338 c.allowed_to_update = PullRequestModel().check_user_update(
338 c.allowed_to_update = PullRequestModel().check_user_update(
339 pull_request_latest, self._rhodecode_user) and not pr_closed
339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 pull_request_latest, self._rhodecode_user) and not pr_closed
341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 pull_request_latest, self._rhodecode_user) and not pr_closed
343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 c.allowed_to_comment = not pr_closed
344 c.allowed_to_comment = not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346
346
347 c.forbid_adding_reviewers = False
347 c.forbid_adding_reviewers = False
348 c.forbid_author_to_review = False
348 c.forbid_author_to_review = False
349 c.forbid_commit_author_to_review = False
349 c.forbid_commit_author_to_review = False
350
350
351 if pull_request_latest.reviewer_data and \
351 if pull_request_latest.reviewer_data and \
352 'rules' in pull_request_latest.reviewer_data:
352 'rules' in pull_request_latest.reviewer_data:
353 rules = pull_request_latest.reviewer_data['rules'] or {}
353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 try:
354 try:
355 c.forbid_adding_reviewers = rules.get(
355 c.forbid_adding_reviewers = rules.get(
356 'forbid_adding_reviewers')
356 'forbid_adding_reviewers')
357 c.forbid_author_to_review = rules.get(
357 c.forbid_author_to_review = rules.get(
358 'forbid_author_to_review')
358 'forbid_author_to_review')
359 c.forbid_commit_author_to_review = rules.get(
359 c.forbid_commit_author_to_review = rules.get(
360 'forbid_commit_author_to_review')
360 'forbid_commit_author_to_review')
361 except Exception:
361 except Exception:
362 pass
362 pass
363
363
364 # check merge capabilities
364 # check merge capabilities
365 _merge_check = MergeCheck.validate(
365 _merge_check = MergeCheck.validate(
366 pull_request_latest, user=self._rhodecode_user,
366 pull_request_latest, user=self._rhodecode_user,
367 translator=self.request.translate)
367 translator=self.request.translate)
368 c.pr_merge_errors = _merge_check.error_details
368 c.pr_merge_errors = _merge_check.error_details
369 c.pr_merge_possible = not _merge_check.failed
369 c.pr_merge_possible = not _merge_check.failed
370 c.pr_merge_message = _merge_check.merge_msg
370 c.pr_merge_message = _merge_check.merge_msg
371
371
372 c.pr_merge_info = MergeCheck.get_merge_conditions(
372 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 pull_request_latest, translator=self.request.translate)
373 pull_request_latest, translator=self.request.translate)
374
374
375 c.pull_request_review_status = _merge_check.review_status
375 c.pull_request_review_status = _merge_check.review_status
376 if merge_checks:
376 if merge_checks:
377 self.request.override_renderer = \
377 self.request.override_renderer = \
378 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
378 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 return self._get_template_context(c)
379 return self._get_template_context(c)
380
380
381 comments_model = CommentsModel()
381 comments_model = CommentsModel()
382
382
383 # reviewers and statuses
383 # reviewers and statuses
384 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
384 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
385 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386
386
387 # GENERAL COMMENTS with versions #
387 # GENERAL COMMENTS with versions #
388 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
388 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 q = q.order_by(ChangesetComment.comment_id.asc())
389 q = q.order_by(ChangesetComment.comment_id.asc())
390 general_comments = q
390 general_comments = q
391
391
392 # pick comments we want to render at current version
392 # pick comments we want to render at current version
393 c.comment_versions = comments_model.aggregate_comments(
393 c.comment_versions = comments_model.aggregate_comments(
394 general_comments, versions, c.at_version_num)
394 general_comments, versions, c.at_version_num)
395 c.comments = c.comment_versions[c.at_version_num]['until']
395 c.comments = c.comment_versions[c.at_version_num]['until']
396
396
397 # INLINE COMMENTS with versions #
397 # INLINE COMMENTS with versions #
398 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
398 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 q = q.order_by(ChangesetComment.comment_id.asc())
399 q = q.order_by(ChangesetComment.comment_id.asc())
400 inline_comments = q
400 inline_comments = q
401
401
402 c.inline_versions = comments_model.aggregate_comments(
402 c.inline_versions = comments_model.aggregate_comments(
403 inline_comments, versions, c.at_version_num, inline=True)
403 inline_comments, versions, c.at_version_num, inline=True)
404
404
405 # inject latest version
405 # inject latest version
406 latest_ver = PullRequest.get_pr_display_object(
406 latest_ver = PullRequest.get_pr_display_object(
407 pull_request_latest, pull_request_latest)
407 pull_request_latest, pull_request_latest)
408
408
409 c.versions = versions + [latest_ver]
409 c.versions = versions + [latest_ver]
410
410
411 # if we use version, then do not show later comments
411 # if we use version, then do not show later comments
412 # than current version
412 # than current version
413 display_inline_comments = collections.defaultdict(
413 display_inline_comments = collections.defaultdict(
414 lambda: collections.defaultdict(list))
414 lambda: collections.defaultdict(list))
415 for co in inline_comments:
415 for co in inline_comments:
416 if c.at_version_num:
416 if c.at_version_num:
417 # pick comments that are at least UPTO given version, so we
417 # pick comments that are at least UPTO given version, so we
418 # don't render comments for higher version
418 # don't render comments for higher version
419 should_render = co.pull_request_version_id and \
419 should_render = co.pull_request_version_id and \
420 co.pull_request_version_id <= c.at_version_num
420 co.pull_request_version_id <= c.at_version_num
421 else:
421 else:
422 # showing all, for 'latest'
422 # showing all, for 'latest'
423 should_render = True
423 should_render = True
424
424
425 if should_render:
425 if should_render:
426 display_inline_comments[co.f_path][co.line_no].append(co)
426 display_inline_comments[co.f_path][co.line_no].append(co)
427
427
428 # load diff data into template context, if we use compare mode then
428 # load diff data into template context, if we use compare mode then
429 # diff is calculated based on changes between versions of PR
429 # diff is calculated based on changes between versions of PR
430
430
431 source_repo = pull_request_at_ver.source_repo
431 source_repo = pull_request_at_ver.source_repo
432 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
432 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433
433
434 target_repo = pull_request_at_ver.target_repo
434 target_repo = pull_request_at_ver.target_repo
435 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
435 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436
436
437 if compare:
437 if compare:
438 # in compare switch the diff base to latest commit from prev version
438 # in compare switch the diff base to latest commit from prev version
439 target_ref_id = prev_pull_request_display_obj.revisions[0]
439 target_ref_id = prev_pull_request_display_obj.revisions[0]
440
440
441 # despite opening commits for bookmarks/branches/tags, we always
441 # despite opening commits for bookmarks/branches/tags, we always
442 # convert this to rev to prevent changes after bookmark or branch change
442 # convert this to rev to prevent changes after bookmark or branch change
443 c.source_ref_type = 'rev'
443 c.source_ref_type = 'rev'
444 c.source_ref = source_ref_id
444 c.source_ref = source_ref_id
445
445
446 c.target_ref_type = 'rev'
446 c.target_ref_type = 'rev'
447 c.target_ref = target_ref_id
447 c.target_ref = target_ref_id
448
448
449 c.source_repo = source_repo
449 c.source_repo = source_repo
450 c.target_repo = target_repo
450 c.target_repo = target_repo
451
451
452 c.commit_ranges = []
452 c.commit_ranges = []
453 source_commit = EmptyCommit()
453 source_commit = EmptyCommit()
454 target_commit = EmptyCommit()
454 target_commit = EmptyCommit()
455 c.missing_requirements = False
455 c.missing_requirements = False
456
456
457 source_scm = source_repo.scm_instance()
457 source_scm = source_repo.scm_instance()
458 target_scm = target_repo.scm_instance()
458 target_scm = target_repo.scm_instance()
459
459
460 # try first shadow repo, fallback to regular repo
460 # try first shadow repo, fallback to regular repo
461 try:
461 try:
462 commits_source_repo = pull_request_latest.get_shadow_repo()
462 commits_source_repo = pull_request_latest.get_shadow_repo()
463 except Exception:
463 except Exception:
464 log.debug('Failed to get shadow repo', exc_info=True)
464 log.debug('Failed to get shadow repo', exc_info=True)
465 commits_source_repo = source_scm
465 commits_source_repo = source_scm
466
466
467 c.commits_source_repo = commits_source_repo
467 c.commits_source_repo = commits_source_repo
468 commit_cache = {}
468 commit_cache = {}
469 try:
469 try:
470 pre_load = ["author", "branch", "date", "message"]
470 pre_load = ["author", "branch", "date", "message"]
471 show_revs = pull_request_at_ver.revisions
471 show_revs = pull_request_at_ver.revisions
472 for rev in show_revs:
472 for rev in show_revs:
473 comm = commits_source_repo.get_commit(
473 comm = commits_source_repo.get_commit(
474 commit_id=rev, pre_load=pre_load)
474 commit_id=rev, pre_load=pre_load)
475 c.commit_ranges.append(comm)
475 c.commit_ranges.append(comm)
476 commit_cache[comm.raw_id] = comm
476 commit_cache[comm.raw_id] = comm
477
477
478 # Order here matters, we first need to get target, and then
478 # Order here matters, we first need to get target, and then
479 # the source
479 # the source
480 target_commit = commits_source_repo.get_commit(
480 target_commit = commits_source_repo.get_commit(
481 commit_id=safe_str(target_ref_id))
481 commit_id=safe_str(target_ref_id))
482
482
483 source_commit = commits_source_repo.get_commit(
483 source_commit = commits_source_repo.get_commit(
484 commit_id=safe_str(source_ref_id))
484 commit_id=safe_str(source_ref_id))
485
485
486 except CommitDoesNotExistError:
486 except CommitDoesNotExistError:
487 log.warning(
487 log.warning(
488 'Failed to get commit from `{}` repo'.format(
488 'Failed to get commit from `{}` repo'.format(
489 commits_source_repo), exc_info=True)
489 commits_source_repo), exc_info=True)
490 except RepositoryRequirementError:
490 except RepositoryRequirementError:
491 log.warning(
491 log.warning(
492 'Failed to get all required data from repo', exc_info=True)
492 'Failed to get all required data from repo', exc_info=True)
493 c.missing_requirements = True
493 c.missing_requirements = True
494
494
495 c.ancestor = None # set it to None, to hide it from PR view
495 c.ancestor = None # set it to None, to hide it from PR view
496
496
497 try:
497 try:
498 ancestor_id = source_scm.get_common_ancestor(
498 ancestor_id = source_scm.get_common_ancestor(
499 source_commit.raw_id, target_commit.raw_id, target_scm)
499 source_commit.raw_id, target_commit.raw_id, target_scm)
500 c.ancestor_commit = source_scm.get_commit(ancestor_id)
500 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 except Exception:
501 except Exception:
502 c.ancestor_commit = None
502 c.ancestor_commit = None
503
503
504 c.statuses = source_repo.statuses(
504 c.statuses = source_repo.statuses(
505 [x.raw_id for x in c.commit_ranges])
505 [x.raw_id for x in c.commit_ranges])
506
506
507 # auto collapse if we have more than limit
507 # auto collapse if we have more than limit
508 collapse_limit = diffs.DiffProcessor._collapse_commits_over
508 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
509 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 c.compare_mode = compare
510 c.compare_mode = compare
511
511
512 # diff_limit is the old behavior, will cut off the whole diff
512 # diff_limit is the old behavior, will cut off the whole diff
513 # if the limit is applied otherwise will just hide the
513 # if the limit is applied otherwise will just hide the
514 # big files from the front-end
514 # big files from the front-end
515 diff_limit = c.visual.cut_off_limit_diff
515 diff_limit = c.visual.cut_off_limit_diff
516 file_limit = c.visual.cut_off_limit_file
516 file_limit = c.visual.cut_off_limit_file
517
517
518 c.missing_commits = False
518 c.missing_commits = False
519 if (c.missing_requirements
519 if (c.missing_requirements
520 or isinstance(source_commit, EmptyCommit)
520 or isinstance(source_commit, EmptyCommit)
521 or source_commit == target_commit):
521 or source_commit == target_commit):
522
522
523 c.missing_commits = True
523 c.missing_commits = True
524 else:
524 else:
525
525
526 c.diffset = self._get_diffset(
526 c.diffset = self._get_diffset(
527 c.source_repo.repo_name, commits_source_repo,
527 c.source_repo.repo_name, commits_source_repo,
528 source_ref_id, target_ref_id,
528 source_ref_id, target_ref_id,
529 target_commit, source_commit,
529 target_commit, source_commit,
530 diff_limit, c.fulldiff, file_limit, display_inline_comments)
530 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531
531
532 c.limited_diff = c.diffset.limited_diff
532 c.limited_diff = c.diffset.limited_diff
533
533
534 # calculate removed files that are bound to comments
534 # calculate removed files that are bound to comments
535 comment_deleted_files = [
535 comment_deleted_files = [
536 fname for fname in display_inline_comments
536 fname for fname in display_inline_comments
537 if fname not in c.diffset.file_stats]
537 if fname not in c.diffset.file_stats]
538
538
539 c.deleted_files_comments = collections.defaultdict(dict)
539 c.deleted_files_comments = collections.defaultdict(dict)
540 for fname, per_line_comments in display_inline_comments.items():
540 for fname, per_line_comments in display_inline_comments.items():
541 if fname in comment_deleted_files:
541 if fname in comment_deleted_files:
542 c.deleted_files_comments[fname]['stats'] = 0
542 c.deleted_files_comments[fname]['stats'] = 0
543 c.deleted_files_comments[fname]['comments'] = list()
543 c.deleted_files_comments[fname]['comments'] = list()
544 for lno, comments in per_line_comments.items():
544 for lno, comments in per_line_comments.items():
545 c.deleted_files_comments[fname]['comments'].extend(
545 c.deleted_files_comments[fname]['comments'].extend(
546 comments)
546 comments)
547
547
548 # this is a hack to properly display links, when creating PR, the
548 # this is a hack to properly display links, when creating PR, the
549 # compare view and others uses different notation, and
549 # compare view and others uses different notation, and
550 # compare_commits.mako renders links based on the target_repo.
550 # compare_commits.mako renders links based on the target_repo.
551 # We need to swap that here to generate it properly on the html side
551 # We need to swap that here to generate it properly on the html side
552 c.target_repo = c.source_repo
552 c.target_repo = c.source_repo
553
553
554 c.commit_statuses = ChangesetStatus.STATUSES
554 c.commit_statuses = ChangesetStatus.STATUSES
555
555
556 c.show_version_changes = not pr_closed
556 c.show_version_changes = not pr_closed
557 if c.show_version_changes:
557 if c.show_version_changes:
558 cur_obj = pull_request_at_ver
558 cur_obj = pull_request_at_ver
559 prev_obj = prev_pull_request_at_ver
559 prev_obj = prev_pull_request_at_ver
560
560
561 old_commit_ids = prev_obj.revisions
561 old_commit_ids = prev_obj.revisions
562 new_commit_ids = cur_obj.revisions
562 new_commit_ids = cur_obj.revisions
563 commit_changes = PullRequestModel()._calculate_commit_id_changes(
563 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 old_commit_ids, new_commit_ids)
564 old_commit_ids, new_commit_ids)
565 c.commit_changes_summary = commit_changes
565 c.commit_changes_summary = commit_changes
566
566
567 # calculate the diff for commits between versions
567 # calculate the diff for commits between versions
568 c.commit_changes = []
568 c.commit_changes = []
569 mark = lambda cs, fw: list(
569 mark = lambda cs, fw: list(
570 h.itertools.izip_longest([], cs, fillvalue=fw))
570 h.itertools.izip_longest([], cs, fillvalue=fw))
571 for c_type, raw_id in mark(commit_changes.added, 'a') \
571 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 + mark(commit_changes.removed, 'r') \
572 + mark(commit_changes.removed, 'r') \
573 + mark(commit_changes.common, 'c'):
573 + mark(commit_changes.common, 'c'):
574
574
575 if raw_id in commit_cache:
575 if raw_id in commit_cache:
576 commit = commit_cache[raw_id]
576 commit = commit_cache[raw_id]
577 else:
577 else:
578 try:
578 try:
579 commit = commits_source_repo.get_commit(raw_id)
579 commit = commits_source_repo.get_commit(raw_id)
580 except CommitDoesNotExistError:
580 except CommitDoesNotExistError:
581 # in case we fail extracting still use "dummy" commit
581 # in case we fail extracting still use "dummy" commit
582 # for display in commit diff
582 # for display in commit diff
583 commit = h.AttributeDict(
583 commit = h.AttributeDict(
584 {'raw_id': raw_id,
584 {'raw_id': raw_id,
585 'message': 'EMPTY or MISSING COMMIT'})
585 'message': 'EMPTY or MISSING COMMIT'})
586 c.commit_changes.append([c_type, commit])
586 c.commit_changes.append([c_type, commit])
587
587
588 # current user review statuses for each version
588 # current user review statuses for each version
589 c.review_versions = {}
589 c.review_versions = {}
590 if self._rhodecode_user.user_id in allowed_reviewers:
590 if self._rhodecode_user.user_id in allowed_reviewers:
591 for co in general_comments:
591 for co in general_comments:
592 if co.author.user_id == self._rhodecode_user.user_id:
592 if co.author.user_id == self._rhodecode_user.user_id:
593 # each comment has a status change
593 # each comment has a status change
594 status = co.status_change
594 status = co.status_change
595 if status:
595 if status:
596 _ver_pr = status[0].comment.pull_request_version_id
596 _ver_pr = status[0].comment.pull_request_version_id
597 c.review_versions[_ver_pr] = status[0]
597 c.review_versions[_ver_pr] = status[0]
598
598
599 return self._get_template_context(c)
599 return self._get_template_context(c)
600
600
601 def assure_not_empty_repo(self):
601 def assure_not_empty_repo(self):
602 _ = self.request.translate
602 _ = self.request.translate
603
603
604 try:
604 try:
605 self.db_repo.scm_instance().get_commit()
605 self.db_repo.scm_instance().get_commit()
606 except EmptyRepositoryError:
606 except EmptyRepositoryError:
607 h.flash(h.literal(_('There are no commits yet')),
607 h.flash(h.literal(_('There are no commits yet')),
608 category='warning')
608 category='warning')
609 raise HTTPFound(
609 raise HTTPFound(
610 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
610 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611
611
612 @LoginRequired()
612 @LoginRequired()
613 @NotAnonymous()
613 @NotAnonymous()
614 @HasRepoPermissionAnyDecorator(
614 @HasRepoPermissionAnyDecorator(
615 'repository.read', 'repository.write', 'repository.admin')
615 'repository.read', 'repository.write', 'repository.admin')
616 @view_config(
616 @view_config(
617 route_name='pullrequest_new', request_method='GET',
617 route_name='pullrequest_new', request_method='GET',
618 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
618 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 def pull_request_new(self):
619 def pull_request_new(self):
620 _ = self.request.translate
620 _ = self.request.translate
621 c = self.load_default_context()
621 c = self.load_default_context()
622
622
623 self.assure_not_empty_repo()
623 self.assure_not_empty_repo()
624 source_repo = self.db_repo
624 source_repo = self.db_repo
625
625
626 commit_id = self.request.GET.get('commit')
626 commit_id = self.request.GET.get('commit')
627 branch_ref = self.request.GET.get('branch')
627 branch_ref = self.request.GET.get('branch')
628 bookmark_ref = self.request.GET.get('bookmark')
628 bookmark_ref = self.request.GET.get('bookmark')
629
629
630 try:
630 try:
631 source_repo_data = PullRequestModel().generate_repo_data(
631 source_repo_data = PullRequestModel().generate_repo_data(
632 source_repo, commit_id=commit_id,
632 source_repo, commit_id=commit_id,
633 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
633 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 except CommitDoesNotExistError as e:
634 except CommitDoesNotExistError as e:
635 log.exception(e)
635 log.exception(e)
636 h.flash(_('Commit does not exist'), 'error')
636 h.flash(_('Commit does not exist'), 'error')
637 raise HTTPFound(
637 raise HTTPFound(
638 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
638 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639
639
640 default_target_repo = source_repo
640 default_target_repo = source_repo
641
641
642 if source_repo.parent:
642 if source_repo.parent:
643 parent_vcs_obj = source_repo.parent.scm_instance()
643 parent_vcs_obj = source_repo.parent.scm_instance()
644 if parent_vcs_obj and not parent_vcs_obj.is_empty():
644 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 # change default if we have a parent repo
645 # change default if we have a parent repo
646 default_target_repo = source_repo.parent
646 default_target_repo = source_repo.parent
647
647
648 target_repo_data = PullRequestModel().generate_repo_data(
648 target_repo_data = PullRequestModel().generate_repo_data(
649 default_target_repo, translator=self.request.translate)
649 default_target_repo, translator=self.request.translate)
650
650
651 selected_source_ref = source_repo_data['refs']['selected_ref']
651 selected_source_ref = source_repo_data['refs']['selected_ref']
652
652
653 title_source_ref = selected_source_ref.split(':', 2)[1]
653 title_source_ref = selected_source_ref.split(':', 2)[1]
654 c.default_title = PullRequestModel().generate_pullrequest_title(
654 c.default_title = PullRequestModel().generate_pullrequest_title(
655 source=source_repo.repo_name,
655 source=source_repo.repo_name,
656 source_ref=title_source_ref,
656 source_ref=title_source_ref,
657 target=default_target_repo.repo_name
657 target=default_target_repo.repo_name
658 )
658 )
659
659
660 c.default_repo_data = {
660 c.default_repo_data = {
661 'source_repo_name': source_repo.repo_name,
661 'source_repo_name': source_repo.repo_name,
662 'source_refs_json': json.dumps(source_repo_data),
662 'source_refs_json': json.dumps(source_repo_data),
663 'target_repo_name': default_target_repo.repo_name,
663 'target_repo_name': default_target_repo.repo_name,
664 'target_refs_json': json.dumps(target_repo_data),
664 'target_refs_json': json.dumps(target_repo_data),
665 }
665 }
666 c.default_source_ref = selected_source_ref
666 c.default_source_ref = selected_source_ref
667
667
668 return self._get_template_context(c)
668 return self._get_template_context(c)
669
669
670 @LoginRequired()
670 @LoginRequired()
671 @NotAnonymous()
671 @NotAnonymous()
672 @HasRepoPermissionAnyDecorator(
672 @HasRepoPermissionAnyDecorator(
673 'repository.read', 'repository.write', 'repository.admin')
673 'repository.read', 'repository.write', 'repository.admin')
674 @view_config(
674 @view_config(
675 route_name='pullrequest_repo_refs', request_method='GET',
675 route_name='pullrequest_repo_refs', request_method='GET',
676 renderer='json_ext', xhr=True)
676 renderer='json_ext', xhr=True)
677 def pull_request_repo_refs(self):
677 def pull_request_repo_refs(self):
678 target_repo_name = self.request.matchdict['target_repo_name']
678 target_repo_name = self.request.matchdict['target_repo_name']
679 repo = Repository.get_by_repo_name(target_repo_name)
679 repo = Repository.get_by_repo_name(target_repo_name)
680 if not repo:
680 if not repo:
681 raise HTTPNotFound()
681 raise HTTPNotFound()
682 return PullRequestModel().generate_repo_data(repo, translator=self.request.translate)
682 return PullRequestModel().generate_repo_data(
683 repo, translator=self.request.translate)
683
684
684 @LoginRequired()
685 @LoginRequired()
685 @NotAnonymous()
686 @NotAnonymous()
686 @HasRepoPermissionAnyDecorator(
687 @HasRepoPermissionAnyDecorator(
687 'repository.read', 'repository.write', 'repository.admin')
688 'repository.read', 'repository.write', 'repository.admin')
688 @view_config(
689 @view_config(
689 route_name='pullrequest_repo_destinations', request_method='GET',
690 route_name='pullrequest_repo_destinations', request_method='GET',
690 renderer='json_ext', xhr=True)
691 renderer='json_ext', xhr=True)
691 def pull_request_repo_destinations(self):
692 def pull_request_repo_destinations(self):
692 _ = self.request.translate
693 _ = self.request.translate
693 filter_query = self.request.GET.get('query')
694 filter_query = self.request.GET.get('query')
694
695
695 query = Repository.query() \
696 query = Repository.query() \
696 .order_by(func.length(Repository.repo_name)) \
697 .order_by(func.length(Repository.repo_name)) \
697 .filter(
698 .filter(
698 or_(Repository.repo_name == self.db_repo.repo_name,
699 or_(Repository.repo_name == self.db_repo.repo_name,
699 Repository.fork_id == self.db_repo.repo_id))
700 Repository.fork_id == self.db_repo.repo_id))
700
701
701 if filter_query:
702 if filter_query:
702 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
703 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
703 query = query.filter(
704 query = query.filter(
704 Repository.repo_name.ilike(ilike_expression))
705 Repository.repo_name.ilike(ilike_expression))
705
706
706 add_parent = False
707 add_parent = False
707 if self.db_repo.parent:
708 if self.db_repo.parent:
708 if filter_query in self.db_repo.parent.repo_name:
709 if filter_query in self.db_repo.parent.repo_name:
709 parent_vcs_obj = self.db_repo.parent.scm_instance()
710 parent_vcs_obj = self.db_repo.parent.scm_instance()
710 if parent_vcs_obj and not parent_vcs_obj.is_empty():
711 if parent_vcs_obj and not parent_vcs_obj.is_empty():
711 add_parent = True
712 add_parent = True
712
713
713 limit = 20 - 1 if add_parent else 20
714 limit = 20 - 1 if add_parent else 20
714 all_repos = query.limit(limit).all()
715 all_repos = query.limit(limit).all()
715 if add_parent:
716 if add_parent:
716 all_repos += [self.db_repo.parent]
717 all_repos += [self.db_repo.parent]
717
718
718 repos = []
719 repos = []
719 for obj in ScmModel().get_repos(all_repos):
720 for obj in ScmModel().get_repos(all_repos):
720 repos.append({
721 repos.append({
721 'id': obj['name'],
722 'id': obj['name'],
722 'text': obj['name'],
723 'text': obj['name'],
723 'type': 'repo',
724 'type': 'repo',
724 'obj': obj['dbrepo']
725 'obj': obj['dbrepo']
725 })
726 })
726
727
727 data = {
728 data = {
728 'more': False,
729 'more': False,
729 'results': [{
730 'results': [{
730 'text': _('Repositories'),
731 'text': _('Repositories'),
731 'children': repos
732 'children': repos
732 }] if repos else []
733 }] if repos else []
733 }
734 }
734 return data
735 return data
735
736
736 @LoginRequired()
737 @LoginRequired()
737 @NotAnonymous()
738 @NotAnonymous()
738 @HasRepoPermissionAnyDecorator(
739 @HasRepoPermissionAnyDecorator(
739 'repository.read', 'repository.write', 'repository.admin')
740 'repository.read', 'repository.write', 'repository.admin')
740 @CSRFRequired()
741 @CSRFRequired()
741 @view_config(
742 @view_config(
742 route_name='pullrequest_create', request_method='POST',
743 route_name='pullrequest_create', request_method='POST',
743 renderer=None)
744 renderer=None)
744 def pull_request_create(self):
745 def pull_request_create(self):
745 _ = self.request.translate
746 _ = self.request.translate
746 self.assure_not_empty_repo()
747 self.assure_not_empty_repo()
747
748
748 controls = peppercorn.parse(self.request.POST.items())
749 controls = peppercorn.parse(self.request.POST.items())
749
750
750 try:
751 try:
751 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
752 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
752 except formencode.Invalid as errors:
753 except formencode.Invalid as errors:
753 if errors.error_dict.get('revisions'):
754 if errors.error_dict.get('revisions'):
754 msg = 'Revisions: %s' % errors.error_dict['revisions']
755 msg = 'Revisions: %s' % errors.error_dict['revisions']
755 elif errors.error_dict.get('pullrequest_title'):
756 elif errors.error_dict.get('pullrequest_title'):
756 msg = _('Pull request requires a title with min. 3 chars')
757 msg = _('Pull request requires a title with min. 3 chars')
757 else:
758 else:
758 msg = _('Error creating pull request: {}').format(errors)
759 msg = _('Error creating pull request: {}').format(errors)
759 log.exception(msg)
760 log.exception(msg)
760 h.flash(msg, 'error')
761 h.flash(msg, 'error')
761
762
762 # would rather just go back to form ...
763 # would rather just go back to form ...
763 raise HTTPFound(
764 raise HTTPFound(
764 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
765 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
765
766
766 source_repo = _form['source_repo']
767 source_repo = _form['source_repo']
767 source_ref = _form['source_ref']
768 source_ref = _form['source_ref']
768 target_repo = _form['target_repo']
769 target_repo = _form['target_repo']
769 target_ref = _form['target_ref']
770 target_ref = _form['target_ref']
770 commit_ids = _form['revisions'][::-1]
771 commit_ids = _form['revisions'][::-1]
771
772
772 # find the ancestor for this pr
773 # find the ancestor for this pr
773 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
774 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
774 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
775 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
775
776
776 # re-check permissions again here
777 # re-check permissions again here
777 # source_repo we must have read permissions
778 # source_repo we must have read permissions
778
779
779 source_perm = HasRepoPermissionAny(
780 source_perm = HasRepoPermissionAny(
780 'repository.read',
781 'repository.read',
781 'repository.write', 'repository.admin')(source_db_repo.repo_name)
782 'repository.write', 'repository.admin')(source_db_repo.repo_name)
782 if not source_perm:
783 if not source_perm:
783 msg = _('Not Enough permissions to source repo `{}`.'.format(
784 msg = _('Not Enough permissions to source repo `{}`.'.format(
784 source_db_repo.repo_name))
785 source_db_repo.repo_name))
785 h.flash(msg, category='error')
786 h.flash(msg, category='error')
786 # copy the args back to redirect
787 # copy the args back to redirect
787 org_query = self.request.GET.mixed()
788 org_query = self.request.GET.mixed()
788 raise HTTPFound(
789 raise HTTPFound(
789 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
790 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
790 _query=org_query))
791 _query=org_query))
791
792
792 # target repo we must have write permissions, and also later on
793 # target repo we must have write permissions, and also later on
793 # we want to check branch permissions here
794 # we want to check branch permissions here
794 target_perm = HasRepoPermissionAny(
795 target_perm = HasRepoPermissionAny(
795 'repository.write', 'repository.admin')(target_db_repo.repo_name)
796 'repository.write', 'repository.admin')(target_db_repo.repo_name)
796 if not target_perm:
797 if not target_perm:
797 msg = _('Not Enough permissions to target repo `{}`.'.format(
798 msg = _('Not Enough permissions to target repo `{}`.'.format(
798 target_db_repo.repo_name))
799 target_db_repo.repo_name))
799 h.flash(msg, category='error')
800 h.flash(msg, category='error')
800 # copy the args back to redirect
801 # copy the args back to redirect
801 org_query = self.request.GET.mixed()
802 org_query = self.request.GET.mixed()
802 raise HTTPFound(
803 raise HTTPFound(
803 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
804 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
804 _query=org_query))
805 _query=org_query))
805
806
806 source_scm = source_db_repo.scm_instance()
807 source_scm = source_db_repo.scm_instance()
807 target_scm = target_db_repo.scm_instance()
808 target_scm = target_db_repo.scm_instance()
808
809
809 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
810 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
810 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
811 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
811
812
812 ancestor = source_scm.get_common_ancestor(
813 ancestor = source_scm.get_common_ancestor(
813 source_commit.raw_id, target_commit.raw_id, target_scm)
814 source_commit.raw_id, target_commit.raw_id, target_scm)
814
815
815 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
816 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
816 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
817 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
817
818
818 pullrequest_title = _form['pullrequest_title']
819 pullrequest_title = _form['pullrequest_title']
819 title_source_ref = source_ref.split(':', 2)[1]
820 title_source_ref = source_ref.split(':', 2)[1]
820 if not pullrequest_title:
821 if not pullrequest_title:
821 pullrequest_title = PullRequestModel().generate_pullrequest_title(
822 pullrequest_title = PullRequestModel().generate_pullrequest_title(
822 source=source_repo,
823 source=source_repo,
823 source_ref=title_source_ref,
824 source_ref=title_source_ref,
824 target=target_repo
825 target=target_repo
825 )
826 )
826
827
827 description = _form['pullrequest_desc']
828 description = _form['pullrequest_desc']
828
829
829 get_default_reviewers_data, validate_default_reviewers = \
830 get_default_reviewers_data, validate_default_reviewers = \
830 PullRequestModel().get_reviewer_functions()
831 PullRequestModel().get_reviewer_functions()
831
832
832 # recalculate reviewers logic, to make sure we can validate this
833 # recalculate reviewers logic, to make sure we can validate this
833 reviewer_rules = get_default_reviewers_data(
834 reviewer_rules = get_default_reviewers_data(
834 self._rhodecode_db_user, source_db_repo,
835 self._rhodecode_db_user, source_db_repo,
835 source_commit, target_db_repo, target_commit)
836 source_commit, target_db_repo, target_commit)
836
837
837 given_reviewers = _form['review_members']
838 given_reviewers = _form['review_members']
838 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
839 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
839
840
840 try:
841 try:
841 pull_request = PullRequestModel().create(
842 pull_request = PullRequestModel().create(
842 self._rhodecode_user.user_id, source_repo, source_ref,
843 self._rhodecode_user.user_id, source_repo, source_ref,
843 target_repo, target_ref, commit_ids, reviewers,
844 target_repo, target_ref, commit_ids, reviewers,
844 pullrequest_title, description, reviewer_rules
845 pullrequest_title, description, reviewer_rules
845 )
846 )
846 Session().commit()
847 Session().commit()
847
848
848 h.flash(_('Successfully opened new pull request'),
849 h.flash(_('Successfully opened new pull request'),
849 category='success')
850 category='success')
850 except Exception:
851 except Exception:
851 msg = _('Error occurred during creation of this pull request.')
852 msg = _('Error occurred during creation of this pull request.')
852 log.exception(msg)
853 log.exception(msg)
853 h.flash(msg, category='error')
854 h.flash(msg, category='error')
854
855
855 # copy the args back to redirect
856 # copy the args back to redirect
856 org_query = self.request.GET.mixed()
857 org_query = self.request.GET.mixed()
857 raise HTTPFound(
858 raise HTTPFound(
858 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
859 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
859 _query=org_query))
860 _query=org_query))
860
861
861 raise HTTPFound(
862 raise HTTPFound(
862 h.route_path('pullrequest_show', repo_name=target_repo,
863 h.route_path('pullrequest_show', repo_name=target_repo,
863 pull_request_id=pull_request.pull_request_id))
864 pull_request_id=pull_request.pull_request_id))
864
865
865 @LoginRequired()
866 @LoginRequired()
866 @NotAnonymous()
867 @NotAnonymous()
867 @HasRepoPermissionAnyDecorator(
868 @HasRepoPermissionAnyDecorator(
868 'repository.read', 'repository.write', 'repository.admin')
869 'repository.read', 'repository.write', 'repository.admin')
869 @CSRFRequired()
870 @CSRFRequired()
870 @view_config(
871 @view_config(
871 route_name='pullrequest_update', request_method='POST',
872 route_name='pullrequest_update', request_method='POST',
872 renderer='json_ext')
873 renderer='json_ext')
873 def pull_request_update(self):
874 def pull_request_update(self):
874 pull_request = PullRequest.get_or_404(
875 pull_request = PullRequest.get_or_404(
875 self.request.matchdict['pull_request_id'])
876 self.request.matchdict['pull_request_id'])
876
877
877 # only owner or admin can update it
878 # only owner or admin can update it
878 allowed_to_update = PullRequestModel().check_user_update(
879 allowed_to_update = PullRequestModel().check_user_update(
879 pull_request, self._rhodecode_user)
880 pull_request, self._rhodecode_user)
880 if allowed_to_update:
881 if allowed_to_update:
881 controls = peppercorn.parse(self.request.POST.items())
882 controls = peppercorn.parse(self.request.POST.items())
882
883
883 if 'review_members' in controls:
884 if 'review_members' in controls:
884 self._update_reviewers(
885 self._update_reviewers(
885 pull_request, controls['review_members'],
886 pull_request, controls['review_members'],
886 pull_request.reviewer_data)
887 pull_request.reviewer_data)
887 elif str2bool(self.request.POST.get('update_commits', 'false')):
888 elif str2bool(self.request.POST.get('update_commits', 'false')):
888 self._update_commits(pull_request)
889 self._update_commits(pull_request)
889 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
890 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
890 self._edit_pull_request(pull_request)
891 self._edit_pull_request(pull_request)
891 else:
892 else:
892 raise HTTPBadRequest()
893 raise HTTPBadRequest()
893 return True
894 return True
894 raise HTTPForbidden()
895 raise HTTPForbidden()
895
896
896 def _edit_pull_request(self, pull_request):
897 def _edit_pull_request(self, pull_request):
897 _ = self.request.translate
898 _ = self.request.translate
898 try:
899 try:
899 PullRequestModel().edit(
900 PullRequestModel().edit(
900 pull_request, self.request.POST.get('title'),
901 pull_request, self.request.POST.get('title'),
901 self.request.POST.get('description'), self._rhodecode_user)
902 self.request.POST.get('description'), self._rhodecode_user)
902 except ValueError:
903 except ValueError:
903 msg = _(u'Cannot update closed pull requests.')
904 msg = _(u'Cannot update closed pull requests.')
904 h.flash(msg, category='error')
905 h.flash(msg, category='error')
905 return
906 return
906 else:
907 else:
907 Session().commit()
908 Session().commit()
908
909
909 msg = _(u'Pull request title & description updated.')
910 msg = _(u'Pull request title & description updated.')
910 h.flash(msg, category='success')
911 h.flash(msg, category='success')
911 return
912 return
912
913
913 def _update_commits(self, pull_request):
914 def _update_commits(self, pull_request):
914 _ = self.request.translate
915 _ = self.request.translate
915 resp = PullRequestModel().update_commits(pull_request)
916 resp = PullRequestModel().update_commits(pull_request)
916
917
917 if resp.executed:
918 if resp.executed:
918
919
919 if resp.target_changed and resp.source_changed:
920 if resp.target_changed and resp.source_changed:
920 changed = 'target and source repositories'
921 changed = 'target and source repositories'
921 elif resp.target_changed and not resp.source_changed:
922 elif resp.target_changed and not resp.source_changed:
922 changed = 'target repository'
923 changed = 'target repository'
923 elif not resp.target_changed and resp.source_changed:
924 elif not resp.target_changed and resp.source_changed:
924 changed = 'source repository'
925 changed = 'source repository'
925 else:
926 else:
926 changed = 'nothing'
927 changed = 'nothing'
927
928
928 msg = _(
929 msg = _(
929 u'Pull request updated to "{source_commit_id}" with '
930 u'Pull request updated to "{source_commit_id}" with '
930 u'{count_added} added, {count_removed} removed commits. '
931 u'{count_added} added, {count_removed} removed commits. '
931 u'Source of changes: {change_source}')
932 u'Source of changes: {change_source}')
932 msg = msg.format(
933 msg = msg.format(
933 source_commit_id=pull_request.source_ref_parts.commit_id,
934 source_commit_id=pull_request.source_ref_parts.commit_id,
934 count_added=len(resp.changes.added),
935 count_added=len(resp.changes.added),
935 count_removed=len(resp.changes.removed),
936 count_removed=len(resp.changes.removed),
936 change_source=changed)
937 change_source=changed)
937 h.flash(msg, category='success')
938 h.flash(msg, category='success')
938
939
939 channel = '/repo${}$/pr/{}'.format(
940 channel = '/repo${}$/pr/{}'.format(
940 pull_request.target_repo.repo_name,
941 pull_request.target_repo.repo_name,
941 pull_request.pull_request_id)
942 pull_request.pull_request_id)
942 message = msg + (
943 message = msg + (
943 ' - <a onclick="window.location.reload()">'
944 ' - <a onclick="window.location.reload()">'
944 '<strong>{}</strong></a>'.format(_('Reload page')))
945 '<strong>{}</strong></a>'.format(_('Reload page')))
945 channelstream.post_message(
946 channelstream.post_message(
946 channel, message, self._rhodecode_user.username,
947 channel, message, self._rhodecode_user.username,
947 registry=self.request.registry)
948 registry=self.request.registry)
948 else:
949 else:
949 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
950 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
950 warning_reasons = [
951 warning_reasons = [
951 UpdateFailureReason.NO_CHANGE,
952 UpdateFailureReason.NO_CHANGE,
952 UpdateFailureReason.WRONG_REF_TYPE,
953 UpdateFailureReason.WRONG_REF_TYPE,
953 ]
954 ]
954 category = 'warning' if resp.reason in warning_reasons else 'error'
955 category = 'warning' if resp.reason in warning_reasons else 'error'
955 h.flash(msg, category=category)
956 h.flash(msg, category=category)
956
957
957 @LoginRequired()
958 @LoginRequired()
958 @NotAnonymous()
959 @NotAnonymous()
959 @HasRepoPermissionAnyDecorator(
960 @HasRepoPermissionAnyDecorator(
960 'repository.read', 'repository.write', 'repository.admin')
961 'repository.read', 'repository.write', 'repository.admin')
961 @CSRFRequired()
962 @CSRFRequired()
962 @view_config(
963 @view_config(
963 route_name='pullrequest_merge', request_method='POST',
964 route_name='pullrequest_merge', request_method='POST',
964 renderer='json_ext')
965 renderer='json_ext')
965 def pull_request_merge(self):
966 def pull_request_merge(self):
966 """
967 """
967 Merge will perform a server-side merge of the specified
968 Merge will perform a server-side merge of the specified
968 pull request, if the pull request is approved and mergeable.
969 pull request, if the pull request is approved and mergeable.
969 After successful merging, the pull request is automatically
970 After successful merging, the pull request is automatically
970 closed, with a relevant comment.
971 closed, with a relevant comment.
971 """
972 """
972 pull_request = PullRequest.get_or_404(
973 pull_request = PullRequest.get_or_404(
973 self.request.matchdict['pull_request_id'])
974 self.request.matchdict['pull_request_id'])
974
975
975 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
976 translator=self.request.translate)
977 translator=self.request.translate)
977 merge_possible = not check.failed
978 merge_possible = not check.failed
978
979
979 for err_type, error_msg in check.errors:
980 for err_type, error_msg in check.errors:
980 h.flash(error_msg, category=err_type)
981 h.flash(error_msg, category=err_type)
981
982
982 if merge_possible:
983 if merge_possible:
983 log.debug("Pre-conditions checked, trying to merge.")
984 log.debug("Pre-conditions checked, trying to merge.")
984 extras = vcs_operation_context(
985 extras = vcs_operation_context(
985 self.request.environ, repo_name=pull_request.target_repo.repo_name,
986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
986 username=self._rhodecode_db_user.username, action='push',
987 username=self._rhodecode_db_user.username, action='push',
987 scm=pull_request.target_repo.repo_type)
988 scm=pull_request.target_repo.repo_type)
988 self._merge_pull_request(
989 self._merge_pull_request(
989 pull_request, self._rhodecode_db_user, extras)
990 pull_request, self._rhodecode_db_user, extras)
990 else:
991 else:
991 log.debug("Pre-conditions failed, NOT merging.")
992 log.debug("Pre-conditions failed, NOT merging.")
992
993
993 raise HTTPFound(
994 raise HTTPFound(
994 h.route_path('pullrequest_show',
995 h.route_path('pullrequest_show',
995 repo_name=pull_request.target_repo.repo_name,
996 repo_name=pull_request.target_repo.repo_name,
996 pull_request_id=pull_request.pull_request_id))
997 pull_request_id=pull_request.pull_request_id))
997
998
998 def _merge_pull_request(self, pull_request, user, extras):
999 def _merge_pull_request(self, pull_request, user, extras):
999 _ = self.request.translate
1000 _ = self.request.translate
1000 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1001
1002
1002 if merge_resp.executed:
1003 if merge_resp.executed:
1003 log.debug("The merge was successful, closing the pull request.")
1004 log.debug("The merge was successful, closing the pull request.")
1004 PullRequestModel().close_pull_request(
1005 PullRequestModel().close_pull_request(
1005 pull_request.pull_request_id, user)
1006 pull_request.pull_request_id, user)
1006 Session().commit()
1007 Session().commit()
1007 msg = _('Pull request was successfully merged and closed.')
1008 msg = _('Pull request was successfully merged and closed.')
1008 h.flash(msg, category='success')
1009 h.flash(msg, category='success')
1009 else:
1010 else:
1010 log.debug(
1011 log.debug(
1011 "The merge was not successful. Merge response: %s",
1012 "The merge was not successful. Merge response: %s",
1012 merge_resp)
1013 merge_resp)
1013 msg = PullRequestModel().merge_status_message(
1014 msg = PullRequestModel().merge_status_message(
1014 merge_resp.failure_reason)
1015 merge_resp.failure_reason)
1015 h.flash(msg, category='error')
1016 h.flash(msg, category='error')
1016
1017
1017 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1018 _ = self.request.translate
1019 _ = self.request.translate
1019 get_default_reviewers_data, validate_default_reviewers = \
1020 get_default_reviewers_data, validate_default_reviewers = \
1020 PullRequestModel().get_reviewer_functions()
1021 PullRequestModel().get_reviewer_functions()
1021
1022
1022 try:
1023 try:
1023 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1024 except ValueError as e:
1025 except ValueError as e:
1025 log.error('Reviewers Validation: {}'.format(e))
1026 log.error('Reviewers Validation: {}'.format(e))
1026 h.flash(e, category='error')
1027 h.flash(e, category='error')
1027 return
1028 return
1028
1029
1029 PullRequestModel().update_reviewers(
1030 PullRequestModel().update_reviewers(
1030 pull_request, reviewers, self._rhodecode_user)
1031 pull_request, reviewers, self._rhodecode_user)
1031 h.flash(_('Pull request reviewers updated.'), category='success')
1032 h.flash(_('Pull request reviewers updated.'), category='success')
1032 Session().commit()
1033 Session().commit()
1033
1034
1034 @LoginRequired()
1035 @LoginRequired()
1035 @NotAnonymous()
1036 @NotAnonymous()
1036 @HasRepoPermissionAnyDecorator(
1037 @HasRepoPermissionAnyDecorator(
1037 'repository.read', 'repository.write', 'repository.admin')
1038 'repository.read', 'repository.write', 'repository.admin')
1038 @CSRFRequired()
1039 @CSRFRequired()
1039 @view_config(
1040 @view_config(
1040 route_name='pullrequest_delete', request_method='POST',
1041 route_name='pullrequest_delete', request_method='POST',
1041 renderer='json_ext')
1042 renderer='json_ext')
1042 def pull_request_delete(self):
1043 def pull_request_delete(self):
1043 _ = self.request.translate
1044 _ = self.request.translate
1044
1045
1045 pull_request = PullRequest.get_or_404(
1046 pull_request = PullRequest.get_or_404(
1046 self.request.matchdict['pull_request_id'])
1047 self.request.matchdict['pull_request_id'])
1047
1048
1048 pr_closed = pull_request.is_closed()
1049 pr_closed = pull_request.is_closed()
1049 allowed_to_delete = PullRequestModel().check_user_delete(
1050 allowed_to_delete = PullRequestModel().check_user_delete(
1050 pull_request, self._rhodecode_user) and not pr_closed
1051 pull_request, self._rhodecode_user) and not pr_closed
1051
1052
1052 # only owner can delete it !
1053 # only owner can delete it !
1053 if allowed_to_delete:
1054 if allowed_to_delete:
1054 PullRequestModel().delete(pull_request, self._rhodecode_user)
1055 PullRequestModel().delete(pull_request, self._rhodecode_user)
1055 Session().commit()
1056 Session().commit()
1056 h.flash(_('Successfully deleted pull request'),
1057 h.flash(_('Successfully deleted pull request'),
1057 category='success')
1058 category='success')
1058 raise HTTPFound(h.route_path('pullrequest_show_all',
1059 raise HTTPFound(h.route_path('pullrequest_show_all',
1059 repo_name=self.db_repo_name))
1060 repo_name=self.db_repo_name))
1060
1061
1061 log.warning('user %s tried to delete pull request without access',
1062 log.warning('user %s tried to delete pull request without access',
1062 self._rhodecode_user)
1063 self._rhodecode_user)
1063 raise HTTPNotFound()
1064 raise HTTPNotFound()
1064
1065
1065 @LoginRequired()
1066 @LoginRequired()
1066 @NotAnonymous()
1067 @NotAnonymous()
1067 @HasRepoPermissionAnyDecorator(
1068 @HasRepoPermissionAnyDecorator(
1068 'repository.read', 'repository.write', 'repository.admin')
1069 'repository.read', 'repository.write', 'repository.admin')
1069 @CSRFRequired()
1070 @CSRFRequired()
1070 @view_config(
1071 @view_config(
1071 route_name='pullrequest_comment_create', request_method='POST',
1072 route_name='pullrequest_comment_create', request_method='POST',
1072 renderer='json_ext')
1073 renderer='json_ext')
1073 def pull_request_comment_create(self):
1074 def pull_request_comment_create(self):
1074 _ = self.request.translate
1075 _ = self.request.translate
1075
1076
1076 pull_request = PullRequest.get_or_404(
1077 pull_request = PullRequest.get_or_404(
1077 self.request.matchdict['pull_request_id'])
1078 self.request.matchdict['pull_request_id'])
1078 pull_request_id = pull_request.pull_request_id
1079 pull_request_id = pull_request.pull_request_id
1079
1080
1080 if pull_request.is_closed():
1081 if pull_request.is_closed():
1081 log.debug('comment: forbidden because pull request is closed')
1082 log.debug('comment: forbidden because pull request is closed')
1082 raise HTTPForbidden()
1083 raise HTTPForbidden()
1083
1084
1085 allowed_to_comment = PullRequestModel().check_user_comment(
1086 pull_request, self._rhodecode_user)
1087 if not allowed_to_comment:
1088 log.debug(
1089 'comment: forbidden because pull request is from forbidden repo')
1090 raise HTTPForbidden()
1091
1084 c = self.load_default_context()
1092 c = self.load_default_context()
1085
1093
1086 status = self.request.POST.get('changeset_status', None)
1094 status = self.request.POST.get('changeset_status', None)
1087 text = self.request.POST.get('text')
1095 text = self.request.POST.get('text')
1088 comment_type = self.request.POST.get('comment_type')
1096 comment_type = self.request.POST.get('comment_type')
1089 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1097 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1090 close_pull_request = self.request.POST.get('close_pull_request')
1098 close_pull_request = self.request.POST.get('close_pull_request')
1091
1099
1092 # the logic here should work like following, if we submit close
1100 # the logic here should work like following, if we submit close
1093 # pr comment, use `close_pull_request_with_comment` function
1101 # pr comment, use `close_pull_request_with_comment` function
1094 # else handle regular comment logic
1102 # else handle regular comment logic
1095
1103
1096 if close_pull_request:
1104 if close_pull_request:
1097 # only owner or admin or person with write permissions
1105 # only owner or admin or person with write permissions
1098 allowed_to_close = PullRequestModel().check_user_update(
1106 allowed_to_close = PullRequestModel().check_user_update(
1099 pull_request, self._rhodecode_user)
1107 pull_request, self._rhodecode_user)
1100 if not allowed_to_close:
1108 if not allowed_to_close:
1101 log.debug('comment: forbidden because not allowed to close '
1109 log.debug('comment: forbidden because not allowed to close '
1102 'pull request %s', pull_request_id)
1110 'pull request %s', pull_request_id)
1103 raise HTTPForbidden()
1111 raise HTTPForbidden()
1104 comment, status = PullRequestModel().close_pull_request_with_comment(
1112 comment, status = PullRequestModel().close_pull_request_with_comment(
1105 pull_request, self._rhodecode_user, self.db_repo, message=text)
1113 pull_request, self._rhodecode_user, self.db_repo, message=text)
1106 Session().flush()
1114 Session().flush()
1107 events.trigger(
1115 events.trigger(
1108 events.PullRequestCommentEvent(pull_request, comment))
1116 events.PullRequestCommentEvent(pull_request, comment))
1109
1117
1110 else:
1118 else:
1111 # regular comment case, could be inline, or one with status.
1119 # regular comment case, could be inline, or one with status.
1112 # for that one we check also permissions
1120 # for that one we check also permissions
1113
1121
1114 allowed_to_change_status = PullRequestModel().check_user_change_status(
1122 allowed_to_change_status = PullRequestModel().check_user_change_status(
1115 pull_request, self._rhodecode_user)
1123 pull_request, self._rhodecode_user)
1116
1124
1117 if status and allowed_to_change_status:
1125 if status and allowed_to_change_status:
1118 message = (_('Status change %(transition_icon)s %(status)s')
1126 message = (_('Status change %(transition_icon)s %(status)s')
1119 % {'transition_icon': '>',
1127 % {'transition_icon': '>',
1120 'status': ChangesetStatus.get_status_lbl(status)})
1128 'status': ChangesetStatus.get_status_lbl(status)})
1121 text = text or message
1129 text = text or message
1122
1130
1123 comment = CommentsModel().create(
1131 comment = CommentsModel().create(
1124 text=text,
1132 text=text,
1125 repo=self.db_repo.repo_id,
1133 repo=self.db_repo.repo_id,
1126 user=self._rhodecode_user.user_id,
1134 user=self._rhodecode_user.user_id,
1127 pull_request=pull_request,
1135 pull_request=pull_request,
1128 f_path=self.request.POST.get('f_path'),
1136 f_path=self.request.POST.get('f_path'),
1129 line_no=self.request.POST.get('line'),
1137 line_no=self.request.POST.get('line'),
1130 status_change=(ChangesetStatus.get_status_lbl(status)
1138 status_change=(ChangesetStatus.get_status_lbl(status)
1131 if status and allowed_to_change_status else None),
1139 if status and allowed_to_change_status else None),
1132 status_change_type=(status
1140 status_change_type=(status
1133 if status and allowed_to_change_status else None),
1141 if status and allowed_to_change_status else None),
1134 comment_type=comment_type,
1142 comment_type=comment_type,
1135 resolves_comment_id=resolves_comment_id
1143 resolves_comment_id=resolves_comment_id
1136 )
1144 )
1137
1145
1138 if allowed_to_change_status:
1146 if allowed_to_change_status:
1139 # calculate old status before we change it
1147 # calculate old status before we change it
1140 old_calculated_status = pull_request.calculated_review_status()
1148 old_calculated_status = pull_request.calculated_review_status()
1141
1149
1142 # get status if set !
1150 # get status if set !
1143 if status:
1151 if status:
1144 ChangesetStatusModel().set_status(
1152 ChangesetStatusModel().set_status(
1145 self.db_repo.repo_id,
1153 self.db_repo.repo_id,
1146 status,
1154 status,
1147 self._rhodecode_user.user_id,
1155 self._rhodecode_user.user_id,
1148 comment,
1156 comment,
1149 pull_request=pull_request
1157 pull_request=pull_request
1150 )
1158 )
1151
1159
1152 Session().flush()
1160 Session().flush()
1153 events.trigger(
1161 events.trigger(
1154 events.PullRequestCommentEvent(pull_request, comment))
1162 events.PullRequestCommentEvent(pull_request, comment))
1155
1163
1156 # we now calculate the status of pull request, and based on that
1164 # we now calculate the status of pull request, and based on that
1157 # calculation we set the commits status
1165 # calculation we set the commits status
1158 calculated_status = pull_request.calculated_review_status()
1166 calculated_status = pull_request.calculated_review_status()
1159 if old_calculated_status != calculated_status:
1167 if old_calculated_status != calculated_status:
1160 PullRequestModel()._trigger_pull_request_hook(
1168 PullRequestModel()._trigger_pull_request_hook(
1161 pull_request, self._rhodecode_user, 'review_status_change')
1169 pull_request, self._rhodecode_user, 'review_status_change')
1162
1170
1163 Session().commit()
1171 Session().commit()
1164
1172
1165 data = {
1173 data = {
1166 'target_id': h.safeid(h.safe_unicode(
1174 'target_id': h.safeid(h.safe_unicode(
1167 self.request.POST.get('f_path'))),
1175 self.request.POST.get('f_path'))),
1168 }
1176 }
1169 if comment:
1177 if comment:
1170 c.co = comment
1178 c.co = comment
1171 rendered_comment = render(
1179 rendered_comment = render(
1172 'rhodecode:templates/changeset/changeset_comment_block.mako',
1180 'rhodecode:templates/changeset/changeset_comment_block.mako',
1173 self._get_template_context(c), self.request)
1181 self._get_template_context(c), self.request)
1174
1182
1175 data.update(comment.get_dict())
1183 data.update(comment.get_dict())
1176 data.update({'rendered_text': rendered_comment})
1184 data.update({'rendered_text': rendered_comment})
1177
1185
1178 return data
1186 return data
1179
1187
1180 @LoginRequired()
1188 @LoginRequired()
1181 @NotAnonymous()
1189 @NotAnonymous()
1182 @HasRepoPermissionAnyDecorator(
1190 @HasRepoPermissionAnyDecorator(
1183 'repository.read', 'repository.write', 'repository.admin')
1191 'repository.read', 'repository.write', 'repository.admin')
1184 @CSRFRequired()
1192 @CSRFRequired()
1185 @view_config(
1193 @view_config(
1186 route_name='pullrequest_comment_delete', request_method='POST',
1194 route_name='pullrequest_comment_delete', request_method='POST',
1187 renderer='json_ext')
1195 renderer='json_ext')
1188 def pull_request_comment_delete(self):
1196 def pull_request_comment_delete(self):
1189 pull_request = PullRequest.get_or_404(
1197 pull_request = PullRequest.get_or_404(
1190 self.request.matchdict['pull_request_id'])
1198 self.request.matchdict['pull_request_id'])
1191
1199
1192 comment = ChangesetComment.get_or_404(
1200 comment = ChangesetComment.get_or_404(
1193 self.request.matchdict['comment_id'])
1201 self.request.matchdict['comment_id'])
1194 comment_id = comment.comment_id
1202 comment_id = comment.comment_id
1195
1203
1196 if pull_request.is_closed():
1204 if pull_request.is_closed():
1197 log.debug('comment: forbidden because pull request is closed')
1205 log.debug('comment: forbidden because pull request is closed')
1198 raise HTTPForbidden()
1206 raise HTTPForbidden()
1199
1207
1200 if not comment:
1208 if not comment:
1201 log.debug('Comment with id:%s not found, skipping', comment_id)
1209 log.debug('Comment with id:%s not found, skipping', comment_id)
1202 # comment already deleted in another call probably
1210 # comment already deleted in another call probably
1203 return True
1211 return True
1204
1212
1205 if comment.pull_request.is_closed():
1213 if comment.pull_request.is_closed():
1206 # don't allow deleting comments on closed pull request
1214 # don't allow deleting comments on closed pull request
1207 raise HTTPForbidden()
1215 raise HTTPForbidden()
1208
1216
1209 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1217 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1210 super_admin = h.HasPermissionAny('hg.admin')()
1218 super_admin = h.HasPermissionAny('hg.admin')()
1211 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1219 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1212 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1220 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1213 comment_repo_admin = is_repo_admin and is_repo_comment
1221 comment_repo_admin = is_repo_admin and is_repo_comment
1214
1222
1215 if super_admin or comment_owner or comment_repo_admin:
1223 if super_admin or comment_owner or comment_repo_admin:
1216 old_calculated_status = comment.pull_request.calculated_review_status()
1224 old_calculated_status = comment.pull_request.calculated_review_status()
1217 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1225 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1218 Session().commit()
1226 Session().commit()
1219 calculated_status = comment.pull_request.calculated_review_status()
1227 calculated_status = comment.pull_request.calculated_review_status()
1220 if old_calculated_status != calculated_status:
1228 if old_calculated_status != calculated_status:
1221 PullRequestModel()._trigger_pull_request_hook(
1229 PullRequestModel()._trigger_pull_request_hook(
1222 comment.pull_request, self._rhodecode_user, 'review_status_change')
1230 comment.pull_request, self._rhodecode_user, 'review_status_change')
1223 return True
1231 return True
1224 else:
1232 else:
1225 log.warning('No permissions for user %s to delete comment_id: %s',
1233 log.warning('No permissions for user %s to delete comment_id: %s',
1226 self._rhodecode_db_user, comment_id)
1234 self._rhodecode_db_user, comment_id)
1227 raise HTTPNotFound()
1235 raise HTTPNotFound()
@@ -1,1622 +1,1626 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext#, _
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository)
54 PullRequestVersion, ChangesetComment, Repository)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
66 # request update.
66 # request update.
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
70
70
71
71
72 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
73
73
74 cls = PullRequest
74 cls = PullRequest
75
75
76 DIFF_CONTEXT = 3
76 DIFF_CONTEXT = 3
77
77
78 MERGE_STATUS_MESSAGES = {
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
87 'This pull request could not be merged because push to target'
88 ' failed.'),
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
112 'to sub repositories.'),
113 }
113 }
114
114
115 UPDATE_STATUS_MESSAGES = {
115 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
116 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
117 'Pull request update successful.'),
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 'Pull request update failed because of an unknown error.'),
119 'Pull request update failed because of an unknown error.'),
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 'No update needed because the source and target have not changed.'),
121 'No update needed because the source and target have not changed.'),
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 'Pull request cannot be updated because the reference type is '
123 'Pull request cannot be updated because the reference type is '
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 'This pull request cannot be updated because the target '
126 'This pull request cannot be updated because the target '
127 'reference is missing.'),
127 'reference is missing.'),
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 'This pull request cannot be updated because the source '
129 'This pull request cannot be updated because the source '
130 'reference is missing.'),
130 'reference is missing.'),
131 }
131 }
132
132
133 def __get_pull_request(self, pull_request):
133 def __get_pull_request(self, pull_request):
134 return self._get_instance((
134 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
135 PullRequest, PullRequestVersion), pull_request)
136
136
137 def _check_perms(self, perms, pull_request, user, api=False):
137 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
138 if not api:
139 return h.HasRepoPermissionAny(*perms)(
139 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
140 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
141 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
142 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
143 user=user, repo_name=pull_request.target_repo.repo_name)
144
144
145 def check_user_read(self, pull_request, user, api=False):
145 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
147 return self._check_perms(_perms, pull_request, user, api)
148
148
149 def check_user_merge(self, pull_request, user, api=False):
149 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
151 return self._check_perms(_perms, pull_request, user, api)
152
152
153 def check_user_update(self, pull_request, user, api=False):
153 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
154 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
155 return self.check_user_merge(pull_request, user, api) or owner
156
156
157 def check_user_delete(self, pull_request, user):
157 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
158 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
159 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
160 return self._check_perms(_perms, pull_request, user) or owner
161
161
162 def check_user_change_status(self, pull_request, user, api=False):
162 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
163 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
164 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
165 return self.check_user_update(pull_request, user, api) or reviewer
166
166
167 def check_user_comment(self, pull_request, user):
168 owner = user.user_id == pull_request.user_id
169 return self.check_user_read(pull_request, user) or owner
170
167 def get(self, pull_request):
171 def get(self, pull_request):
168 return self.__get_pull_request(pull_request)
172 return self.__get_pull_request(pull_request)
169
173
170 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 opened_by=None, order_by=None,
175 opened_by=None, order_by=None,
172 order_dir='desc'):
176 order_dir='desc'):
173 repo = None
177 repo = None
174 if repo_name:
178 if repo_name:
175 repo = self._get_repo(repo_name)
179 repo = self._get_repo(repo_name)
176
180
177 q = PullRequest.query()
181 q = PullRequest.query()
178
182
179 # source or target
183 # source or target
180 if repo and source:
184 if repo and source:
181 q = q.filter(PullRequest.source_repo == repo)
185 q = q.filter(PullRequest.source_repo == repo)
182 elif repo:
186 elif repo:
183 q = q.filter(PullRequest.target_repo == repo)
187 q = q.filter(PullRequest.target_repo == repo)
184
188
185 # closed,opened
189 # closed,opened
186 if statuses:
190 if statuses:
187 q = q.filter(PullRequest.status.in_(statuses))
191 q = q.filter(PullRequest.status.in_(statuses))
188
192
189 # opened by filter
193 # opened by filter
190 if opened_by:
194 if opened_by:
191 q = q.filter(PullRequest.user_id.in_(opened_by))
195 q = q.filter(PullRequest.user_id.in_(opened_by))
192
196
193 if order_by:
197 if order_by:
194 order_map = {
198 order_map = {
195 'name_raw': PullRequest.pull_request_id,
199 'name_raw': PullRequest.pull_request_id,
196 'title': PullRequest.title,
200 'title': PullRequest.title,
197 'updated_on_raw': PullRequest.updated_on,
201 'updated_on_raw': PullRequest.updated_on,
198 'target_repo': PullRequest.target_repo_id
202 'target_repo': PullRequest.target_repo_id
199 }
203 }
200 if order_dir == 'asc':
204 if order_dir == 'asc':
201 q = q.order_by(order_map[order_by].asc())
205 q = q.order_by(order_map[order_by].asc())
202 else:
206 else:
203 q = q.order_by(order_map[order_by].desc())
207 q = q.order_by(order_map[order_by].desc())
204
208
205 return q
209 return q
206
210
207 def count_all(self, repo_name, source=False, statuses=None,
211 def count_all(self, repo_name, source=False, statuses=None,
208 opened_by=None):
212 opened_by=None):
209 """
213 """
210 Count the number of pull requests for a specific repository.
214 Count the number of pull requests for a specific repository.
211
215
212 :param repo_name: target or source repo
216 :param repo_name: target or source repo
213 :param source: boolean flag to specify if repo_name refers to source
217 :param source: boolean flag to specify if repo_name refers to source
214 :param statuses: list of pull request statuses
218 :param statuses: list of pull request statuses
215 :param opened_by: author user of the pull request
219 :param opened_by: author user of the pull request
216 :returns: int number of pull requests
220 :returns: int number of pull requests
217 """
221 """
218 q = self._prepare_get_all_query(
222 q = self._prepare_get_all_query(
219 repo_name, source=source, statuses=statuses, opened_by=opened_by)
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220
224
221 return q.count()
225 return q.count()
222
226
223 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 offset=0, length=None, order_by=None, order_dir='desc'):
228 offset=0, length=None, order_by=None, order_dir='desc'):
225 """
229 """
226 Get all pull requests for a specific repository.
230 Get all pull requests for a specific repository.
227
231
228 :param repo_name: target or source repo
232 :param repo_name: target or source repo
229 :param source: boolean flag to specify if repo_name refers to source
233 :param source: boolean flag to specify if repo_name refers to source
230 :param statuses: list of pull request statuses
234 :param statuses: list of pull request statuses
231 :param opened_by: author user of the pull request
235 :param opened_by: author user of the pull request
232 :param offset: pagination offset
236 :param offset: pagination offset
233 :param length: length of returned list
237 :param length: length of returned list
234 :param order_by: order of the returned list
238 :param order_by: order of the returned list
235 :param order_dir: 'asc' or 'desc' ordering direction
239 :param order_dir: 'asc' or 'desc' ordering direction
236 :returns: list of pull requests
240 :returns: list of pull requests
237 """
241 """
238 q = self._prepare_get_all_query(
242 q = self._prepare_get_all_query(
239 repo_name, source=source, statuses=statuses, opened_by=opened_by,
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 order_by=order_by, order_dir=order_dir)
244 order_by=order_by, order_dir=order_dir)
241
245
242 if length:
246 if length:
243 pull_requests = q.limit(length).offset(offset).all()
247 pull_requests = q.limit(length).offset(offset).all()
244 else:
248 else:
245 pull_requests = q.all()
249 pull_requests = q.all()
246
250
247 return pull_requests
251 return pull_requests
248
252
249 def count_awaiting_review(self, repo_name, source=False, statuses=None,
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 opened_by=None):
254 opened_by=None):
251 """
255 """
252 Count the number of pull requests for a specific repository that are
256 Count the number of pull requests for a specific repository that are
253 awaiting review.
257 awaiting review.
254
258
255 :param repo_name: target or source repo
259 :param repo_name: target or source repo
256 :param source: boolean flag to specify if repo_name refers to source
260 :param source: boolean flag to specify if repo_name refers to source
257 :param statuses: list of pull request statuses
261 :param statuses: list of pull request statuses
258 :param opened_by: author user of the pull request
262 :param opened_by: author user of the pull request
259 :returns: int number of pull requests
263 :returns: int number of pull requests
260 """
264 """
261 pull_requests = self.get_awaiting_review(
265 pull_requests = self.get_awaiting_review(
262 repo_name, source=source, statuses=statuses, opened_by=opened_by)
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263
267
264 return len(pull_requests)
268 return len(pull_requests)
265
269
266 def get_awaiting_review(self, repo_name, source=False, statuses=None,
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 opened_by=None, offset=0, length=None,
271 opened_by=None, offset=0, length=None,
268 order_by=None, order_dir='desc'):
272 order_by=None, order_dir='desc'):
269 """
273 """
270 Get all pull requests for a specific repository that are awaiting
274 Get all pull requests for a specific repository that are awaiting
271 review.
275 review.
272
276
273 :param repo_name: target or source repo
277 :param repo_name: target or source repo
274 :param source: boolean flag to specify if repo_name refers to source
278 :param source: boolean flag to specify if repo_name refers to source
275 :param statuses: list of pull request statuses
279 :param statuses: list of pull request statuses
276 :param opened_by: author user of the pull request
280 :param opened_by: author user of the pull request
277 :param offset: pagination offset
281 :param offset: pagination offset
278 :param length: length of returned list
282 :param length: length of returned list
279 :param order_by: order of the returned list
283 :param order_by: order of the returned list
280 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
281 :returns: list of pull requests
285 :returns: list of pull requests
282 """
286 """
283 pull_requests = self.get_all(
287 pull_requests = self.get_all(
284 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
286
290
287 _filtered_pull_requests = []
291 _filtered_pull_requests = []
288 for pr in pull_requests:
292 for pr in pull_requests:
289 status = pr.calculated_review_status()
293 status = pr.calculated_review_status()
290 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 ChangesetStatus.STATUS_UNDER_REVIEW]:
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 _filtered_pull_requests.append(pr)
296 _filtered_pull_requests.append(pr)
293 if length:
297 if length:
294 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
295 else:
299 else:
296 return _filtered_pull_requests
300 return _filtered_pull_requests
297
301
298 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 opened_by=None, user_id=None):
303 opened_by=None, user_id=None):
300 """
304 """
301 Count the number of pull requests for a specific repository that are
305 Count the number of pull requests for a specific repository that are
302 awaiting review from a specific user.
306 awaiting review from a specific user.
303
307
304 :param repo_name: target or source repo
308 :param repo_name: target or source repo
305 :param source: boolean flag to specify if repo_name refers to source
309 :param source: boolean flag to specify if repo_name refers to source
306 :param statuses: list of pull request statuses
310 :param statuses: list of pull request statuses
307 :param opened_by: author user of the pull request
311 :param opened_by: author user of the pull request
308 :param user_id: reviewer user of the pull request
312 :param user_id: reviewer user of the pull request
309 :returns: int number of pull requests
313 :returns: int number of pull requests
310 """
314 """
311 pull_requests = self.get_awaiting_my_review(
315 pull_requests = self.get_awaiting_my_review(
312 repo_name, source=source, statuses=statuses, opened_by=opened_by,
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 user_id=user_id)
317 user_id=user_id)
314
318
315 return len(pull_requests)
319 return len(pull_requests)
316
320
317 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 opened_by=None, user_id=None, offset=0,
322 opened_by=None, user_id=None, offset=0,
319 length=None, order_by=None, order_dir='desc'):
323 length=None, order_by=None, order_dir='desc'):
320 """
324 """
321 Get all pull requests for a specific repository that are awaiting
325 Get all pull requests for a specific repository that are awaiting
322 review from a specific user.
326 review from a specific user.
323
327
324 :param repo_name: target or source repo
328 :param repo_name: target or source repo
325 :param source: boolean flag to specify if repo_name refers to source
329 :param source: boolean flag to specify if repo_name refers to source
326 :param statuses: list of pull request statuses
330 :param statuses: list of pull request statuses
327 :param opened_by: author user of the pull request
331 :param opened_by: author user of the pull request
328 :param user_id: reviewer user of the pull request
332 :param user_id: reviewer user of the pull request
329 :param offset: pagination offset
333 :param offset: pagination offset
330 :param length: length of returned list
334 :param length: length of returned list
331 :param order_by: order of the returned list
335 :param order_by: order of the returned list
332 :param order_dir: 'asc' or 'desc' ordering direction
336 :param order_dir: 'asc' or 'desc' ordering direction
333 :returns: list of pull requests
337 :returns: list of pull requests
334 """
338 """
335 pull_requests = self.get_all(
339 pull_requests = self.get_all(
336 repo_name, source=source, statuses=statuses, opened_by=opened_by,
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 order_by=order_by, order_dir=order_dir)
341 order_by=order_by, order_dir=order_dir)
338
342
339 _my = PullRequestModel().get_not_reviewed(user_id)
343 _my = PullRequestModel().get_not_reviewed(user_id)
340 my_participation = []
344 my_participation = []
341 for pr in pull_requests:
345 for pr in pull_requests:
342 if pr in _my:
346 if pr in _my:
343 my_participation.append(pr)
347 my_participation.append(pr)
344 _filtered_pull_requests = my_participation
348 _filtered_pull_requests = my_participation
345 if length:
349 if length:
346 return _filtered_pull_requests[offset:offset+length]
350 return _filtered_pull_requests[offset:offset+length]
347 else:
351 else:
348 return _filtered_pull_requests
352 return _filtered_pull_requests
349
353
350 def get_not_reviewed(self, user_id):
354 def get_not_reviewed(self, user_id):
351 return [
355 return [
352 x.pull_request for x in PullRequestReviewers.query().filter(
356 x.pull_request for x in PullRequestReviewers.query().filter(
353 PullRequestReviewers.user_id == user_id).all()
357 PullRequestReviewers.user_id == user_id).all()
354 ]
358 ]
355
359
356 def _prepare_participating_query(self, user_id=None, statuses=None,
360 def _prepare_participating_query(self, user_id=None, statuses=None,
357 order_by=None, order_dir='desc'):
361 order_by=None, order_dir='desc'):
358 q = PullRequest.query()
362 q = PullRequest.query()
359 if user_id:
363 if user_id:
360 reviewers_subquery = Session().query(
364 reviewers_subquery = Session().query(
361 PullRequestReviewers.pull_request_id).filter(
365 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.user_id == user_id).subquery()
366 PullRequestReviewers.user_id == user_id).subquery()
363 user_filter = or_(
367 user_filter = or_(
364 PullRequest.user_id == user_id,
368 PullRequest.user_id == user_id,
365 PullRequest.pull_request_id.in_(reviewers_subquery)
369 PullRequest.pull_request_id.in_(reviewers_subquery)
366 )
370 )
367 q = PullRequest.query().filter(user_filter)
371 q = PullRequest.query().filter(user_filter)
368
372
369 # closed,opened
373 # closed,opened
370 if statuses:
374 if statuses:
371 q = q.filter(PullRequest.status.in_(statuses))
375 q = q.filter(PullRequest.status.in_(statuses))
372
376
373 if order_by:
377 if order_by:
374 order_map = {
378 order_map = {
375 'name_raw': PullRequest.pull_request_id,
379 'name_raw': PullRequest.pull_request_id,
376 'title': PullRequest.title,
380 'title': PullRequest.title,
377 'updated_on_raw': PullRequest.updated_on,
381 'updated_on_raw': PullRequest.updated_on,
378 'target_repo': PullRequest.target_repo_id
382 'target_repo': PullRequest.target_repo_id
379 }
383 }
380 if order_dir == 'asc':
384 if order_dir == 'asc':
381 q = q.order_by(order_map[order_by].asc())
385 q = q.order_by(order_map[order_by].asc())
382 else:
386 else:
383 q = q.order_by(order_map[order_by].desc())
387 q = q.order_by(order_map[order_by].desc())
384
388
385 return q
389 return q
386
390
387 def count_im_participating_in(self, user_id=None, statuses=None):
391 def count_im_participating_in(self, user_id=None, statuses=None):
388 q = self._prepare_participating_query(user_id, statuses=statuses)
392 q = self._prepare_participating_query(user_id, statuses=statuses)
389 return q.count()
393 return q.count()
390
394
391 def get_im_participating_in(
395 def get_im_participating_in(
392 self, user_id=None, statuses=None, offset=0,
396 self, user_id=None, statuses=None, offset=0,
393 length=None, order_by=None, order_dir='desc'):
397 length=None, order_by=None, order_dir='desc'):
394 """
398 """
395 Get all Pull requests that i'm participating in, or i have opened
399 Get all Pull requests that i'm participating in, or i have opened
396 """
400 """
397
401
398 q = self._prepare_participating_query(
402 q = self._prepare_participating_query(
399 user_id, statuses=statuses, order_by=order_by,
403 user_id, statuses=statuses, order_by=order_by,
400 order_dir=order_dir)
404 order_dir=order_dir)
401
405
402 if length:
406 if length:
403 pull_requests = q.limit(length).offset(offset).all()
407 pull_requests = q.limit(length).offset(offset).all()
404 else:
408 else:
405 pull_requests = q.all()
409 pull_requests = q.all()
406
410
407 return pull_requests
411 return pull_requests
408
412
409 def get_versions(self, pull_request):
413 def get_versions(self, pull_request):
410 """
414 """
411 returns version of pull request sorted by ID descending
415 returns version of pull request sorted by ID descending
412 """
416 """
413 return PullRequestVersion.query()\
417 return PullRequestVersion.query()\
414 .filter(PullRequestVersion.pull_request == pull_request)\
418 .filter(PullRequestVersion.pull_request == pull_request)\
415 .order_by(PullRequestVersion.pull_request_version_id.asc())\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .all()
420 .all()
417
421
418 def create(self, created_by, source_repo, source_ref, target_repo,
422 def create(self, created_by, source_repo, source_ref, target_repo,
419 target_ref, revisions, reviewers, title, description=None,
423 target_ref, revisions, reviewers, title, description=None,
420 reviewer_data=None, translator=None):
424 reviewer_data=None, translator=None):
421 translator = translator or get_current_request().translate
425 translator = translator or get_current_request().translate
422
426
423 created_by_user = self._get_user(created_by)
427 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
428 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
429 target_repo = self._get_repo(target_repo)
426
430
427 pull_request = PullRequest()
431 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
432 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
433 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
434 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
435 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
436 pull_request.revisions = revisions
433 pull_request.title = title
437 pull_request.title = title
434 pull_request.description = description
438 pull_request.description = description
435 pull_request.author = created_by_user
439 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
440 pull_request.reviewer_data = reviewer_data
437
441
438 Session().add(pull_request)
442 Session().add(pull_request)
439 Session().flush()
443 Session().flush()
440
444
441 reviewer_ids = set()
445 reviewer_ids = set()
442 # members / reviewers
446 # members / reviewers
443 for reviewer_object in reviewers:
447 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
448 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
449 user = self._get_user(user_id)
446
450
447 # skip duplicates
451 # skip duplicates
448 if user.user_id in reviewer_ids:
452 if user.user_id in reviewer_ids:
449 continue
453 continue
450
454
451 reviewer_ids.add(user.user_id)
455 reviewer_ids.add(user.user_id)
452
456
453 reviewer = PullRequestReviewers()
457 reviewer = PullRequestReviewers()
454 reviewer.user = user
458 reviewer.user = user
455 reviewer.pull_request = pull_request
459 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
460 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
461 reviewer.mandatory = mandatory
458 Session().add(reviewer)
462 Session().add(reviewer)
459
463
460 # Set approval status to "Under Review" for all commits which are
464 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
465 # part of this pull request.
462 ChangesetStatusModel().set_status(
466 ChangesetStatusModel().set_status(
463 repo=target_repo,
467 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
468 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
469 user=created_by_user,
466 pull_request=pull_request
470 pull_request=pull_request
467 )
471 )
468
472
469 MergeCheck.validate(
473 MergeCheck.validate(
470 pull_request, user=created_by_user, translator=translator)
474 pull_request, user=created_by_user, translator=translator)
471
475
472 self.notify_reviewers(pull_request, reviewer_ids)
476 self.notify_reviewers(pull_request, reviewer_ids)
473 self._trigger_pull_request_hook(
477 self._trigger_pull_request_hook(
474 pull_request, created_by_user, 'create')
478 pull_request, created_by_user, 'create')
475
479
476 creation_data = pull_request.get_api_data(with_merge_state=False)
480 creation_data = pull_request.get_api_data(with_merge_state=False)
477 self._log_audit_action(
481 self._log_audit_action(
478 'repo.pull_request.create', {'data': creation_data},
482 'repo.pull_request.create', {'data': creation_data},
479 created_by_user, pull_request)
483 created_by_user, pull_request)
480
484
481 return pull_request
485 return pull_request
482
486
483 def _trigger_pull_request_hook(self, pull_request, user, action):
487 def _trigger_pull_request_hook(self, pull_request, user, action):
484 pull_request = self.__get_pull_request(pull_request)
488 pull_request = self.__get_pull_request(pull_request)
485 target_scm = pull_request.target_repo.scm_instance()
489 target_scm = pull_request.target_repo.scm_instance()
486 if action == 'create':
490 if action == 'create':
487 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
491 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
488 elif action == 'merge':
492 elif action == 'merge':
489 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
493 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
490 elif action == 'close':
494 elif action == 'close':
491 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
495 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
492 elif action == 'review_status_change':
496 elif action == 'review_status_change':
493 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
497 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
494 elif action == 'update':
498 elif action == 'update':
495 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
499 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
496 else:
500 else:
497 return
501 return
498
502
499 trigger_hook(
503 trigger_hook(
500 username=user.username,
504 username=user.username,
501 repo_name=pull_request.target_repo.repo_name,
505 repo_name=pull_request.target_repo.repo_name,
502 repo_alias=target_scm.alias,
506 repo_alias=target_scm.alias,
503 pull_request=pull_request)
507 pull_request=pull_request)
504
508
505 def _get_commit_ids(self, pull_request):
509 def _get_commit_ids(self, pull_request):
506 """
510 """
507 Return the commit ids of the merged pull request.
511 Return the commit ids of the merged pull request.
508
512
509 This method is not dealing correctly yet with the lack of autoupdates
513 This method is not dealing correctly yet with the lack of autoupdates
510 nor with the implicit target updates.
514 nor with the implicit target updates.
511 For example: if a commit in the source repo is already in the target it
515 For example: if a commit in the source repo is already in the target it
512 will be reported anyways.
516 will be reported anyways.
513 """
517 """
514 merge_rev = pull_request.merge_rev
518 merge_rev = pull_request.merge_rev
515 if merge_rev is None:
519 if merge_rev is None:
516 raise ValueError('This pull request was not merged yet')
520 raise ValueError('This pull request was not merged yet')
517
521
518 commit_ids = list(pull_request.revisions)
522 commit_ids = list(pull_request.revisions)
519 if merge_rev not in commit_ids:
523 if merge_rev not in commit_ids:
520 commit_ids.append(merge_rev)
524 commit_ids.append(merge_rev)
521
525
522 return commit_ids
526 return commit_ids
523
527
524 def merge(self, pull_request, user, extras):
528 def merge(self, pull_request, user, extras):
525 log.debug("Merging pull request %s", pull_request.pull_request_id)
529 log.debug("Merging pull request %s", pull_request.pull_request_id)
526 merge_state = self._merge_pull_request(pull_request, user, extras)
530 merge_state = self._merge_pull_request(pull_request, user, extras)
527 if merge_state.executed:
531 if merge_state.executed:
528 log.debug(
532 log.debug(
529 "Merge was successful, updating the pull request comments.")
533 "Merge was successful, updating the pull request comments.")
530 self._comment_and_close_pr(pull_request, user, merge_state)
534 self._comment_and_close_pr(pull_request, user, merge_state)
531
535
532 self._log_audit_action(
536 self._log_audit_action(
533 'repo.pull_request.merge',
537 'repo.pull_request.merge',
534 {'merge_state': merge_state.__dict__},
538 {'merge_state': merge_state.__dict__},
535 user, pull_request)
539 user, pull_request)
536
540
537 else:
541 else:
538 log.warn("Merge failed, not updating the pull request.")
542 log.warn("Merge failed, not updating the pull request.")
539 return merge_state
543 return merge_state
540
544
541 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
545 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
542 target_vcs = pull_request.target_repo.scm_instance()
546 target_vcs = pull_request.target_repo.scm_instance()
543 source_vcs = pull_request.source_repo.scm_instance()
547 source_vcs = pull_request.source_repo.scm_instance()
544 target_ref = self._refresh_reference(
548 target_ref = self._refresh_reference(
545 pull_request.target_ref_parts, target_vcs)
549 pull_request.target_ref_parts, target_vcs)
546
550
547 message = merge_msg or (
551 message = merge_msg or (
548 'Merge pull request #%(pr_id)s from '
552 'Merge pull request #%(pr_id)s from '
549 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
553 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
550 'pr_id': pull_request.pull_request_id,
554 'pr_id': pull_request.pull_request_id,
551 'source_repo': source_vcs.name,
555 'source_repo': source_vcs.name,
552 'source_ref_name': pull_request.source_ref_parts.name,
556 'source_ref_name': pull_request.source_ref_parts.name,
553 'pr_title': pull_request.title
557 'pr_title': pull_request.title
554 }
558 }
555
559
556 workspace_id = self._workspace_id(pull_request)
560 workspace_id = self._workspace_id(pull_request)
557 use_rebase = self._use_rebase_for_merging(pull_request)
561 use_rebase = self._use_rebase_for_merging(pull_request)
558 close_branch = self._close_branch_before_merging(pull_request)
562 close_branch = self._close_branch_before_merging(pull_request)
559
563
560 callback_daemon, extras = prepare_callback_daemon(
564 callback_daemon, extras = prepare_callback_daemon(
561 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
565 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
562 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
566 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
563
567
564 with callback_daemon:
568 with callback_daemon:
565 # TODO: johbo: Implement a clean way to run a config_override
569 # TODO: johbo: Implement a clean way to run a config_override
566 # for a single call.
570 # for a single call.
567 target_vcs.config.set(
571 target_vcs.config.set(
568 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
572 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
569 merge_state = target_vcs.merge(
573 merge_state = target_vcs.merge(
570 target_ref, source_vcs, pull_request.source_ref_parts,
574 target_ref, source_vcs, pull_request.source_ref_parts,
571 workspace_id, user_name=user.username,
575 workspace_id, user_name=user.username,
572 user_email=user.email, message=message, use_rebase=use_rebase,
576 user_email=user.email, message=message, use_rebase=use_rebase,
573 close_branch=close_branch)
577 close_branch=close_branch)
574 return merge_state
578 return merge_state
575
579
576 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
580 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
577 pull_request.merge_rev = merge_state.merge_ref.commit_id
581 pull_request.merge_rev = merge_state.merge_ref.commit_id
578 pull_request.updated_on = datetime.datetime.now()
582 pull_request.updated_on = datetime.datetime.now()
579 close_msg = close_msg or 'Pull request merged and closed'
583 close_msg = close_msg or 'Pull request merged and closed'
580
584
581 CommentsModel().create(
585 CommentsModel().create(
582 text=safe_unicode(close_msg),
586 text=safe_unicode(close_msg),
583 repo=pull_request.target_repo.repo_id,
587 repo=pull_request.target_repo.repo_id,
584 user=user.user_id,
588 user=user.user_id,
585 pull_request=pull_request.pull_request_id,
589 pull_request=pull_request.pull_request_id,
586 f_path=None,
590 f_path=None,
587 line_no=None,
591 line_no=None,
588 closing_pr=True
592 closing_pr=True
589 )
593 )
590
594
591 Session().add(pull_request)
595 Session().add(pull_request)
592 Session().flush()
596 Session().flush()
593 # TODO: paris: replace invalidation with less radical solution
597 # TODO: paris: replace invalidation with less radical solution
594 ScmModel().mark_for_invalidation(
598 ScmModel().mark_for_invalidation(
595 pull_request.target_repo.repo_name)
599 pull_request.target_repo.repo_name)
596 self._trigger_pull_request_hook(pull_request, user, 'merge')
600 self._trigger_pull_request_hook(pull_request, user, 'merge')
597
601
598 def has_valid_update_type(self, pull_request):
602 def has_valid_update_type(self, pull_request):
599 source_ref_type = pull_request.source_ref_parts.type
603 source_ref_type = pull_request.source_ref_parts.type
600 return source_ref_type in ['book', 'branch', 'tag']
604 return source_ref_type in ['book', 'branch', 'tag']
601
605
602 def update_commits(self, pull_request):
606 def update_commits(self, pull_request):
603 """
607 """
604 Get the updated list of commits for the pull request
608 Get the updated list of commits for the pull request
605 and return the new pull request version and the list
609 and return the new pull request version and the list
606 of commits processed by this update action
610 of commits processed by this update action
607 """
611 """
608 pull_request = self.__get_pull_request(pull_request)
612 pull_request = self.__get_pull_request(pull_request)
609 source_ref_type = pull_request.source_ref_parts.type
613 source_ref_type = pull_request.source_ref_parts.type
610 source_ref_name = pull_request.source_ref_parts.name
614 source_ref_name = pull_request.source_ref_parts.name
611 source_ref_id = pull_request.source_ref_parts.commit_id
615 source_ref_id = pull_request.source_ref_parts.commit_id
612
616
613 target_ref_type = pull_request.target_ref_parts.type
617 target_ref_type = pull_request.target_ref_parts.type
614 target_ref_name = pull_request.target_ref_parts.name
618 target_ref_name = pull_request.target_ref_parts.name
615 target_ref_id = pull_request.target_ref_parts.commit_id
619 target_ref_id = pull_request.target_ref_parts.commit_id
616
620
617 if not self.has_valid_update_type(pull_request):
621 if not self.has_valid_update_type(pull_request):
618 log.debug(
622 log.debug(
619 "Skipping update of pull request %s due to ref type: %s",
623 "Skipping update of pull request %s due to ref type: %s",
620 pull_request, source_ref_type)
624 pull_request, source_ref_type)
621 return UpdateResponse(
625 return UpdateResponse(
622 executed=False,
626 executed=False,
623 reason=UpdateFailureReason.WRONG_REF_TYPE,
627 reason=UpdateFailureReason.WRONG_REF_TYPE,
624 old=pull_request, new=None, changes=None,
628 old=pull_request, new=None, changes=None,
625 source_changed=False, target_changed=False)
629 source_changed=False, target_changed=False)
626
630
627 # source repo
631 # source repo
628 source_repo = pull_request.source_repo.scm_instance()
632 source_repo = pull_request.source_repo.scm_instance()
629 try:
633 try:
630 source_commit = source_repo.get_commit(commit_id=source_ref_name)
634 source_commit = source_repo.get_commit(commit_id=source_ref_name)
631 except CommitDoesNotExistError:
635 except CommitDoesNotExistError:
632 return UpdateResponse(
636 return UpdateResponse(
633 executed=False,
637 executed=False,
634 reason=UpdateFailureReason.MISSING_SOURCE_REF,
638 reason=UpdateFailureReason.MISSING_SOURCE_REF,
635 old=pull_request, new=None, changes=None,
639 old=pull_request, new=None, changes=None,
636 source_changed=False, target_changed=False)
640 source_changed=False, target_changed=False)
637
641
638 source_changed = source_ref_id != source_commit.raw_id
642 source_changed = source_ref_id != source_commit.raw_id
639
643
640 # target repo
644 # target repo
641 target_repo = pull_request.target_repo.scm_instance()
645 target_repo = pull_request.target_repo.scm_instance()
642 try:
646 try:
643 target_commit = target_repo.get_commit(commit_id=target_ref_name)
647 target_commit = target_repo.get_commit(commit_id=target_ref_name)
644 except CommitDoesNotExistError:
648 except CommitDoesNotExistError:
645 return UpdateResponse(
649 return UpdateResponse(
646 executed=False,
650 executed=False,
647 reason=UpdateFailureReason.MISSING_TARGET_REF,
651 reason=UpdateFailureReason.MISSING_TARGET_REF,
648 old=pull_request, new=None, changes=None,
652 old=pull_request, new=None, changes=None,
649 source_changed=False, target_changed=False)
653 source_changed=False, target_changed=False)
650 target_changed = target_ref_id != target_commit.raw_id
654 target_changed = target_ref_id != target_commit.raw_id
651
655
652 if not (source_changed or target_changed):
656 if not (source_changed or target_changed):
653 log.debug("Nothing changed in pull request %s", pull_request)
657 log.debug("Nothing changed in pull request %s", pull_request)
654 return UpdateResponse(
658 return UpdateResponse(
655 executed=False,
659 executed=False,
656 reason=UpdateFailureReason.NO_CHANGE,
660 reason=UpdateFailureReason.NO_CHANGE,
657 old=pull_request, new=None, changes=None,
661 old=pull_request, new=None, changes=None,
658 source_changed=target_changed, target_changed=source_changed)
662 source_changed=target_changed, target_changed=source_changed)
659
663
660 change_in_found = 'target repo' if target_changed else 'source repo'
664 change_in_found = 'target repo' if target_changed else 'source repo'
661 log.debug('Updating pull request because of change in %s detected',
665 log.debug('Updating pull request because of change in %s detected',
662 change_in_found)
666 change_in_found)
663
667
664 # Finally there is a need for an update, in case of source change
668 # Finally there is a need for an update, in case of source change
665 # we create a new version, else just an update
669 # we create a new version, else just an update
666 if source_changed:
670 if source_changed:
667 pull_request_version = self._create_version_from_snapshot(pull_request)
671 pull_request_version = self._create_version_from_snapshot(pull_request)
668 self._link_comments_to_version(pull_request_version)
672 self._link_comments_to_version(pull_request_version)
669 else:
673 else:
670 try:
674 try:
671 ver = pull_request.versions[-1]
675 ver = pull_request.versions[-1]
672 except IndexError:
676 except IndexError:
673 ver = None
677 ver = None
674
678
675 pull_request.pull_request_version_id = \
679 pull_request.pull_request_version_id = \
676 ver.pull_request_version_id if ver else None
680 ver.pull_request_version_id if ver else None
677 pull_request_version = pull_request
681 pull_request_version = pull_request
678
682
679 try:
683 try:
680 if target_ref_type in ('tag', 'branch', 'book'):
684 if target_ref_type in ('tag', 'branch', 'book'):
681 target_commit = target_repo.get_commit(target_ref_name)
685 target_commit = target_repo.get_commit(target_ref_name)
682 else:
686 else:
683 target_commit = target_repo.get_commit(target_ref_id)
687 target_commit = target_repo.get_commit(target_ref_id)
684 except CommitDoesNotExistError:
688 except CommitDoesNotExistError:
685 return UpdateResponse(
689 return UpdateResponse(
686 executed=False,
690 executed=False,
687 reason=UpdateFailureReason.MISSING_TARGET_REF,
691 reason=UpdateFailureReason.MISSING_TARGET_REF,
688 old=pull_request, new=None, changes=None,
692 old=pull_request, new=None, changes=None,
689 source_changed=source_changed, target_changed=target_changed)
693 source_changed=source_changed, target_changed=target_changed)
690
694
691 # re-compute commit ids
695 # re-compute commit ids
692 old_commit_ids = pull_request.revisions
696 old_commit_ids = pull_request.revisions
693 pre_load = ["author", "branch", "date", "message"]
697 pre_load = ["author", "branch", "date", "message"]
694 commit_ranges = target_repo.compare(
698 commit_ranges = target_repo.compare(
695 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
699 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
696 pre_load=pre_load)
700 pre_load=pre_load)
697
701
698 ancestor = target_repo.get_common_ancestor(
702 ancestor = target_repo.get_common_ancestor(
699 target_commit.raw_id, source_commit.raw_id, source_repo)
703 target_commit.raw_id, source_commit.raw_id, source_repo)
700
704
701 pull_request.source_ref = '%s:%s:%s' % (
705 pull_request.source_ref = '%s:%s:%s' % (
702 source_ref_type, source_ref_name, source_commit.raw_id)
706 source_ref_type, source_ref_name, source_commit.raw_id)
703 pull_request.target_ref = '%s:%s:%s' % (
707 pull_request.target_ref = '%s:%s:%s' % (
704 target_ref_type, target_ref_name, ancestor)
708 target_ref_type, target_ref_name, ancestor)
705
709
706 pull_request.revisions = [
710 pull_request.revisions = [
707 commit.raw_id for commit in reversed(commit_ranges)]
711 commit.raw_id for commit in reversed(commit_ranges)]
708 pull_request.updated_on = datetime.datetime.now()
712 pull_request.updated_on = datetime.datetime.now()
709 Session().add(pull_request)
713 Session().add(pull_request)
710 new_commit_ids = pull_request.revisions
714 new_commit_ids = pull_request.revisions
711
715
712 old_diff_data, new_diff_data = self._generate_update_diffs(
716 old_diff_data, new_diff_data = self._generate_update_diffs(
713 pull_request, pull_request_version)
717 pull_request, pull_request_version)
714
718
715 # calculate commit and file changes
719 # calculate commit and file changes
716 changes = self._calculate_commit_id_changes(
720 changes = self._calculate_commit_id_changes(
717 old_commit_ids, new_commit_ids)
721 old_commit_ids, new_commit_ids)
718 file_changes = self._calculate_file_changes(
722 file_changes = self._calculate_file_changes(
719 old_diff_data, new_diff_data)
723 old_diff_data, new_diff_data)
720
724
721 # set comments as outdated if DIFFS changed
725 # set comments as outdated if DIFFS changed
722 CommentsModel().outdate_comments(
726 CommentsModel().outdate_comments(
723 pull_request, old_diff_data=old_diff_data,
727 pull_request, old_diff_data=old_diff_data,
724 new_diff_data=new_diff_data)
728 new_diff_data=new_diff_data)
725
729
726 commit_changes = (changes.added or changes.removed)
730 commit_changes = (changes.added or changes.removed)
727 file_node_changes = (
731 file_node_changes = (
728 file_changes.added or file_changes.modified or file_changes.removed)
732 file_changes.added or file_changes.modified or file_changes.removed)
729 pr_has_changes = commit_changes or file_node_changes
733 pr_has_changes = commit_changes or file_node_changes
730
734
731 # Add an automatic comment to the pull request, in case
735 # Add an automatic comment to the pull request, in case
732 # anything has changed
736 # anything has changed
733 if pr_has_changes:
737 if pr_has_changes:
734 update_comment = CommentsModel().create(
738 update_comment = CommentsModel().create(
735 text=self._render_update_message(changes, file_changes),
739 text=self._render_update_message(changes, file_changes),
736 repo=pull_request.target_repo,
740 repo=pull_request.target_repo,
737 user=pull_request.author,
741 user=pull_request.author,
738 pull_request=pull_request,
742 pull_request=pull_request,
739 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
743 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
740
744
741 # Update status to "Under Review" for added commits
745 # Update status to "Under Review" for added commits
742 for commit_id in changes.added:
746 for commit_id in changes.added:
743 ChangesetStatusModel().set_status(
747 ChangesetStatusModel().set_status(
744 repo=pull_request.source_repo,
748 repo=pull_request.source_repo,
745 status=ChangesetStatus.STATUS_UNDER_REVIEW,
749 status=ChangesetStatus.STATUS_UNDER_REVIEW,
746 comment=update_comment,
750 comment=update_comment,
747 user=pull_request.author,
751 user=pull_request.author,
748 pull_request=pull_request,
752 pull_request=pull_request,
749 revision=commit_id)
753 revision=commit_id)
750
754
751 log.debug(
755 log.debug(
752 'Updated pull request %s, added_ids: %s, common_ids: %s, '
756 'Updated pull request %s, added_ids: %s, common_ids: %s, '
753 'removed_ids: %s', pull_request.pull_request_id,
757 'removed_ids: %s', pull_request.pull_request_id,
754 changes.added, changes.common, changes.removed)
758 changes.added, changes.common, changes.removed)
755 log.debug(
759 log.debug(
756 'Updated pull request with the following file changes: %s',
760 'Updated pull request with the following file changes: %s',
757 file_changes)
761 file_changes)
758
762
759 log.info(
763 log.info(
760 "Updated pull request %s from commit %s to commit %s, "
764 "Updated pull request %s from commit %s to commit %s, "
761 "stored new version %s of this pull request.",
765 "stored new version %s of this pull request.",
762 pull_request.pull_request_id, source_ref_id,
766 pull_request.pull_request_id, source_ref_id,
763 pull_request.source_ref_parts.commit_id,
767 pull_request.source_ref_parts.commit_id,
764 pull_request_version.pull_request_version_id)
768 pull_request_version.pull_request_version_id)
765 Session().commit()
769 Session().commit()
766 self._trigger_pull_request_hook(
770 self._trigger_pull_request_hook(
767 pull_request, pull_request.author, 'update')
771 pull_request, pull_request.author, 'update')
768
772
769 return UpdateResponse(
773 return UpdateResponse(
770 executed=True, reason=UpdateFailureReason.NONE,
774 executed=True, reason=UpdateFailureReason.NONE,
771 old=pull_request, new=pull_request_version, changes=changes,
775 old=pull_request, new=pull_request_version, changes=changes,
772 source_changed=source_changed, target_changed=target_changed)
776 source_changed=source_changed, target_changed=target_changed)
773
777
774 def _create_version_from_snapshot(self, pull_request):
778 def _create_version_from_snapshot(self, pull_request):
775 version = PullRequestVersion()
779 version = PullRequestVersion()
776 version.title = pull_request.title
780 version.title = pull_request.title
777 version.description = pull_request.description
781 version.description = pull_request.description
778 version.status = pull_request.status
782 version.status = pull_request.status
779 version.created_on = datetime.datetime.now()
783 version.created_on = datetime.datetime.now()
780 version.updated_on = pull_request.updated_on
784 version.updated_on = pull_request.updated_on
781 version.user_id = pull_request.user_id
785 version.user_id = pull_request.user_id
782 version.source_repo = pull_request.source_repo
786 version.source_repo = pull_request.source_repo
783 version.source_ref = pull_request.source_ref
787 version.source_ref = pull_request.source_ref
784 version.target_repo = pull_request.target_repo
788 version.target_repo = pull_request.target_repo
785 version.target_ref = pull_request.target_ref
789 version.target_ref = pull_request.target_ref
786
790
787 version._last_merge_source_rev = pull_request._last_merge_source_rev
791 version._last_merge_source_rev = pull_request._last_merge_source_rev
788 version._last_merge_target_rev = pull_request._last_merge_target_rev
792 version._last_merge_target_rev = pull_request._last_merge_target_rev
789 version.last_merge_status = pull_request.last_merge_status
793 version.last_merge_status = pull_request.last_merge_status
790 version.shadow_merge_ref = pull_request.shadow_merge_ref
794 version.shadow_merge_ref = pull_request.shadow_merge_ref
791 version.merge_rev = pull_request.merge_rev
795 version.merge_rev = pull_request.merge_rev
792 version.reviewer_data = pull_request.reviewer_data
796 version.reviewer_data = pull_request.reviewer_data
793
797
794 version.revisions = pull_request.revisions
798 version.revisions = pull_request.revisions
795 version.pull_request = pull_request
799 version.pull_request = pull_request
796 Session().add(version)
800 Session().add(version)
797 Session().flush()
801 Session().flush()
798
802
799 return version
803 return version
800
804
801 def _generate_update_diffs(self, pull_request, pull_request_version):
805 def _generate_update_diffs(self, pull_request, pull_request_version):
802
806
803 diff_context = (
807 diff_context = (
804 self.DIFF_CONTEXT +
808 self.DIFF_CONTEXT +
805 CommentsModel.needed_extra_diff_context())
809 CommentsModel.needed_extra_diff_context())
806
810
807 source_repo = pull_request_version.source_repo
811 source_repo = pull_request_version.source_repo
808 source_ref_id = pull_request_version.source_ref_parts.commit_id
812 source_ref_id = pull_request_version.source_ref_parts.commit_id
809 target_ref_id = pull_request_version.target_ref_parts.commit_id
813 target_ref_id = pull_request_version.target_ref_parts.commit_id
810 old_diff = self._get_diff_from_pr_or_version(
814 old_diff = self._get_diff_from_pr_or_version(
811 source_repo, source_ref_id, target_ref_id, context=diff_context)
815 source_repo, source_ref_id, target_ref_id, context=diff_context)
812
816
813 source_repo = pull_request.source_repo
817 source_repo = pull_request.source_repo
814 source_ref_id = pull_request.source_ref_parts.commit_id
818 source_ref_id = pull_request.source_ref_parts.commit_id
815 target_ref_id = pull_request.target_ref_parts.commit_id
819 target_ref_id = pull_request.target_ref_parts.commit_id
816
820
817 new_diff = self._get_diff_from_pr_or_version(
821 new_diff = self._get_diff_from_pr_or_version(
818 source_repo, source_ref_id, target_ref_id, context=diff_context)
822 source_repo, source_ref_id, target_ref_id, context=diff_context)
819
823
820 old_diff_data = diffs.DiffProcessor(old_diff)
824 old_diff_data = diffs.DiffProcessor(old_diff)
821 old_diff_data.prepare()
825 old_diff_data.prepare()
822 new_diff_data = diffs.DiffProcessor(new_diff)
826 new_diff_data = diffs.DiffProcessor(new_diff)
823 new_diff_data.prepare()
827 new_diff_data.prepare()
824
828
825 return old_diff_data, new_diff_data
829 return old_diff_data, new_diff_data
826
830
827 def _link_comments_to_version(self, pull_request_version):
831 def _link_comments_to_version(self, pull_request_version):
828 """
832 """
829 Link all unlinked comments of this pull request to the given version.
833 Link all unlinked comments of this pull request to the given version.
830
834
831 :param pull_request_version: The `PullRequestVersion` to which
835 :param pull_request_version: The `PullRequestVersion` to which
832 the comments shall be linked.
836 the comments shall be linked.
833
837
834 """
838 """
835 pull_request = pull_request_version.pull_request
839 pull_request = pull_request_version.pull_request
836 comments = ChangesetComment.query()\
840 comments = ChangesetComment.query()\
837 .filter(
841 .filter(
838 # TODO: johbo: Should we query for the repo at all here?
842 # TODO: johbo: Should we query for the repo at all here?
839 # Pending decision on how comments of PRs are to be related
843 # Pending decision on how comments of PRs are to be related
840 # to either the source repo, the target repo or no repo at all.
844 # to either the source repo, the target repo or no repo at all.
841 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
845 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
842 ChangesetComment.pull_request == pull_request,
846 ChangesetComment.pull_request == pull_request,
843 ChangesetComment.pull_request_version == None)\
847 ChangesetComment.pull_request_version == None)\
844 .order_by(ChangesetComment.comment_id.asc())
848 .order_by(ChangesetComment.comment_id.asc())
845
849
846 # TODO: johbo: Find out why this breaks if it is done in a bulk
850 # TODO: johbo: Find out why this breaks if it is done in a bulk
847 # operation.
851 # operation.
848 for comment in comments:
852 for comment in comments:
849 comment.pull_request_version_id = (
853 comment.pull_request_version_id = (
850 pull_request_version.pull_request_version_id)
854 pull_request_version.pull_request_version_id)
851 Session().add(comment)
855 Session().add(comment)
852
856
853 def _calculate_commit_id_changes(self, old_ids, new_ids):
857 def _calculate_commit_id_changes(self, old_ids, new_ids):
854 added = [x for x in new_ids if x not in old_ids]
858 added = [x for x in new_ids if x not in old_ids]
855 common = [x for x in new_ids if x in old_ids]
859 common = [x for x in new_ids if x in old_ids]
856 removed = [x for x in old_ids if x not in new_ids]
860 removed = [x for x in old_ids if x not in new_ids]
857 total = new_ids
861 total = new_ids
858 return ChangeTuple(added, common, removed, total)
862 return ChangeTuple(added, common, removed, total)
859
863
860 def _calculate_file_changes(self, old_diff_data, new_diff_data):
864 def _calculate_file_changes(self, old_diff_data, new_diff_data):
861
865
862 old_files = OrderedDict()
866 old_files = OrderedDict()
863 for diff_data in old_diff_data.parsed_diff:
867 for diff_data in old_diff_data.parsed_diff:
864 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
868 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
865
869
866 added_files = []
870 added_files = []
867 modified_files = []
871 modified_files = []
868 removed_files = []
872 removed_files = []
869 for diff_data in new_diff_data.parsed_diff:
873 for diff_data in new_diff_data.parsed_diff:
870 new_filename = diff_data['filename']
874 new_filename = diff_data['filename']
871 new_hash = md5_safe(diff_data['raw_diff'])
875 new_hash = md5_safe(diff_data['raw_diff'])
872
876
873 old_hash = old_files.get(new_filename)
877 old_hash = old_files.get(new_filename)
874 if not old_hash:
878 if not old_hash:
875 # file is not present in old diff, means it's added
879 # file is not present in old diff, means it's added
876 added_files.append(new_filename)
880 added_files.append(new_filename)
877 else:
881 else:
878 if new_hash != old_hash:
882 if new_hash != old_hash:
879 modified_files.append(new_filename)
883 modified_files.append(new_filename)
880 # now remove a file from old, since we have seen it already
884 # now remove a file from old, since we have seen it already
881 del old_files[new_filename]
885 del old_files[new_filename]
882
886
883 # removed files is when there are present in old, but not in NEW,
887 # removed files is when there are present in old, but not in NEW,
884 # since we remove old files that are present in new diff, left-overs
888 # since we remove old files that are present in new diff, left-overs
885 # if any should be the removed files
889 # if any should be the removed files
886 removed_files.extend(old_files.keys())
890 removed_files.extend(old_files.keys())
887
891
888 return FileChangeTuple(added_files, modified_files, removed_files)
892 return FileChangeTuple(added_files, modified_files, removed_files)
889
893
890 def _render_update_message(self, changes, file_changes):
894 def _render_update_message(self, changes, file_changes):
891 """
895 """
892 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
896 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
893 so it's always looking the same disregarding on which default
897 so it's always looking the same disregarding on which default
894 renderer system is using.
898 renderer system is using.
895
899
896 :param changes: changes named tuple
900 :param changes: changes named tuple
897 :param file_changes: file changes named tuple
901 :param file_changes: file changes named tuple
898
902
899 """
903 """
900 new_status = ChangesetStatus.get_status_lbl(
904 new_status = ChangesetStatus.get_status_lbl(
901 ChangesetStatus.STATUS_UNDER_REVIEW)
905 ChangesetStatus.STATUS_UNDER_REVIEW)
902
906
903 changed_files = (
907 changed_files = (
904 file_changes.added + file_changes.modified + file_changes.removed)
908 file_changes.added + file_changes.modified + file_changes.removed)
905
909
906 params = {
910 params = {
907 'under_review_label': new_status,
911 'under_review_label': new_status,
908 'added_commits': changes.added,
912 'added_commits': changes.added,
909 'removed_commits': changes.removed,
913 'removed_commits': changes.removed,
910 'changed_files': changed_files,
914 'changed_files': changed_files,
911 'added_files': file_changes.added,
915 'added_files': file_changes.added,
912 'modified_files': file_changes.modified,
916 'modified_files': file_changes.modified,
913 'removed_files': file_changes.removed,
917 'removed_files': file_changes.removed,
914 }
918 }
915 renderer = RstTemplateRenderer()
919 renderer = RstTemplateRenderer()
916 return renderer.render('pull_request_update.mako', **params)
920 return renderer.render('pull_request_update.mako', **params)
917
921
918 def edit(self, pull_request, title, description, user):
922 def edit(self, pull_request, title, description, user):
919 pull_request = self.__get_pull_request(pull_request)
923 pull_request = self.__get_pull_request(pull_request)
920 old_data = pull_request.get_api_data(with_merge_state=False)
924 old_data = pull_request.get_api_data(with_merge_state=False)
921 if pull_request.is_closed():
925 if pull_request.is_closed():
922 raise ValueError('This pull request is closed')
926 raise ValueError('This pull request is closed')
923 if title:
927 if title:
924 pull_request.title = title
928 pull_request.title = title
925 pull_request.description = description
929 pull_request.description = description
926 pull_request.updated_on = datetime.datetime.now()
930 pull_request.updated_on = datetime.datetime.now()
927 Session().add(pull_request)
931 Session().add(pull_request)
928 self._log_audit_action(
932 self._log_audit_action(
929 'repo.pull_request.edit', {'old_data': old_data},
933 'repo.pull_request.edit', {'old_data': old_data},
930 user, pull_request)
934 user, pull_request)
931
935
932 def update_reviewers(self, pull_request, reviewer_data, user):
936 def update_reviewers(self, pull_request, reviewer_data, user):
933 """
937 """
934 Update the reviewers in the pull request
938 Update the reviewers in the pull request
935
939
936 :param pull_request: the pr to update
940 :param pull_request: the pr to update
937 :param reviewer_data: list of tuples
941 :param reviewer_data: list of tuples
938 [(user, ['reason1', 'reason2'], mandatory_flag)]
942 [(user, ['reason1', 'reason2'], mandatory_flag)]
939 """
943 """
940
944
941 reviewers = {}
945 reviewers = {}
942 for user_id, reasons, mandatory in reviewer_data:
946 for user_id, reasons, mandatory in reviewer_data:
943 if isinstance(user_id, (int, basestring)):
947 if isinstance(user_id, (int, basestring)):
944 user_id = self._get_user(user_id).user_id
948 user_id = self._get_user(user_id).user_id
945 reviewers[user_id] = {
949 reviewers[user_id] = {
946 'reasons': reasons, 'mandatory': mandatory}
950 'reasons': reasons, 'mandatory': mandatory}
947
951
948 reviewers_ids = set(reviewers.keys())
952 reviewers_ids = set(reviewers.keys())
949 pull_request = self.__get_pull_request(pull_request)
953 pull_request = self.__get_pull_request(pull_request)
950 current_reviewers = PullRequestReviewers.query()\
954 current_reviewers = PullRequestReviewers.query()\
951 .filter(PullRequestReviewers.pull_request ==
955 .filter(PullRequestReviewers.pull_request ==
952 pull_request).all()
956 pull_request).all()
953 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
957 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
954
958
955 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
959 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
956 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
960 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
957
961
958 log.debug("Adding %s reviewers", ids_to_add)
962 log.debug("Adding %s reviewers", ids_to_add)
959 log.debug("Removing %s reviewers", ids_to_remove)
963 log.debug("Removing %s reviewers", ids_to_remove)
960 changed = False
964 changed = False
961 for uid in ids_to_add:
965 for uid in ids_to_add:
962 changed = True
966 changed = True
963 _usr = self._get_user(uid)
967 _usr = self._get_user(uid)
964 reviewer = PullRequestReviewers()
968 reviewer = PullRequestReviewers()
965 reviewer.user = _usr
969 reviewer.user = _usr
966 reviewer.pull_request = pull_request
970 reviewer.pull_request = pull_request
967 reviewer.reasons = reviewers[uid]['reasons']
971 reviewer.reasons = reviewers[uid]['reasons']
968 # NOTE(marcink): mandatory shouldn't be changed now
972 # NOTE(marcink): mandatory shouldn't be changed now
969 # reviewer.mandatory = reviewers[uid]['reasons']
973 # reviewer.mandatory = reviewers[uid]['reasons']
970 Session().add(reviewer)
974 Session().add(reviewer)
971 self._log_audit_action(
975 self._log_audit_action(
972 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
976 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
973 user, pull_request)
977 user, pull_request)
974
978
975 for uid in ids_to_remove:
979 for uid in ids_to_remove:
976 changed = True
980 changed = True
977 reviewers = PullRequestReviewers.query()\
981 reviewers = PullRequestReviewers.query()\
978 .filter(PullRequestReviewers.user_id == uid,
982 .filter(PullRequestReviewers.user_id == uid,
979 PullRequestReviewers.pull_request == pull_request)\
983 PullRequestReviewers.pull_request == pull_request)\
980 .all()
984 .all()
981 # use .all() in case we accidentally added the same person twice
985 # use .all() in case we accidentally added the same person twice
982 # this CAN happen due to the lack of DB checks
986 # this CAN happen due to the lack of DB checks
983 for obj in reviewers:
987 for obj in reviewers:
984 old_data = obj.get_dict()
988 old_data = obj.get_dict()
985 Session().delete(obj)
989 Session().delete(obj)
986 self._log_audit_action(
990 self._log_audit_action(
987 'repo.pull_request.reviewer.delete',
991 'repo.pull_request.reviewer.delete',
988 {'old_data': old_data}, user, pull_request)
992 {'old_data': old_data}, user, pull_request)
989
993
990 if changed:
994 if changed:
991 pull_request.updated_on = datetime.datetime.now()
995 pull_request.updated_on = datetime.datetime.now()
992 Session().add(pull_request)
996 Session().add(pull_request)
993
997
994 self.notify_reviewers(pull_request, ids_to_add)
998 self.notify_reviewers(pull_request, ids_to_add)
995 return ids_to_add, ids_to_remove
999 return ids_to_add, ids_to_remove
996
1000
997 def get_url(self, pull_request, request=None, permalink=False):
1001 def get_url(self, pull_request, request=None, permalink=False):
998 if not request:
1002 if not request:
999 request = get_current_request()
1003 request = get_current_request()
1000
1004
1001 if permalink:
1005 if permalink:
1002 return request.route_url(
1006 return request.route_url(
1003 'pull_requests_global',
1007 'pull_requests_global',
1004 pull_request_id=pull_request.pull_request_id,)
1008 pull_request_id=pull_request.pull_request_id,)
1005 else:
1009 else:
1006 return request.route_url('pullrequest_show',
1010 return request.route_url('pullrequest_show',
1007 repo_name=safe_str(pull_request.target_repo.repo_name),
1011 repo_name=safe_str(pull_request.target_repo.repo_name),
1008 pull_request_id=pull_request.pull_request_id,)
1012 pull_request_id=pull_request.pull_request_id,)
1009
1013
1010 def get_shadow_clone_url(self, pull_request):
1014 def get_shadow_clone_url(self, pull_request):
1011 """
1015 """
1012 Returns qualified url pointing to the shadow repository. If this pull
1016 Returns qualified url pointing to the shadow repository. If this pull
1013 request is closed there is no shadow repository and ``None`` will be
1017 request is closed there is no shadow repository and ``None`` will be
1014 returned.
1018 returned.
1015 """
1019 """
1016 if pull_request.is_closed():
1020 if pull_request.is_closed():
1017 return None
1021 return None
1018 else:
1022 else:
1019 pr_url = urllib.unquote(self.get_url(pull_request))
1023 pr_url = urllib.unquote(self.get_url(pull_request))
1020 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1024 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1021
1025
1022 def notify_reviewers(self, pull_request, reviewers_ids):
1026 def notify_reviewers(self, pull_request, reviewers_ids):
1023 # notification to reviewers
1027 # notification to reviewers
1024 if not reviewers_ids:
1028 if not reviewers_ids:
1025 return
1029 return
1026
1030
1027 pull_request_obj = pull_request
1031 pull_request_obj = pull_request
1028 # get the current participants of this pull request
1032 # get the current participants of this pull request
1029 recipients = reviewers_ids
1033 recipients = reviewers_ids
1030 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1034 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1031
1035
1032 pr_source_repo = pull_request_obj.source_repo
1036 pr_source_repo = pull_request_obj.source_repo
1033 pr_target_repo = pull_request_obj.target_repo
1037 pr_target_repo = pull_request_obj.target_repo
1034
1038
1035 pr_url = h.route_url('pullrequest_show',
1039 pr_url = h.route_url('pullrequest_show',
1036 repo_name=pr_target_repo.repo_name,
1040 repo_name=pr_target_repo.repo_name,
1037 pull_request_id=pull_request_obj.pull_request_id,)
1041 pull_request_id=pull_request_obj.pull_request_id,)
1038
1042
1039 # set some variables for email notification
1043 # set some variables for email notification
1040 pr_target_repo_url = h.route_url(
1044 pr_target_repo_url = h.route_url(
1041 'repo_summary', repo_name=pr_target_repo.repo_name)
1045 'repo_summary', repo_name=pr_target_repo.repo_name)
1042
1046
1043 pr_source_repo_url = h.route_url(
1047 pr_source_repo_url = h.route_url(
1044 'repo_summary', repo_name=pr_source_repo.repo_name)
1048 'repo_summary', repo_name=pr_source_repo.repo_name)
1045
1049
1046 # pull request specifics
1050 # pull request specifics
1047 pull_request_commits = [
1051 pull_request_commits = [
1048 (x.raw_id, x.message)
1052 (x.raw_id, x.message)
1049 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1053 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1050
1054
1051 kwargs = {
1055 kwargs = {
1052 'user': pull_request.author,
1056 'user': pull_request.author,
1053 'pull_request': pull_request_obj,
1057 'pull_request': pull_request_obj,
1054 'pull_request_commits': pull_request_commits,
1058 'pull_request_commits': pull_request_commits,
1055
1059
1056 'pull_request_target_repo': pr_target_repo,
1060 'pull_request_target_repo': pr_target_repo,
1057 'pull_request_target_repo_url': pr_target_repo_url,
1061 'pull_request_target_repo_url': pr_target_repo_url,
1058
1062
1059 'pull_request_source_repo': pr_source_repo,
1063 'pull_request_source_repo': pr_source_repo,
1060 'pull_request_source_repo_url': pr_source_repo_url,
1064 'pull_request_source_repo_url': pr_source_repo_url,
1061
1065
1062 'pull_request_url': pr_url,
1066 'pull_request_url': pr_url,
1063 }
1067 }
1064
1068
1065 # pre-generate the subject for notification itself
1069 # pre-generate the subject for notification itself
1066 (subject,
1070 (subject,
1067 _h, _e, # we don't care about those
1071 _h, _e, # we don't care about those
1068 body_plaintext) = EmailNotificationModel().render_email(
1072 body_plaintext) = EmailNotificationModel().render_email(
1069 notification_type, **kwargs)
1073 notification_type, **kwargs)
1070
1074
1071 # create notification objects, and emails
1075 # create notification objects, and emails
1072 NotificationModel().create(
1076 NotificationModel().create(
1073 created_by=pull_request.author,
1077 created_by=pull_request.author,
1074 notification_subject=subject,
1078 notification_subject=subject,
1075 notification_body=body_plaintext,
1079 notification_body=body_plaintext,
1076 notification_type=notification_type,
1080 notification_type=notification_type,
1077 recipients=recipients,
1081 recipients=recipients,
1078 email_kwargs=kwargs,
1082 email_kwargs=kwargs,
1079 )
1083 )
1080
1084
1081 def delete(self, pull_request, user):
1085 def delete(self, pull_request, user):
1082 pull_request = self.__get_pull_request(pull_request)
1086 pull_request = self.__get_pull_request(pull_request)
1083 old_data = pull_request.get_api_data(with_merge_state=False)
1087 old_data = pull_request.get_api_data(with_merge_state=False)
1084 self._cleanup_merge_workspace(pull_request)
1088 self._cleanup_merge_workspace(pull_request)
1085 self._log_audit_action(
1089 self._log_audit_action(
1086 'repo.pull_request.delete', {'old_data': old_data},
1090 'repo.pull_request.delete', {'old_data': old_data},
1087 user, pull_request)
1091 user, pull_request)
1088 Session().delete(pull_request)
1092 Session().delete(pull_request)
1089
1093
1090 def close_pull_request(self, pull_request, user):
1094 def close_pull_request(self, pull_request, user):
1091 pull_request = self.__get_pull_request(pull_request)
1095 pull_request = self.__get_pull_request(pull_request)
1092 self._cleanup_merge_workspace(pull_request)
1096 self._cleanup_merge_workspace(pull_request)
1093 pull_request.status = PullRequest.STATUS_CLOSED
1097 pull_request.status = PullRequest.STATUS_CLOSED
1094 pull_request.updated_on = datetime.datetime.now()
1098 pull_request.updated_on = datetime.datetime.now()
1095 Session().add(pull_request)
1099 Session().add(pull_request)
1096 self._trigger_pull_request_hook(
1100 self._trigger_pull_request_hook(
1097 pull_request, pull_request.author, 'close')
1101 pull_request, pull_request.author, 'close')
1098
1102
1099 pr_data = pull_request.get_api_data(with_merge_state=False)
1103 pr_data = pull_request.get_api_data(with_merge_state=False)
1100 self._log_audit_action(
1104 self._log_audit_action(
1101 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1105 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1102
1106
1103 def close_pull_request_with_comment(
1107 def close_pull_request_with_comment(
1104 self, pull_request, user, repo, message=None):
1108 self, pull_request, user, repo, message=None):
1105
1109
1106 pull_request_review_status = pull_request.calculated_review_status()
1110 pull_request_review_status = pull_request.calculated_review_status()
1107
1111
1108 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1112 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1109 # approved only if we have voting consent
1113 # approved only if we have voting consent
1110 status = ChangesetStatus.STATUS_APPROVED
1114 status = ChangesetStatus.STATUS_APPROVED
1111 else:
1115 else:
1112 status = ChangesetStatus.STATUS_REJECTED
1116 status = ChangesetStatus.STATUS_REJECTED
1113 status_lbl = ChangesetStatus.get_status_lbl(status)
1117 status_lbl = ChangesetStatus.get_status_lbl(status)
1114
1118
1115 default_message = (
1119 default_message = (
1116 'Closing with status change {transition_icon} {status}.'
1120 'Closing with status change {transition_icon} {status}.'
1117 ).format(transition_icon='>', status=status_lbl)
1121 ).format(transition_icon='>', status=status_lbl)
1118 text = message or default_message
1122 text = message or default_message
1119
1123
1120 # create a comment, and link it to new status
1124 # create a comment, and link it to new status
1121 comment = CommentsModel().create(
1125 comment = CommentsModel().create(
1122 text=text,
1126 text=text,
1123 repo=repo.repo_id,
1127 repo=repo.repo_id,
1124 user=user.user_id,
1128 user=user.user_id,
1125 pull_request=pull_request.pull_request_id,
1129 pull_request=pull_request.pull_request_id,
1126 status_change=status_lbl,
1130 status_change=status_lbl,
1127 status_change_type=status,
1131 status_change_type=status,
1128 closing_pr=True
1132 closing_pr=True
1129 )
1133 )
1130
1134
1131 # calculate old status before we change it
1135 # calculate old status before we change it
1132 old_calculated_status = pull_request.calculated_review_status()
1136 old_calculated_status = pull_request.calculated_review_status()
1133 ChangesetStatusModel().set_status(
1137 ChangesetStatusModel().set_status(
1134 repo.repo_id,
1138 repo.repo_id,
1135 status,
1139 status,
1136 user.user_id,
1140 user.user_id,
1137 comment=comment,
1141 comment=comment,
1138 pull_request=pull_request.pull_request_id
1142 pull_request=pull_request.pull_request_id
1139 )
1143 )
1140
1144
1141 Session().flush()
1145 Session().flush()
1142 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1146 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1143 # we now calculate the status of pull request again, and based on that
1147 # we now calculate the status of pull request again, and based on that
1144 # calculation trigger status change. This might happen in cases
1148 # calculation trigger status change. This might happen in cases
1145 # that non-reviewer admin closes a pr, which means his vote doesn't
1149 # that non-reviewer admin closes a pr, which means his vote doesn't
1146 # change the status, while if he's a reviewer this might change it.
1150 # change the status, while if he's a reviewer this might change it.
1147 calculated_status = pull_request.calculated_review_status()
1151 calculated_status = pull_request.calculated_review_status()
1148 if old_calculated_status != calculated_status:
1152 if old_calculated_status != calculated_status:
1149 self._trigger_pull_request_hook(
1153 self._trigger_pull_request_hook(
1150 pull_request, user, 'review_status_change')
1154 pull_request, user, 'review_status_change')
1151
1155
1152 # finally close the PR
1156 # finally close the PR
1153 PullRequestModel().close_pull_request(
1157 PullRequestModel().close_pull_request(
1154 pull_request.pull_request_id, user)
1158 pull_request.pull_request_id, user)
1155
1159
1156 return comment, status
1160 return comment, status
1157
1161
1158 def merge_status(self, pull_request, translator=None):
1162 def merge_status(self, pull_request, translator=None):
1159 _ = translator or get_current_request().translate
1163 _ = translator or get_current_request().translate
1160
1164
1161 if not self._is_merge_enabled(pull_request):
1165 if not self._is_merge_enabled(pull_request):
1162 return False, _('Server-side pull request merging is disabled.')
1166 return False, _('Server-side pull request merging is disabled.')
1163 if pull_request.is_closed():
1167 if pull_request.is_closed():
1164 return False, _('This pull request is closed.')
1168 return False, _('This pull request is closed.')
1165 merge_possible, msg = self._check_repo_requirements(
1169 merge_possible, msg = self._check_repo_requirements(
1166 target=pull_request.target_repo, source=pull_request.source_repo,
1170 target=pull_request.target_repo, source=pull_request.source_repo,
1167 translator=_)
1171 translator=_)
1168 if not merge_possible:
1172 if not merge_possible:
1169 return merge_possible, msg
1173 return merge_possible, msg
1170
1174
1171 try:
1175 try:
1172 resp = self._try_merge(pull_request)
1176 resp = self._try_merge(pull_request)
1173 log.debug("Merge response: %s", resp)
1177 log.debug("Merge response: %s", resp)
1174 status = resp.possible, self.merge_status_message(
1178 status = resp.possible, self.merge_status_message(
1175 resp.failure_reason)
1179 resp.failure_reason)
1176 except NotImplementedError:
1180 except NotImplementedError:
1177 status = False, _('Pull request merging is not supported.')
1181 status = False, _('Pull request merging is not supported.')
1178
1182
1179 return status
1183 return status
1180
1184
1181 def _check_repo_requirements(self, target, source, translator):
1185 def _check_repo_requirements(self, target, source, translator):
1182 """
1186 """
1183 Check if `target` and `source` have compatible requirements.
1187 Check if `target` and `source` have compatible requirements.
1184
1188
1185 Currently this is just checking for largefiles.
1189 Currently this is just checking for largefiles.
1186 """
1190 """
1187 _ = translator
1191 _ = translator
1188 target_has_largefiles = self._has_largefiles(target)
1192 target_has_largefiles = self._has_largefiles(target)
1189 source_has_largefiles = self._has_largefiles(source)
1193 source_has_largefiles = self._has_largefiles(source)
1190 merge_possible = True
1194 merge_possible = True
1191 message = u''
1195 message = u''
1192
1196
1193 if target_has_largefiles != source_has_largefiles:
1197 if target_has_largefiles != source_has_largefiles:
1194 merge_possible = False
1198 merge_possible = False
1195 if source_has_largefiles:
1199 if source_has_largefiles:
1196 message = _(
1200 message = _(
1197 'Target repository large files support is disabled.')
1201 'Target repository large files support is disabled.')
1198 else:
1202 else:
1199 message = _(
1203 message = _(
1200 'Source repository large files support is disabled.')
1204 'Source repository large files support is disabled.')
1201
1205
1202 return merge_possible, message
1206 return merge_possible, message
1203
1207
1204 def _has_largefiles(self, repo):
1208 def _has_largefiles(self, repo):
1205 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1209 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1206 'extensions', 'largefiles')
1210 'extensions', 'largefiles')
1207 return largefiles_ui and largefiles_ui[0].active
1211 return largefiles_ui and largefiles_ui[0].active
1208
1212
1209 def _try_merge(self, pull_request):
1213 def _try_merge(self, pull_request):
1210 """
1214 """
1211 Try to merge the pull request and return the merge status.
1215 Try to merge the pull request and return the merge status.
1212 """
1216 """
1213 log.debug(
1217 log.debug(
1214 "Trying out if the pull request %s can be merged.",
1218 "Trying out if the pull request %s can be merged.",
1215 pull_request.pull_request_id)
1219 pull_request.pull_request_id)
1216 target_vcs = pull_request.target_repo.scm_instance()
1220 target_vcs = pull_request.target_repo.scm_instance()
1217
1221
1218 # Refresh the target reference.
1222 # Refresh the target reference.
1219 try:
1223 try:
1220 target_ref = self._refresh_reference(
1224 target_ref = self._refresh_reference(
1221 pull_request.target_ref_parts, target_vcs)
1225 pull_request.target_ref_parts, target_vcs)
1222 except CommitDoesNotExistError:
1226 except CommitDoesNotExistError:
1223 merge_state = MergeResponse(
1227 merge_state = MergeResponse(
1224 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1228 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1225 return merge_state
1229 return merge_state
1226
1230
1227 target_locked = pull_request.target_repo.locked
1231 target_locked = pull_request.target_repo.locked
1228 if target_locked and target_locked[0]:
1232 if target_locked and target_locked[0]:
1229 log.debug("The target repository is locked.")
1233 log.debug("The target repository is locked.")
1230 merge_state = MergeResponse(
1234 merge_state = MergeResponse(
1231 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1235 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1232 elif self._needs_merge_state_refresh(pull_request, target_ref):
1236 elif self._needs_merge_state_refresh(pull_request, target_ref):
1233 log.debug("Refreshing the merge status of the repository.")
1237 log.debug("Refreshing the merge status of the repository.")
1234 merge_state = self._refresh_merge_state(
1238 merge_state = self._refresh_merge_state(
1235 pull_request, target_vcs, target_ref)
1239 pull_request, target_vcs, target_ref)
1236 else:
1240 else:
1237 possible = pull_request.\
1241 possible = pull_request.\
1238 last_merge_status == MergeFailureReason.NONE
1242 last_merge_status == MergeFailureReason.NONE
1239 merge_state = MergeResponse(
1243 merge_state = MergeResponse(
1240 possible, False, None, pull_request.last_merge_status)
1244 possible, False, None, pull_request.last_merge_status)
1241
1245
1242 return merge_state
1246 return merge_state
1243
1247
1244 def _refresh_reference(self, reference, vcs_repository):
1248 def _refresh_reference(self, reference, vcs_repository):
1245 if reference.type in ('branch', 'book'):
1249 if reference.type in ('branch', 'book'):
1246 name_or_id = reference.name
1250 name_or_id = reference.name
1247 else:
1251 else:
1248 name_or_id = reference.commit_id
1252 name_or_id = reference.commit_id
1249 refreshed_commit = vcs_repository.get_commit(name_or_id)
1253 refreshed_commit = vcs_repository.get_commit(name_or_id)
1250 refreshed_reference = Reference(
1254 refreshed_reference = Reference(
1251 reference.type, reference.name, refreshed_commit.raw_id)
1255 reference.type, reference.name, refreshed_commit.raw_id)
1252 return refreshed_reference
1256 return refreshed_reference
1253
1257
1254 def _needs_merge_state_refresh(self, pull_request, target_reference):
1258 def _needs_merge_state_refresh(self, pull_request, target_reference):
1255 return not(
1259 return not(
1256 pull_request.revisions and
1260 pull_request.revisions and
1257 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1261 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1258 target_reference.commit_id == pull_request._last_merge_target_rev)
1262 target_reference.commit_id == pull_request._last_merge_target_rev)
1259
1263
1260 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1264 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1261 workspace_id = self._workspace_id(pull_request)
1265 workspace_id = self._workspace_id(pull_request)
1262 source_vcs = pull_request.source_repo.scm_instance()
1266 source_vcs = pull_request.source_repo.scm_instance()
1263 use_rebase = self._use_rebase_for_merging(pull_request)
1267 use_rebase = self._use_rebase_for_merging(pull_request)
1264 close_branch = self._close_branch_before_merging(pull_request)
1268 close_branch = self._close_branch_before_merging(pull_request)
1265 merge_state = target_vcs.merge(
1269 merge_state = target_vcs.merge(
1266 target_reference, source_vcs, pull_request.source_ref_parts,
1270 target_reference, source_vcs, pull_request.source_ref_parts,
1267 workspace_id, dry_run=True, use_rebase=use_rebase,
1271 workspace_id, dry_run=True, use_rebase=use_rebase,
1268 close_branch=close_branch)
1272 close_branch=close_branch)
1269
1273
1270 # Do not store the response if there was an unknown error.
1274 # Do not store the response if there was an unknown error.
1271 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1275 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1272 pull_request._last_merge_source_rev = \
1276 pull_request._last_merge_source_rev = \
1273 pull_request.source_ref_parts.commit_id
1277 pull_request.source_ref_parts.commit_id
1274 pull_request._last_merge_target_rev = target_reference.commit_id
1278 pull_request._last_merge_target_rev = target_reference.commit_id
1275 pull_request.last_merge_status = merge_state.failure_reason
1279 pull_request.last_merge_status = merge_state.failure_reason
1276 pull_request.shadow_merge_ref = merge_state.merge_ref
1280 pull_request.shadow_merge_ref = merge_state.merge_ref
1277 Session().add(pull_request)
1281 Session().add(pull_request)
1278 Session().commit()
1282 Session().commit()
1279
1283
1280 return merge_state
1284 return merge_state
1281
1285
1282 def _workspace_id(self, pull_request):
1286 def _workspace_id(self, pull_request):
1283 workspace_id = 'pr-%s' % pull_request.pull_request_id
1287 workspace_id = 'pr-%s' % pull_request.pull_request_id
1284 return workspace_id
1288 return workspace_id
1285
1289
1286 def merge_status_message(self, status_code):
1290 def merge_status_message(self, status_code):
1287 """
1291 """
1288 Return a human friendly error message for the given merge status code.
1292 Return a human friendly error message for the given merge status code.
1289 """
1293 """
1290 return self.MERGE_STATUS_MESSAGES[status_code]
1294 return self.MERGE_STATUS_MESSAGES[status_code]
1291
1295
1292 def generate_repo_data(self, repo, commit_id=None, branch=None,
1296 def generate_repo_data(self, repo, commit_id=None, branch=None,
1293 bookmark=None, translator=None):
1297 bookmark=None, translator=None):
1294
1298
1295 all_refs, selected_ref = \
1299 all_refs, selected_ref = \
1296 self._get_repo_pullrequest_sources(
1300 self._get_repo_pullrequest_sources(
1297 repo.scm_instance(), commit_id=commit_id,
1301 repo.scm_instance(), commit_id=commit_id,
1298 branch=branch, bookmark=bookmark, translator=translator)
1302 branch=branch, bookmark=bookmark, translator=translator)
1299
1303
1300 refs_select2 = []
1304 refs_select2 = []
1301 for element in all_refs:
1305 for element in all_refs:
1302 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1306 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1303 refs_select2.append({'text': element[1], 'children': children})
1307 refs_select2.append({'text': element[1], 'children': children})
1304
1308
1305 return {
1309 return {
1306 'user': {
1310 'user': {
1307 'user_id': repo.user.user_id,
1311 'user_id': repo.user.user_id,
1308 'username': repo.user.username,
1312 'username': repo.user.username,
1309 'firstname': repo.user.first_name,
1313 'firstname': repo.user.first_name,
1310 'lastname': repo.user.last_name,
1314 'lastname': repo.user.last_name,
1311 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1315 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1312 },
1316 },
1313 'description': h.chop_at_smart(repo.description_safe, '\n'),
1317 'description': h.chop_at_smart(repo.description_safe, '\n'),
1314 'refs': {
1318 'refs': {
1315 'all_refs': all_refs,
1319 'all_refs': all_refs,
1316 'selected_ref': selected_ref,
1320 'selected_ref': selected_ref,
1317 'select2_refs': refs_select2
1321 'select2_refs': refs_select2
1318 }
1322 }
1319 }
1323 }
1320
1324
1321 def generate_pullrequest_title(self, source, source_ref, target):
1325 def generate_pullrequest_title(self, source, source_ref, target):
1322 return u'{source}#{at_ref} to {target}'.format(
1326 return u'{source}#{at_ref} to {target}'.format(
1323 source=source,
1327 source=source,
1324 at_ref=source_ref,
1328 at_ref=source_ref,
1325 target=target,
1329 target=target,
1326 )
1330 )
1327
1331
1328 def _cleanup_merge_workspace(self, pull_request):
1332 def _cleanup_merge_workspace(self, pull_request):
1329 # Merging related cleanup
1333 # Merging related cleanup
1330 target_scm = pull_request.target_repo.scm_instance()
1334 target_scm = pull_request.target_repo.scm_instance()
1331 workspace_id = 'pr-%s' % pull_request.pull_request_id
1335 workspace_id = 'pr-%s' % pull_request.pull_request_id
1332
1336
1333 try:
1337 try:
1334 target_scm.cleanup_merge_workspace(workspace_id)
1338 target_scm.cleanup_merge_workspace(workspace_id)
1335 except NotImplementedError:
1339 except NotImplementedError:
1336 pass
1340 pass
1337
1341
1338 def _get_repo_pullrequest_sources(
1342 def _get_repo_pullrequest_sources(
1339 self, repo, commit_id=None, branch=None, bookmark=None,
1343 self, repo, commit_id=None, branch=None, bookmark=None,
1340 translator=None):
1344 translator=None):
1341 """
1345 """
1342 Return a structure with repo's interesting commits, suitable for
1346 Return a structure with repo's interesting commits, suitable for
1343 the selectors in pullrequest controller
1347 the selectors in pullrequest controller
1344
1348
1345 :param commit_id: a commit that must be in the list somehow
1349 :param commit_id: a commit that must be in the list somehow
1346 and selected by default
1350 and selected by default
1347 :param branch: a branch that must be in the list and selected
1351 :param branch: a branch that must be in the list and selected
1348 by default - even if closed
1352 by default - even if closed
1349 :param bookmark: a bookmark that must be in the list and selected
1353 :param bookmark: a bookmark that must be in the list and selected
1350 """
1354 """
1351 _ = translator or get_current_request().translate
1355 _ = translator or get_current_request().translate
1352
1356
1353 commit_id = safe_str(commit_id) if commit_id else None
1357 commit_id = safe_str(commit_id) if commit_id else None
1354 branch = safe_str(branch) if branch else None
1358 branch = safe_str(branch) if branch else None
1355 bookmark = safe_str(bookmark) if bookmark else None
1359 bookmark = safe_str(bookmark) if bookmark else None
1356
1360
1357 selected = None
1361 selected = None
1358
1362
1359 # order matters: first source that has commit_id in it will be selected
1363 # order matters: first source that has commit_id in it will be selected
1360 sources = []
1364 sources = []
1361 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1365 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1362 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1366 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1363
1367
1364 if commit_id:
1368 if commit_id:
1365 ref_commit = (h.short_id(commit_id), commit_id)
1369 ref_commit = (h.short_id(commit_id), commit_id)
1366 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1370 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1367
1371
1368 sources.append(
1372 sources.append(
1369 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1373 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1370 )
1374 )
1371
1375
1372 groups = []
1376 groups = []
1373 for group_key, ref_list, group_name, match in sources:
1377 for group_key, ref_list, group_name, match in sources:
1374 group_refs = []
1378 group_refs = []
1375 for ref_name, ref_id in ref_list:
1379 for ref_name, ref_id in ref_list:
1376 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1380 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1377 group_refs.append((ref_key, ref_name))
1381 group_refs.append((ref_key, ref_name))
1378
1382
1379 if not selected:
1383 if not selected:
1380 if set([commit_id, match]) & set([ref_id, ref_name]):
1384 if set([commit_id, match]) & set([ref_id, ref_name]):
1381 selected = ref_key
1385 selected = ref_key
1382
1386
1383 if group_refs:
1387 if group_refs:
1384 groups.append((group_refs, group_name))
1388 groups.append((group_refs, group_name))
1385
1389
1386 if not selected:
1390 if not selected:
1387 ref = commit_id or branch or bookmark
1391 ref = commit_id or branch or bookmark
1388 if ref:
1392 if ref:
1389 raise CommitDoesNotExistError(
1393 raise CommitDoesNotExistError(
1390 'No commit refs could be found matching: %s' % ref)
1394 'No commit refs could be found matching: %s' % ref)
1391 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1395 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1392 selected = 'branch:%s:%s' % (
1396 selected = 'branch:%s:%s' % (
1393 repo.DEFAULT_BRANCH_NAME,
1397 repo.DEFAULT_BRANCH_NAME,
1394 repo.branches[repo.DEFAULT_BRANCH_NAME]
1398 repo.branches[repo.DEFAULT_BRANCH_NAME]
1395 )
1399 )
1396 elif repo.commit_ids:
1400 elif repo.commit_ids:
1397 rev = repo.commit_ids[0]
1401 rev = repo.commit_ids[0]
1398 selected = 'rev:%s:%s' % (rev, rev)
1402 selected = 'rev:%s:%s' % (rev, rev)
1399 else:
1403 else:
1400 raise EmptyRepositoryError()
1404 raise EmptyRepositoryError()
1401 return groups, selected
1405 return groups, selected
1402
1406
1403 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1407 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1404 return self._get_diff_from_pr_or_version(
1408 return self._get_diff_from_pr_or_version(
1405 source_repo, source_ref_id, target_ref_id, context=context)
1409 source_repo, source_ref_id, target_ref_id, context=context)
1406
1410
1407 def _get_diff_from_pr_or_version(
1411 def _get_diff_from_pr_or_version(
1408 self, source_repo, source_ref_id, target_ref_id, context):
1412 self, source_repo, source_ref_id, target_ref_id, context):
1409 target_commit = source_repo.get_commit(
1413 target_commit = source_repo.get_commit(
1410 commit_id=safe_str(target_ref_id))
1414 commit_id=safe_str(target_ref_id))
1411 source_commit = source_repo.get_commit(
1415 source_commit = source_repo.get_commit(
1412 commit_id=safe_str(source_ref_id))
1416 commit_id=safe_str(source_ref_id))
1413 if isinstance(source_repo, Repository):
1417 if isinstance(source_repo, Repository):
1414 vcs_repo = source_repo.scm_instance()
1418 vcs_repo = source_repo.scm_instance()
1415 else:
1419 else:
1416 vcs_repo = source_repo
1420 vcs_repo = source_repo
1417
1421
1418 # TODO: johbo: In the context of an update, we cannot reach
1422 # TODO: johbo: In the context of an update, we cannot reach
1419 # the old commit anymore with our normal mechanisms. It needs
1423 # the old commit anymore with our normal mechanisms. It needs
1420 # some sort of special support in the vcs layer to avoid this
1424 # some sort of special support in the vcs layer to avoid this
1421 # workaround.
1425 # workaround.
1422 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1426 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1423 vcs_repo.alias == 'git'):
1427 vcs_repo.alias == 'git'):
1424 source_commit.raw_id = safe_str(source_ref_id)
1428 source_commit.raw_id = safe_str(source_ref_id)
1425
1429
1426 log.debug('calculating diff between '
1430 log.debug('calculating diff between '
1427 'source_ref:%s and target_ref:%s for repo `%s`',
1431 'source_ref:%s and target_ref:%s for repo `%s`',
1428 target_ref_id, source_ref_id,
1432 target_ref_id, source_ref_id,
1429 safe_unicode(vcs_repo.path))
1433 safe_unicode(vcs_repo.path))
1430
1434
1431 vcs_diff = vcs_repo.get_diff(
1435 vcs_diff = vcs_repo.get_diff(
1432 commit1=target_commit, commit2=source_commit, context=context)
1436 commit1=target_commit, commit2=source_commit, context=context)
1433 return vcs_diff
1437 return vcs_diff
1434
1438
1435 def _is_merge_enabled(self, pull_request):
1439 def _is_merge_enabled(self, pull_request):
1436 return self._get_general_setting(
1440 return self._get_general_setting(
1437 pull_request, 'rhodecode_pr_merge_enabled')
1441 pull_request, 'rhodecode_pr_merge_enabled')
1438
1442
1439 def _use_rebase_for_merging(self, pull_request):
1443 def _use_rebase_for_merging(self, pull_request):
1440 repo_type = pull_request.target_repo.repo_type
1444 repo_type = pull_request.target_repo.repo_type
1441 if repo_type == 'hg':
1445 if repo_type == 'hg':
1442 return self._get_general_setting(
1446 return self._get_general_setting(
1443 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1447 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1444 elif repo_type == 'git':
1448 elif repo_type == 'git':
1445 return self._get_general_setting(
1449 return self._get_general_setting(
1446 pull_request, 'rhodecode_git_use_rebase_for_merging')
1450 pull_request, 'rhodecode_git_use_rebase_for_merging')
1447
1451
1448 return False
1452 return False
1449
1453
1450 def _close_branch_before_merging(self, pull_request):
1454 def _close_branch_before_merging(self, pull_request):
1451 repo_type = pull_request.target_repo.repo_type
1455 repo_type = pull_request.target_repo.repo_type
1452 if repo_type == 'hg':
1456 if repo_type == 'hg':
1453 return self._get_general_setting(
1457 return self._get_general_setting(
1454 pull_request, 'rhodecode_hg_close_branch_before_merging')
1458 pull_request, 'rhodecode_hg_close_branch_before_merging')
1455 elif repo_type == 'git':
1459 elif repo_type == 'git':
1456 return self._get_general_setting(
1460 return self._get_general_setting(
1457 pull_request, 'rhodecode_git_close_branch_before_merging')
1461 pull_request, 'rhodecode_git_close_branch_before_merging')
1458
1462
1459 return False
1463 return False
1460
1464
1461 def _get_general_setting(self, pull_request, settings_key, default=False):
1465 def _get_general_setting(self, pull_request, settings_key, default=False):
1462 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1466 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1463 settings = settings_model.get_general_settings()
1467 settings = settings_model.get_general_settings()
1464 return settings.get(settings_key, default)
1468 return settings.get(settings_key, default)
1465
1469
1466 def _log_audit_action(self, action, action_data, user, pull_request):
1470 def _log_audit_action(self, action, action_data, user, pull_request):
1467 audit_logger.store(
1471 audit_logger.store(
1468 action=action,
1472 action=action,
1469 action_data=action_data,
1473 action_data=action_data,
1470 user=user,
1474 user=user,
1471 repo=pull_request.target_repo)
1475 repo=pull_request.target_repo)
1472
1476
1473 def get_reviewer_functions(self):
1477 def get_reviewer_functions(self):
1474 """
1478 """
1475 Fetches functions for validation and fetching default reviewers.
1479 Fetches functions for validation and fetching default reviewers.
1476 If available we use the EE package, else we fallback to CE
1480 If available we use the EE package, else we fallback to CE
1477 package functions
1481 package functions
1478 """
1482 """
1479 try:
1483 try:
1480 from rc_reviewers.utils import get_default_reviewers_data
1484 from rc_reviewers.utils import get_default_reviewers_data
1481 from rc_reviewers.utils import validate_default_reviewers
1485 from rc_reviewers.utils import validate_default_reviewers
1482 except ImportError:
1486 except ImportError:
1483 from rhodecode.apps.repository.utils import \
1487 from rhodecode.apps.repository.utils import \
1484 get_default_reviewers_data
1488 get_default_reviewers_data
1485 from rhodecode.apps.repository.utils import \
1489 from rhodecode.apps.repository.utils import \
1486 validate_default_reviewers
1490 validate_default_reviewers
1487
1491
1488 return get_default_reviewers_data, validate_default_reviewers
1492 return get_default_reviewers_data, validate_default_reviewers
1489
1493
1490
1494
1491 class MergeCheck(object):
1495 class MergeCheck(object):
1492 """
1496 """
1493 Perform Merge Checks and returns a check object which stores information
1497 Perform Merge Checks and returns a check object which stores information
1494 about merge errors, and merge conditions
1498 about merge errors, and merge conditions
1495 """
1499 """
1496 TODO_CHECK = 'todo'
1500 TODO_CHECK = 'todo'
1497 PERM_CHECK = 'perm'
1501 PERM_CHECK = 'perm'
1498 REVIEW_CHECK = 'review'
1502 REVIEW_CHECK = 'review'
1499 MERGE_CHECK = 'merge'
1503 MERGE_CHECK = 'merge'
1500
1504
1501 def __init__(self):
1505 def __init__(self):
1502 self.review_status = None
1506 self.review_status = None
1503 self.merge_possible = None
1507 self.merge_possible = None
1504 self.merge_msg = ''
1508 self.merge_msg = ''
1505 self.failed = None
1509 self.failed = None
1506 self.errors = []
1510 self.errors = []
1507 self.error_details = OrderedDict()
1511 self.error_details = OrderedDict()
1508
1512
1509 def push_error(self, error_type, message, error_key, details):
1513 def push_error(self, error_type, message, error_key, details):
1510 self.failed = True
1514 self.failed = True
1511 self.errors.append([error_type, message])
1515 self.errors.append([error_type, message])
1512 self.error_details[error_key] = dict(
1516 self.error_details[error_key] = dict(
1513 details=details,
1517 details=details,
1514 error_type=error_type,
1518 error_type=error_type,
1515 message=message
1519 message=message
1516 )
1520 )
1517
1521
1518 @classmethod
1522 @classmethod
1519 def validate(cls, pull_request, user, translator, fail_early=False):
1523 def validate(cls, pull_request, user, translator, fail_early=False):
1520 _ = translator
1524 _ = translator
1521 merge_check = cls()
1525 merge_check = cls()
1522
1526
1523 # permissions to merge
1527 # permissions to merge
1524 user_allowed_to_merge = PullRequestModel().check_user_merge(
1528 user_allowed_to_merge = PullRequestModel().check_user_merge(
1525 pull_request, user)
1529 pull_request, user)
1526 if not user_allowed_to_merge:
1530 if not user_allowed_to_merge:
1527 log.debug("MergeCheck: cannot merge, approval is pending.")
1531 log.debug("MergeCheck: cannot merge, approval is pending.")
1528
1532
1529 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1533 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1530 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1534 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1531 if fail_early:
1535 if fail_early:
1532 return merge_check
1536 return merge_check
1533
1537
1534 # review status, must be always present
1538 # review status, must be always present
1535 review_status = pull_request.calculated_review_status()
1539 review_status = pull_request.calculated_review_status()
1536 merge_check.review_status = review_status
1540 merge_check.review_status = review_status
1537
1541
1538 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1542 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1539 if not status_approved:
1543 if not status_approved:
1540 log.debug("MergeCheck: cannot merge, approval is pending.")
1544 log.debug("MergeCheck: cannot merge, approval is pending.")
1541
1545
1542 msg = _('Pull request reviewer approval is pending.')
1546 msg = _('Pull request reviewer approval is pending.')
1543
1547
1544 merge_check.push_error(
1548 merge_check.push_error(
1545 'warning', msg, cls.REVIEW_CHECK, review_status)
1549 'warning', msg, cls.REVIEW_CHECK, review_status)
1546
1550
1547 if fail_early:
1551 if fail_early:
1548 return merge_check
1552 return merge_check
1549
1553
1550 # left over TODOs
1554 # left over TODOs
1551 todos = CommentsModel().get_unresolved_todos(pull_request)
1555 todos = CommentsModel().get_unresolved_todos(pull_request)
1552 if todos:
1556 if todos:
1553 log.debug("MergeCheck: cannot merge, {} "
1557 log.debug("MergeCheck: cannot merge, {} "
1554 "unresolved todos left.".format(len(todos)))
1558 "unresolved todos left.".format(len(todos)))
1555
1559
1556 if len(todos) == 1:
1560 if len(todos) == 1:
1557 msg = _('Cannot merge, {} TODO still not resolved.').format(
1561 msg = _('Cannot merge, {} TODO still not resolved.').format(
1558 len(todos))
1562 len(todos))
1559 else:
1563 else:
1560 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1564 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1561 len(todos))
1565 len(todos))
1562
1566
1563 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1567 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1564
1568
1565 if fail_early:
1569 if fail_early:
1566 return merge_check
1570 return merge_check
1567
1571
1568 # merge possible
1572 # merge possible
1569 merge_status, msg = PullRequestModel().merge_status(
1573 merge_status, msg = PullRequestModel().merge_status(
1570 pull_request, translator=translator)
1574 pull_request, translator=translator)
1571 merge_check.merge_possible = merge_status
1575 merge_check.merge_possible = merge_status
1572 merge_check.merge_msg = msg
1576 merge_check.merge_msg = msg
1573 if not merge_status:
1577 if not merge_status:
1574 log.debug(
1578 log.debug(
1575 "MergeCheck: cannot merge, pull request merge not possible.")
1579 "MergeCheck: cannot merge, pull request merge not possible.")
1576 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1580 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1577
1581
1578 if fail_early:
1582 if fail_early:
1579 return merge_check
1583 return merge_check
1580
1584
1581 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1585 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1582 return merge_check
1586 return merge_check
1583
1587
1584 @classmethod
1588 @classmethod
1585 def get_merge_conditions(cls, pull_request, translator):
1589 def get_merge_conditions(cls, pull_request, translator):
1586 _ = translator
1590 _ = translator
1587 merge_details = {}
1591 merge_details = {}
1588
1592
1589 model = PullRequestModel()
1593 model = PullRequestModel()
1590 use_rebase = model._use_rebase_for_merging(pull_request)
1594 use_rebase = model._use_rebase_for_merging(pull_request)
1591
1595
1592 if use_rebase:
1596 if use_rebase:
1593 merge_details['merge_strategy'] = dict(
1597 merge_details['merge_strategy'] = dict(
1594 details={},
1598 details={},
1595 message=_('Merge strategy: rebase')
1599 message=_('Merge strategy: rebase')
1596 )
1600 )
1597 else:
1601 else:
1598 merge_details['merge_strategy'] = dict(
1602 merge_details['merge_strategy'] = dict(
1599 details={},
1603 details={},
1600 message=_('Merge strategy: explicit merge commit')
1604 message=_('Merge strategy: explicit merge commit')
1601 )
1605 )
1602
1606
1603 close_branch = model._close_branch_before_merging(pull_request)
1607 close_branch = model._close_branch_before_merging(pull_request)
1604 if close_branch:
1608 if close_branch:
1605 repo_type = pull_request.target_repo.repo_type
1609 repo_type = pull_request.target_repo.repo_type
1606 if repo_type == 'hg':
1610 if repo_type == 'hg':
1607 close_msg = _('Source branch will be closed after merge.')
1611 close_msg = _('Source branch will be closed after merge.')
1608 elif repo_type == 'git':
1612 elif repo_type == 'git':
1609 close_msg = _('Source branch will be deleted after merge.')
1613 close_msg = _('Source branch will be deleted after merge.')
1610
1614
1611 merge_details['close_branch'] = dict(
1615 merge_details['close_branch'] = dict(
1612 details={},
1616 details={},
1613 message=close_msg
1617 message=close_msg
1614 )
1618 )
1615
1619
1616 return merge_details
1620 return merge_details
1617
1621
1618 ChangeTuple = collections.namedtuple(
1622 ChangeTuple = collections.namedtuple(
1619 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1623 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1620
1624
1621 FileChangeTuple = collections.namedtuple(
1625 FileChangeTuple = collections.namedtuple(
1622 'FileChangeTuple', ['added', 'modified', 'removed'])
1626 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now