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