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