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