##// END OF EJS Templates
pull-requests: expose commit versions in the pull-request commit list. Fixes #5642
milka -
r4615:ca0827b2 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1854 +1,1857 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
29
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 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
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 statuses=statuses, offset=start, length=limit,
83 statuses=statuses, offset=start, length=limit,
84 order_by=order_by, order_dir=order_dir)
84 order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 opened_by=opened_by)
87 opened_by=opened_by)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by,
93 order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
96 statuses=statuses, opened_by=opened_by)
97 else:
97 else:
98 pull_requests = PullRequestModel().get_all(
98 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
100 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
101 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
102 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
104 opened_by=opened_by)
105
105
106 data = []
106 data = []
107 comments_model = CommentsModel()
107 comments_model = CommentsModel()
108 for pr in pull_requests:
108 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr,
110 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
111 include_drafts=False, count_only=True)
112
112
113 data.append({
113 data.append({
114 'name': _render('pullrequest_name',
114 'name': _render('pullrequest_name',
115 pr.pull_request_id, pr.pull_request_state,
115 pr.pull_request_id, pr.pull_request_state,
116 pr.work_in_progress, pr.target_repo.repo_name,
116 pr.work_in_progress, pr.target_repo.repo_name,
117 short=True),
117 short=True),
118 'name_raw': pr.pull_request_id,
118 'name_raw': pr.pull_request_id,
119 'status': _render('pullrequest_status',
119 'status': _render('pullrequest_status',
120 pr.calculated_review_status()),
120 pr.calculated_review_status()),
121 'title': _render('pullrequest_title', pr.title, pr.description),
121 'title': _render('pullrequest_title', pr.title, pr.description),
122 'description': h.escape(pr.description),
122 'description': h.escape(pr.description),
123 'updated_on': _render('pullrequest_updated_on',
123 'updated_on': _render('pullrequest_updated_on',
124 h.datetime_to_time(pr.updated_on),
124 h.datetime_to_time(pr.updated_on),
125 pr.versions_count),
125 pr.versions_count),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 'created_on': _render('pullrequest_updated_on',
127 'created_on': _render('pullrequest_updated_on',
128 h.datetime_to_time(pr.created_on)),
128 h.datetime_to_time(pr.created_on)),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
130 'state': pr.pull_request_state,
130 'state': pr.pull_request_state,
131 'author': _render('pullrequest_author',
131 'author': _render('pullrequest_author',
132 pr.author.full_contact, ),
132 pr.author.full_contact, ),
133 'author_raw': pr.author.full_name,
133 'author_raw': pr.author.full_name,
134 'comments': _render('pullrequest_comments', comments_count),
134 'comments': _render('pullrequest_comments', comments_count),
135 'comments_raw': comments_count,
135 'comments_raw': comments_count,
136 'closed': pr.is_closed(),
136 'closed': pr.is_closed(),
137 })
137 })
138
138
139 data = ({
139 data = ({
140 'draw': draw,
140 'draw': draw,
141 'data': data,
141 'data': data,
142 'recordsTotal': pull_requests_total_count,
142 'recordsTotal': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
144 })
144 })
145 return data
145 return data
146
146
147 @LoginRequired()
147 @LoginRequired()
148 @HasRepoPermissionAnyDecorator(
148 @HasRepoPermissionAnyDecorator(
149 'repository.read', 'repository.write', 'repository.admin')
149 'repository.read', 'repository.write', 'repository.admin')
150 def pull_request_list(self):
150 def pull_request_list(self):
151 c = self.load_default_context()
151 c = self.load_default_context()
152
152
153 req_get = self.request.GET
153 req_get = self.request.GET
154 c.source = str2bool(req_get.get('source'))
154 c.source = str2bool(req_get.get('source'))
155 c.closed = str2bool(req_get.get('closed'))
155 c.closed = str2bool(req_get.get('closed'))
156 c.my = str2bool(req_get.get('my'))
156 c.my = str2bool(req_get.get('my'))
157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159
159
160 c.active = 'open'
160 c.active = 'open'
161 if c.my:
161 if c.my:
162 c.active = 'my'
162 c.active = 'my'
163 if c.closed:
163 if c.closed:
164 c.active = 'closed'
164 c.active = 'closed'
165 if c.awaiting_review and not c.source:
165 if c.awaiting_review and not c.source:
166 c.active = 'awaiting'
166 c.active = 'awaiting'
167 if c.source and not c.awaiting_review:
167 if c.source and not c.awaiting_review:
168 c.active = 'source'
168 c.active = 'source'
169 if c.awaiting_my_review:
169 if c.awaiting_my_review:
170 c.active = 'awaiting_my'
170 c.active = 'awaiting_my'
171
171
172 return self._get_template_context(c)
172 return self._get_template_context(c)
173
173
174 @LoginRequired()
174 @LoginRequired()
175 @HasRepoPermissionAnyDecorator(
175 @HasRepoPermissionAnyDecorator(
176 'repository.read', 'repository.write', 'repository.admin')
176 'repository.read', 'repository.write', 'repository.admin')
177 def pull_request_list_data(self):
177 def pull_request_list_data(self):
178 self.load_default_context()
178 self.load_default_context()
179
179
180 # additional filters
180 # additional filters
181 req_get = self.request.GET
181 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
182 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
183 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
184 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
187
188 filter_type = 'awaiting_review' if awaiting_review \
188 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else None
190 else None
191
191
192 opened_by = None
192 opened_by = None
193 if my:
193 if my:
194 opened_by = [self._rhodecode_user.user_id]
194 opened_by = [self._rhodecode_user.user_id]
195
195
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
197 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
198 statuses = [PullRequest.STATUS_CLOSED]
199
199
200 data = self._get_pull_requests_list(
200 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
201 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
203
204 return data
204 return data
205
205
206 def _is_diff_cache_enabled(self, target_repo):
206 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
207 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
208 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
210 return caching_enabled
211
211
212 def _get_diffset(self, source_repo_name, source_repo,
212 def _get_diffset(self, source_repo_name, source_repo,
213 ancestor_commit,
213 ancestor_commit,
214 source_ref_id, target_ref_id,
214 source_ref_id, target_ref_id,
215 target_commit, source_commit, diff_limit, file_limit,
215 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
216 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217
217
218 target_commit_final = target_commit
218 target_commit_final = target_commit
219 source_commit_final = source_commit
219 source_commit_final = source_commit
220
220
221 if use_ancestor:
221 if use_ancestor:
222 # we might want to not use it for versions
222 # we might want to not use it for versions
223 target_ref_id = ancestor_commit.raw_id
223 target_ref_id = ancestor_commit.raw_id
224 target_commit_final = ancestor_commit
224 target_commit_final = ancestor_commit
225
225
226 vcs_diff = PullRequestModel().get_diff(
226 vcs_diff = PullRequestModel().get_diff(
227 source_repo, source_ref_id, target_ref_id,
227 source_repo, source_ref_id, target_ref_id,
228 hide_whitespace_changes, diff_context)
228 hide_whitespace_changes, diff_context)
229
229
230 diff_processor = diffs.DiffProcessor(
230 diff_processor = diffs.DiffProcessor(
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 file_limit=file_limit, show_full_diff=fulldiff)
232 file_limit=file_limit, show_full_diff=fulldiff)
233
233
234 _parsed = diff_processor.prepare()
234 _parsed = diff_processor.prepare()
235
235
236 diffset = codeblocks.DiffSet(
236 diffset = codeblocks.DiffSet(
237 repo_name=self.db_repo_name,
237 repo_name=self.db_repo_name,
238 source_repo_name=source_repo_name,
238 source_repo_name=source_repo_name,
239 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
239 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
241 )
241 )
242 diffset = self.path_filter.render_patchset_filtered(
242 diffset = self.path_filter.render_patchset_filtered(
243 diffset, _parsed, target_ref_id, source_ref_id)
243 diffset, _parsed, target_ref_id, source_ref_id)
244
244
245 return diffset
245 return diffset
246
246
247 def _get_range_diffset(self, source_scm, source_repo,
247 def _get_range_diffset(self, source_scm, source_repo,
248 commit1, commit2, diff_limit, file_limit,
248 commit1, commit2, diff_limit, file_limit,
249 fulldiff, hide_whitespace_changes, diff_context):
249 fulldiff, hide_whitespace_changes, diff_context):
250 vcs_diff = source_scm.get_diff(
250 vcs_diff = source_scm.get_diff(
251 commit1, commit2,
251 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
252 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
253 context=diff_context)
254
254
255 diff_processor = diffs.DiffProcessor(
255 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=fulldiff)
257 file_limit=file_limit, show_full_diff=fulldiff)
258
258
259 _parsed = diff_processor.prepare()
259 _parsed = diff_processor.prepare()
260
260
261 diffset = codeblocks.DiffSet(
261 diffset = codeblocks.DiffSet(
262 repo_name=source_repo.repo_name,
262 repo_name=source_repo.repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
265
266 diffset = self.path_filter.render_patchset_filtered(
266 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
268
269 return diffset
269 return diffset
270
270
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
272 comments_model = CommentsModel()
273
273
274 # GENERAL COMMENTS with versions #
274 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
276 q = q.order_by(ChangesetComment.comment_id.asc())
277 if not include_drafts:
277 if not include_drafts:
278 q = q.filter(ChangesetComment.draft == false())
278 q = q.filter(ChangesetComment.draft == false())
279 general_comments = q
279 general_comments = q
280
280
281 # pick comments we want to render at current version
281 # pick comments we want to render at current version
282 c.comment_versions = comments_model.aggregate_comments(
282 c.comment_versions = comments_model.aggregate_comments(
283 general_comments, versions, c.at_version_num)
283 general_comments, versions, c.at_version_num)
284
284
285 # INLINE COMMENTS with versions #
285 # INLINE COMMENTS with versions #
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 q = q.order_by(ChangesetComment.comment_id.asc())
287 q = q.order_by(ChangesetComment.comment_id.asc())
288 if not include_drafts:
288 if not include_drafts:
289 q = q.filter(ChangesetComment.draft == false())
289 q = q.filter(ChangesetComment.draft == false())
290 inline_comments = q
290 inline_comments = q
291
291
292 c.inline_versions = comments_model.aggregate_comments(
292 c.inline_versions = comments_model.aggregate_comments(
293 inline_comments, versions, c.at_version_num, inline=True)
293 inline_comments, versions, c.at_version_num, inline=True)
294
294
295 # Comments inline+general
295 # Comments inline+general
296 if c.at_version:
296 if c.at_version:
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
299 else:
299 else:
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
302
302
303 return general_comments, inline_comments
303 return general_comments, inline_comments
304
304
305 @LoginRequired()
305 @LoginRequired()
306 @HasRepoPermissionAnyDecorator(
306 @HasRepoPermissionAnyDecorator(
307 'repository.read', 'repository.write', 'repository.admin')
307 'repository.read', 'repository.write', 'repository.admin')
308 def pull_request_show(self):
308 def pull_request_show(self):
309 _ = self.request.translate
309 _ = self.request.translate
310 c = self.load_default_context()
310 c = self.load_default_context()
311
311
312 pull_request = PullRequest.get_or_404(
312 pull_request = PullRequest.get_or_404(
313 self.request.matchdict['pull_request_id'])
313 self.request.matchdict['pull_request_id'])
314 pull_request_id = pull_request.pull_request_id
314 pull_request_id = pull_request.pull_request_id
315
315
316 c.state_progressing = pull_request.is_state_changing()
316 c.state_progressing = pull_request.is_state_changing()
317 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
317 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
318
318
319 _new_state = {
319 _new_state = {
320 'created': PullRequest.STATE_CREATED,
320 'created': PullRequest.STATE_CREATED,
321 }.get(self.request.GET.get('force_state'))
321 }.get(self.request.GET.get('force_state'))
322
322
323 if c.is_super_admin and _new_state:
323 if c.is_super_admin and _new_state:
324 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
324 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
325 h.flash(
325 h.flash(
326 _('Pull Request state was force changed to `{}`').format(_new_state),
326 _('Pull Request state was force changed to `{}`').format(_new_state),
327 category='success')
327 category='success')
328 Session().commit()
328 Session().commit()
329
329
330 raise HTTPFound(h.route_path(
330 raise HTTPFound(h.route_path(
331 'pullrequest_show', repo_name=self.db_repo_name,
331 'pullrequest_show', repo_name=self.db_repo_name,
332 pull_request_id=pull_request_id))
332 pull_request_id=pull_request_id))
333
333
334 version = self.request.GET.get('version')
334 version = self.request.GET.get('version')
335 from_version = self.request.GET.get('from_version') or version
335 from_version = self.request.GET.get('from_version') or version
336 merge_checks = self.request.GET.get('merge_checks')
336 merge_checks = self.request.GET.get('merge_checks')
337 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
337 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
338 force_refresh = str2bool(self.request.GET.get('force_refresh'))
338 force_refresh = str2bool(self.request.GET.get('force_refresh'))
339 c.range_diff_on = self.request.GET.get('range-diff') == "1"
339 c.range_diff_on = self.request.GET.get('range-diff') == "1"
340
340
341 # fetch global flags of ignore ws or context lines
341 # fetch global flags of ignore ws or context lines
342 diff_context = diffs.get_diff_context(self.request)
342 diff_context = diffs.get_diff_context(self.request)
343 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
343 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
344
344
345 (pull_request_latest,
345 (pull_request_latest,
346 pull_request_at_ver,
346 pull_request_at_ver,
347 pull_request_display_obj,
347 pull_request_display_obj,
348 at_version) = PullRequestModel().get_pr_version(
348 at_version) = PullRequestModel().get_pr_version(
349 pull_request_id, version=version)
349 pull_request_id, version=version)
350
350
351 pr_closed = pull_request_latest.is_closed()
351 pr_closed = pull_request_latest.is_closed()
352
352
353 if pr_closed and (version or from_version):
353 if pr_closed and (version or from_version):
354 # not allow to browse versions for closed PR
354 # not allow to browse versions for closed PR
355 raise HTTPFound(h.route_path(
355 raise HTTPFound(h.route_path(
356 'pullrequest_show', repo_name=self.db_repo_name,
356 'pullrequest_show', repo_name=self.db_repo_name,
357 pull_request_id=pull_request_id))
357 pull_request_id=pull_request_id))
358
358
359 versions = pull_request_display_obj.versions()
359 versions = pull_request_display_obj.versions()
360
361 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
362
360 # used to store per-commit range diffs
363 # used to store per-commit range diffs
361 c.changes = collections.OrderedDict()
364 c.changes = collections.OrderedDict()
362
365
363 c.at_version = at_version
366 c.at_version = at_version
364 c.at_version_num = (at_version
367 c.at_version_num = (at_version
365 if at_version and at_version != PullRequest.LATEST_VER
368 if at_version and at_version != PullRequest.LATEST_VER
366 else None)
369 else None)
367
370
368 c.at_version_index = ChangesetComment.get_index_from_version(
371 c.at_version_index = ChangesetComment.get_index_from_version(
369 c.at_version_num, versions)
372 c.at_version_num, versions)
370
373
371 (prev_pull_request_latest,
374 (prev_pull_request_latest,
372 prev_pull_request_at_ver,
375 prev_pull_request_at_ver,
373 prev_pull_request_display_obj,
376 prev_pull_request_display_obj,
374 prev_at_version) = PullRequestModel().get_pr_version(
377 prev_at_version) = PullRequestModel().get_pr_version(
375 pull_request_id, version=from_version)
378 pull_request_id, version=from_version)
376
379
377 c.from_version = prev_at_version
380 c.from_version = prev_at_version
378 c.from_version_num = (prev_at_version
381 c.from_version_num = (prev_at_version
379 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
380 else None)
383 else None)
381 c.from_version_index = ChangesetComment.get_index_from_version(
384 c.from_version_index = ChangesetComment.get_index_from_version(
382 c.from_version_num, versions)
385 c.from_version_num, versions)
383
386
384 # define if we're in COMPARE mode or VIEW at version mode
387 # define if we're in COMPARE mode or VIEW at version mode
385 compare = at_version != prev_at_version
388 compare = at_version != prev_at_version
386
389
387 # pull_requests repo_name we opened it against
390 # pull_requests repo_name we opened it against
388 # ie. target_repo must match
391 # ie. target_repo must match
389 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
390 log.warning('Mismatch between the current repo: %s, and target %s',
393 log.warning('Mismatch between the current repo: %s, and target %s',
391 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
392 raise HTTPNotFound()
395 raise HTTPNotFound()
393
396
394 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
395
398
396 c.pull_request = pull_request_display_obj
399 c.pull_request = pull_request_display_obj
397 c.renderer = pull_request_at_ver.description_renderer or c.renderer
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
398 c.pull_request_latest = pull_request_latest
401 c.pull_request_latest = pull_request_latest
399
402
400 # inject latest version
403 # inject latest version
401 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
402 c.versions = versions + [latest_ver]
405 c.versions = versions + [latest_ver]
403
406
404 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
405 c.allowed_to_change_status = False
408 c.allowed_to_change_status = False
406 c.allowed_to_update = False
409 c.allowed_to_update = False
407 c.allowed_to_merge = False
410 c.allowed_to_merge = False
408 c.allowed_to_delete = False
411 c.allowed_to_delete = False
409 c.allowed_to_comment = False
412 c.allowed_to_comment = False
410 c.allowed_to_close = False
413 c.allowed_to_close = False
411 else:
414 else:
412 can_change_status = PullRequestModel().check_user_change_status(
415 can_change_status = PullRequestModel().check_user_change_status(
413 pull_request_at_ver, self._rhodecode_user)
416 pull_request_at_ver, self._rhodecode_user)
414 c.allowed_to_change_status = can_change_status and not pr_closed
417 c.allowed_to_change_status = can_change_status and not pr_closed
415
418
416 c.allowed_to_update = PullRequestModel().check_user_update(
419 c.allowed_to_update = PullRequestModel().check_user_update(
417 pull_request_latest, self._rhodecode_user) and not pr_closed
420 pull_request_latest, self._rhodecode_user) and not pr_closed
418 c.allowed_to_merge = PullRequestModel().check_user_merge(
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
419 pull_request_latest, self._rhodecode_user) and not pr_closed
422 pull_request_latest, self._rhodecode_user) and not pr_closed
420 c.allowed_to_delete = PullRequestModel().check_user_delete(
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
421 pull_request_latest, self._rhodecode_user) and not pr_closed
424 pull_request_latest, self._rhodecode_user) and not pr_closed
422 c.allowed_to_comment = not pr_closed
425 c.allowed_to_comment = not pr_closed
423 c.allowed_to_close = c.allowed_to_merge and not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
424
427
425 c.forbid_adding_reviewers = False
428 c.forbid_adding_reviewers = False
426
429
427 if pull_request_latest.reviewer_data and \
430 if pull_request_latest.reviewer_data and \
428 'rules' in pull_request_latest.reviewer_data:
431 'rules' in pull_request_latest.reviewer_data:
429 rules = pull_request_latest.reviewer_data['rules'] or {}
432 rules = pull_request_latest.reviewer_data['rules'] or {}
430 try:
433 try:
431 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
434 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
432 except Exception:
435 except Exception:
433 pass
436 pass
434
437
435 # check merge capabilities
438 # check merge capabilities
436 _merge_check = MergeCheck.validate(
439 _merge_check = MergeCheck.validate(
437 pull_request_latest, auth_user=self._rhodecode_user,
440 pull_request_latest, auth_user=self._rhodecode_user,
438 translator=self.request.translate,
441 translator=self.request.translate,
439 force_shadow_repo_refresh=force_refresh)
442 force_shadow_repo_refresh=force_refresh)
440
443
441 c.pr_merge_errors = _merge_check.error_details
444 c.pr_merge_errors = _merge_check.error_details
442 c.pr_merge_possible = not _merge_check.failed
445 c.pr_merge_possible = not _merge_check.failed
443 c.pr_merge_message = _merge_check.merge_msg
446 c.pr_merge_message = _merge_check.merge_msg
444 c.pr_merge_source_commit = _merge_check.source_commit
447 c.pr_merge_source_commit = _merge_check.source_commit
445 c.pr_merge_target_commit = _merge_check.target_commit
448 c.pr_merge_target_commit = _merge_check.target_commit
446
449
447 c.pr_merge_info = MergeCheck.get_merge_conditions(
450 c.pr_merge_info = MergeCheck.get_merge_conditions(
448 pull_request_latest, translator=self.request.translate)
451 pull_request_latest, translator=self.request.translate)
449
452
450 c.pull_request_review_status = _merge_check.review_status
453 c.pull_request_review_status = _merge_check.review_status
451 if merge_checks:
454 if merge_checks:
452 self.request.override_renderer = \
455 self.request.override_renderer = \
453 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
454 return self._get_template_context(c)
457 return self._get_template_context(c)
455
458
456 c.reviewers_count = pull_request.reviewers_count
459 c.reviewers_count = pull_request.reviewers_count
457 c.observers_count = pull_request.observers_count
460 c.observers_count = pull_request.observers_count
458
461
459 # reviewers and statuses
462 # reviewers and statuses
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
462 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
463
466
464 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
465 member_reviewer = h.reviewer_as_json(
468 member_reviewer = h.reviewer_as_json(
466 member, reasons=reasons, mandatory=mandatory,
469 member, reasons=reasons, mandatory=mandatory,
467 role=review_obj.role,
470 role=review_obj.role,
468 user_group=review_obj.rule_user_group_data()
471 user_group=review_obj.rule_user_group_data()
469 )
472 )
470
473
471 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
472 member_reviewer['review_status'] = current_review_status
475 member_reviewer['review_status'] = current_review_status
473 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
474 member_reviewer['allowed_to_update'] = c.allowed_to_update
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
475 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
476
479
477 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
478
481
479 for observer_obj, member in pull_request_at_ver.observers():
482 for observer_obj, member in pull_request_at_ver.observers():
480 member_observer = h.reviewer_as_json(
483 member_observer = h.reviewer_as_json(
481 member, reasons=[], mandatory=False,
484 member, reasons=[], mandatory=False,
482 role=observer_obj.role,
485 role=observer_obj.role,
483 user_group=observer_obj.rule_user_group_data()
486 user_group=observer_obj.rule_user_group_data()
484 )
487 )
485 member_observer['allowed_to_update'] = c.allowed_to_update
488 member_observer['allowed_to_update'] = c.allowed_to_update
486 c.pull_request_set_observers_data_json['observers'].append(member_observer)
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
487
490
488 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
489
492
490 general_comments, inline_comments = \
493 general_comments, inline_comments = \
491 self.register_comments_vars(c, pull_request_latest, versions)
494 self.register_comments_vars(c, pull_request_latest, versions)
492
495
493 # TODOs
496 # TODOs
494 c.unresolved_comments = CommentsModel() \
497 c.unresolved_comments = CommentsModel() \
495 .get_pull_request_unresolved_todos(pull_request_latest)
498 .get_pull_request_unresolved_todos(pull_request_latest)
496 c.resolved_comments = CommentsModel() \
499 c.resolved_comments = CommentsModel() \
497 .get_pull_request_resolved_todos(pull_request_latest)
500 .get_pull_request_resolved_todos(pull_request_latest)
498
501
499 # Drafts
502 # Drafts
500 c.draft_comments = CommentsModel().get_pull_request_drafts(
503 c.draft_comments = CommentsModel().get_pull_request_drafts(
501 self._rhodecode_db_user.user_id,
504 self._rhodecode_db_user.user_id,
502 pull_request_latest)
505 pull_request_latest)
503
506
504 # if we use version, then do not show later comments
507 # if we use version, then do not show later comments
505 # than current version
508 # than current version
506 display_inline_comments = collections.defaultdict(
509 display_inline_comments = collections.defaultdict(
507 lambda: collections.defaultdict(list))
510 lambda: collections.defaultdict(list))
508 for co in inline_comments:
511 for co in inline_comments:
509 if c.at_version_num:
512 if c.at_version_num:
510 # pick comments that are at least UPTO given version, so we
513 # pick comments that are at least UPTO given version, so we
511 # don't render comments for higher version
514 # don't render comments for higher version
512 should_render = co.pull_request_version_id and \
515 should_render = co.pull_request_version_id and \
513 co.pull_request_version_id <= c.at_version_num
516 co.pull_request_version_id <= c.at_version_num
514 else:
517 else:
515 # showing all, for 'latest'
518 # showing all, for 'latest'
516 should_render = True
519 should_render = True
517
520
518 if should_render:
521 if should_render:
519 display_inline_comments[co.f_path][co.line_no].append(co)
522 display_inline_comments[co.f_path][co.line_no].append(co)
520
523
521 # load diff data into template context, if we use compare mode then
524 # load diff data into template context, if we use compare mode then
522 # diff is calculated based on changes between versions of PR
525 # diff is calculated based on changes between versions of PR
523
526
524 source_repo = pull_request_at_ver.source_repo
527 source_repo = pull_request_at_ver.source_repo
525 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
526
529
527 target_repo = pull_request_at_ver.target_repo
530 target_repo = pull_request_at_ver.target_repo
528 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
529
532
530 if compare:
533 if compare:
531 # in compare switch the diff base to latest commit from prev version
534 # in compare switch the diff base to latest commit from prev version
532 target_ref_id = prev_pull_request_display_obj.revisions[0]
535 target_ref_id = prev_pull_request_display_obj.revisions[0]
533
536
534 # despite opening commits for bookmarks/branches/tags, we always
537 # despite opening commits for bookmarks/branches/tags, we always
535 # convert this to rev to prevent changes after bookmark or branch change
538 # convert this to rev to prevent changes after bookmark or branch change
536 c.source_ref_type = 'rev'
539 c.source_ref_type = 'rev'
537 c.source_ref = source_ref_id
540 c.source_ref = source_ref_id
538
541
539 c.target_ref_type = 'rev'
542 c.target_ref_type = 'rev'
540 c.target_ref = target_ref_id
543 c.target_ref = target_ref_id
541
544
542 c.source_repo = source_repo
545 c.source_repo = source_repo
543 c.target_repo = target_repo
546 c.target_repo = target_repo
544
547
545 c.commit_ranges = []
548 c.commit_ranges = []
546 source_commit = EmptyCommit()
549 source_commit = EmptyCommit()
547 target_commit = EmptyCommit()
550 target_commit = EmptyCommit()
548 c.missing_requirements = False
551 c.missing_requirements = False
549
552
550 source_scm = source_repo.scm_instance()
553 source_scm = source_repo.scm_instance()
551 target_scm = target_repo.scm_instance()
554 target_scm = target_repo.scm_instance()
552
555
553 shadow_scm = None
556 shadow_scm = None
554 try:
557 try:
555 shadow_scm = pull_request_latest.get_shadow_repo()
558 shadow_scm = pull_request_latest.get_shadow_repo()
556 except Exception:
559 except Exception:
557 log.debug('Failed to get shadow repo', exc_info=True)
560 log.debug('Failed to get shadow repo', exc_info=True)
558 # try first the existing source_repo, and then shadow
561 # try first the existing source_repo, and then shadow
559 # repo if we can obtain one
562 # repo if we can obtain one
560 commits_source_repo = source_scm
563 commits_source_repo = source_scm
561 if shadow_scm:
564 if shadow_scm:
562 commits_source_repo = shadow_scm
565 commits_source_repo = shadow_scm
563
566
564 c.commits_source_repo = commits_source_repo
567 c.commits_source_repo = commits_source_repo
565 c.ancestor = None # set it to None, to hide it from PR view
568 c.ancestor = None # set it to None, to hide it from PR view
566
569
567 # empty version means latest, so we keep this to prevent
570 # empty version means latest, so we keep this to prevent
568 # double caching
571 # double caching
569 version_normalized = version or PullRequest.LATEST_VER
572 version_normalized = version or PullRequest.LATEST_VER
570 from_version_normalized = from_version or PullRequest.LATEST_VER
573 from_version_normalized = from_version or PullRequest.LATEST_VER
571
574
572 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
573 cache_file_path = diff_cache_exist(
576 cache_file_path = diff_cache_exist(
574 cache_path, 'pull_request', pull_request_id, version_normalized,
577 cache_path, 'pull_request', pull_request_id, version_normalized,
575 from_version_normalized, source_ref_id, target_ref_id,
578 from_version_normalized, source_ref_id, target_ref_id,
576 hide_whitespace_changes, diff_context, c.fulldiff)
579 hide_whitespace_changes, diff_context, c.fulldiff)
577
580
578 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
579 force_recache = self.get_recache_flag()
582 force_recache = self.get_recache_flag()
580
583
581 cached_diff = None
584 cached_diff = None
582 if caching_enabled:
585 if caching_enabled:
583 cached_diff = load_cached_diff(cache_file_path)
586 cached_diff = load_cached_diff(cache_file_path)
584
587
585 has_proper_commit_cache = (
588 has_proper_commit_cache = (
586 cached_diff and cached_diff.get('commits')
589 cached_diff and cached_diff.get('commits')
587 and len(cached_diff.get('commits', [])) == 5
590 and len(cached_diff.get('commits', [])) == 5
588 and cached_diff.get('commits')[0]
591 and cached_diff.get('commits')[0]
589 and cached_diff.get('commits')[3])
592 and cached_diff.get('commits')[3])
590
593
591 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
592 diff_commit_cache = \
595 diff_commit_cache = \
593 (ancestor_commit, commit_cache, missing_requirements,
596 (ancestor_commit, commit_cache, missing_requirements,
594 source_commit, target_commit) = cached_diff['commits']
597 source_commit, target_commit) = cached_diff['commits']
595 else:
598 else:
596 # NOTE(marcink): we reach potentially unreachable errors when a PR has
599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
597 # merge errors resulting in potentially hidden commits in the shadow repo.
600 # merge errors resulting in potentially hidden commits in the shadow repo.
598 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
599 and _merge_check.merge_response
602 and _merge_check.merge_response
600 maybe_unreachable = maybe_unreachable \
603 maybe_unreachable = maybe_unreachable \
601 and _merge_check.merge_response.metadata.get('unresolved_files')
604 and _merge_check.merge_response.metadata.get('unresolved_files')
602 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
603 diff_commit_cache = \
606 diff_commit_cache = \
604 (ancestor_commit, commit_cache, missing_requirements,
607 (ancestor_commit, commit_cache, missing_requirements,
605 source_commit, target_commit) = self.get_commits(
608 source_commit, target_commit) = self.get_commits(
606 commits_source_repo,
609 commits_source_repo,
607 pull_request_at_ver,
610 pull_request_at_ver,
608 source_commit,
611 source_commit,
609 source_ref_id,
612 source_ref_id,
610 source_scm,
613 source_scm,
611 target_commit,
614 target_commit,
612 target_ref_id,
615 target_ref_id,
613 target_scm,
616 target_scm,
614 maybe_unreachable=maybe_unreachable)
617 maybe_unreachable=maybe_unreachable)
615
618
616 # register our commit range
619 # register our commit range
617 for comm in commit_cache.values():
620 for comm in commit_cache.values():
618 c.commit_ranges.append(comm)
621 c.commit_ranges.append(comm)
619
622
620 c.missing_requirements = missing_requirements
623 c.missing_requirements = missing_requirements
621 c.ancestor_commit = ancestor_commit
624 c.ancestor_commit = ancestor_commit
622 c.statuses = source_repo.statuses(
625 c.statuses = source_repo.statuses(
623 [x.raw_id for x in c.commit_ranges])
626 [x.raw_id for x in c.commit_ranges])
624
627
625 # auto collapse if we have more than limit
628 # auto collapse if we have more than limit
626 collapse_limit = diffs.DiffProcessor._collapse_commits_over
629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
627 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
628 c.compare_mode = compare
631 c.compare_mode = compare
629
632
630 # diff_limit is the old behavior, will cut off the whole diff
633 # diff_limit is the old behavior, will cut off the whole diff
631 # if the limit is applied otherwise will just hide the
634 # if the limit is applied otherwise will just hide the
632 # big files from the front-end
635 # big files from the front-end
633 diff_limit = c.visual.cut_off_limit_diff
636 diff_limit = c.visual.cut_off_limit_diff
634 file_limit = c.visual.cut_off_limit_file
637 file_limit = c.visual.cut_off_limit_file
635
638
636 c.missing_commits = False
639 c.missing_commits = False
637 if (c.missing_requirements
640 if (c.missing_requirements
638 or isinstance(source_commit, EmptyCommit)
641 or isinstance(source_commit, EmptyCommit)
639 or source_commit == target_commit):
642 or source_commit == target_commit):
640
643
641 c.missing_commits = True
644 c.missing_commits = True
642 else:
645 else:
643 c.inline_comments = display_inline_comments
646 c.inline_comments = display_inline_comments
644
647
645 use_ancestor = True
648 use_ancestor = True
646 if from_version_normalized != version_normalized:
649 if from_version_normalized != version_normalized:
647 use_ancestor = False
650 use_ancestor = False
648
651
649 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
650 if not force_recache and has_proper_diff_cache:
653 if not force_recache and has_proper_diff_cache:
651 c.diffset = cached_diff['diff']
654 c.diffset = cached_diff['diff']
652 else:
655 else:
653 try:
656 try:
654 c.diffset = self._get_diffset(
657 c.diffset = self._get_diffset(
655 c.source_repo.repo_name, commits_source_repo,
658 c.source_repo.repo_name, commits_source_repo,
656 c.ancestor_commit,
659 c.ancestor_commit,
657 source_ref_id, target_ref_id,
660 source_ref_id, target_ref_id,
658 target_commit, source_commit,
661 target_commit, source_commit,
659 diff_limit, file_limit, c.fulldiff,
662 diff_limit, file_limit, c.fulldiff,
660 hide_whitespace_changes, diff_context,
663 hide_whitespace_changes, diff_context,
661 use_ancestor=use_ancestor
664 use_ancestor=use_ancestor
662 )
665 )
663
666
664 # save cached diff
667 # save cached diff
665 if caching_enabled:
668 if caching_enabled:
666 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
667 except CommitDoesNotExistError:
670 except CommitDoesNotExistError:
668 log.exception('Failed to generate diffset')
671 log.exception('Failed to generate diffset')
669 c.missing_commits = True
672 c.missing_commits = True
670
673
671 if not c.missing_commits:
674 if not c.missing_commits:
672
675
673 c.limited_diff = c.diffset.limited_diff
676 c.limited_diff = c.diffset.limited_diff
674
677
675 # calculate removed files that are bound to comments
678 # calculate removed files that are bound to comments
676 comment_deleted_files = [
679 comment_deleted_files = [
677 fname for fname in display_inline_comments
680 fname for fname in display_inline_comments
678 if fname not in c.diffset.file_stats]
681 if fname not in c.diffset.file_stats]
679
682
680 c.deleted_files_comments = collections.defaultdict(dict)
683 c.deleted_files_comments = collections.defaultdict(dict)
681 for fname, per_line_comments in display_inline_comments.items():
684 for fname, per_line_comments in display_inline_comments.items():
682 if fname in comment_deleted_files:
685 if fname in comment_deleted_files:
683 c.deleted_files_comments[fname]['stats'] = 0
686 c.deleted_files_comments[fname]['stats'] = 0
684 c.deleted_files_comments[fname]['comments'] = list()
687 c.deleted_files_comments[fname]['comments'] = list()
685 for lno, comments in per_line_comments.items():
688 for lno, comments in per_line_comments.items():
686 c.deleted_files_comments[fname]['comments'].extend(comments)
689 c.deleted_files_comments[fname]['comments'].extend(comments)
687
690
688 # maybe calculate the range diff
691 # maybe calculate the range diff
689 if c.range_diff_on:
692 if c.range_diff_on:
690 # TODO(marcink): set whitespace/context
693 # TODO(marcink): set whitespace/context
691 context_lcl = 3
694 context_lcl = 3
692 ign_whitespace_lcl = False
695 ign_whitespace_lcl = False
693
696
694 for commit in c.commit_ranges:
697 for commit in c.commit_ranges:
695 commit2 = commit
698 commit2 = commit
696 commit1 = commit.first_parent
699 commit1 = commit.first_parent
697
700
698 range_diff_cache_file_path = diff_cache_exist(
701 range_diff_cache_file_path = diff_cache_exist(
699 cache_path, 'diff', commit.raw_id,
702 cache_path, 'diff', commit.raw_id,
700 ign_whitespace_lcl, context_lcl, c.fulldiff)
703 ign_whitespace_lcl, context_lcl, c.fulldiff)
701
704
702 cached_diff = None
705 cached_diff = None
703 if caching_enabled:
706 if caching_enabled:
704 cached_diff = load_cached_diff(range_diff_cache_file_path)
707 cached_diff = load_cached_diff(range_diff_cache_file_path)
705
708
706 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
707 if not force_recache and has_proper_diff_cache:
710 if not force_recache and has_proper_diff_cache:
708 diffset = cached_diff['diff']
711 diffset = cached_diff['diff']
709 else:
712 else:
710 diffset = self._get_range_diffset(
713 diffset = self._get_range_diffset(
711 commits_source_repo, source_repo,
714 commits_source_repo, source_repo,
712 commit1, commit2, diff_limit, file_limit,
715 commit1, commit2, diff_limit, file_limit,
713 c.fulldiff, ign_whitespace_lcl, context_lcl
716 c.fulldiff, ign_whitespace_lcl, context_lcl
714 )
717 )
715
718
716 # save cached diff
719 # save cached diff
717 if caching_enabled:
720 if caching_enabled:
718 cache_diff(range_diff_cache_file_path, diffset, None)
721 cache_diff(range_diff_cache_file_path, diffset, None)
719
722
720 c.changes[commit.raw_id] = diffset
723 c.changes[commit.raw_id] = diffset
721
724
722 # this is a hack to properly display links, when creating PR, the
725 # this is a hack to properly display links, when creating PR, the
723 # compare view and others uses different notation, and
726 # compare view and others uses different notation, and
724 # compare_commits.mako renders links based on the target_repo.
727 # compare_commits.mako renders links based on the target_repo.
725 # We need to swap that here to generate it properly on the html side
728 # We need to swap that here to generate it properly on the html side
726 c.target_repo = c.source_repo
729 c.target_repo = c.source_repo
727
730
728 c.commit_statuses = ChangesetStatus.STATUSES
731 c.commit_statuses = ChangesetStatus.STATUSES
729
732
730 c.show_version_changes = not pr_closed
733 c.show_version_changes = not pr_closed
731 if c.show_version_changes:
734 if c.show_version_changes:
732 cur_obj = pull_request_at_ver
735 cur_obj = pull_request_at_ver
733 prev_obj = prev_pull_request_at_ver
736 prev_obj = prev_pull_request_at_ver
734
737
735 old_commit_ids = prev_obj.revisions
738 old_commit_ids = prev_obj.revisions
736 new_commit_ids = cur_obj.revisions
739 new_commit_ids = cur_obj.revisions
737 commit_changes = PullRequestModel()._calculate_commit_id_changes(
740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
738 old_commit_ids, new_commit_ids)
741 old_commit_ids, new_commit_ids)
739 c.commit_changes_summary = commit_changes
742 c.commit_changes_summary = commit_changes
740
743
741 # calculate the diff for commits between versions
744 # calculate the diff for commits between versions
742 c.commit_changes = []
745 c.commit_changes = []
743
746
744 def mark(cs, fw):
747 def mark(cs, fw):
745 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
746
749
747 for c_type, raw_id in mark(commit_changes.added, 'a') \
750 for c_type, raw_id in mark(commit_changes.added, 'a') \
748 + mark(commit_changes.removed, 'r') \
751 + mark(commit_changes.removed, 'r') \
749 + mark(commit_changes.common, 'c'):
752 + mark(commit_changes.common, 'c'):
750
753
751 if raw_id in commit_cache:
754 if raw_id in commit_cache:
752 commit = commit_cache[raw_id]
755 commit = commit_cache[raw_id]
753 else:
756 else:
754 try:
757 try:
755 commit = commits_source_repo.get_commit(raw_id)
758 commit = commits_source_repo.get_commit(raw_id)
756 except CommitDoesNotExistError:
759 except CommitDoesNotExistError:
757 # in case we fail extracting still use "dummy" commit
760 # in case we fail extracting still use "dummy" commit
758 # for display in commit diff
761 # for display in commit diff
759 commit = h.AttributeDict(
762 commit = h.AttributeDict(
760 {'raw_id': raw_id,
763 {'raw_id': raw_id,
761 'message': 'EMPTY or MISSING COMMIT'})
764 'message': 'EMPTY or MISSING COMMIT'})
762 c.commit_changes.append([c_type, commit])
765 c.commit_changes.append([c_type, commit])
763
766
764 # current user review statuses for each version
767 # current user review statuses for each version
765 c.review_versions = {}
768 c.review_versions = {}
766 is_reviewer = PullRequestModel().is_user_reviewer(
769 is_reviewer = PullRequestModel().is_user_reviewer(
767 pull_request, self._rhodecode_user)
770 pull_request, self._rhodecode_user)
768 if is_reviewer:
771 if is_reviewer:
769 for co in general_comments:
772 for co in general_comments:
770 if co.author.user_id == self._rhodecode_user.user_id:
773 if co.author.user_id == self._rhodecode_user.user_id:
771 status = co.status_change
774 status = co.status_change
772 if status:
775 if status:
773 _ver_pr = status[0].comment.pull_request_version_id
776 _ver_pr = status[0].comment.pull_request_version_id
774 c.review_versions[_ver_pr] = status[0]
777 c.review_versions[_ver_pr] = status[0]
775
778
776 return self._get_template_context(c)
779 return self._get_template_context(c)
777
780
778 def get_commits(
781 def get_commits(
779 self, commits_source_repo, pull_request_at_ver, source_commit,
782 self, commits_source_repo, pull_request_at_ver, source_commit,
780 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
781 maybe_unreachable=False):
784 maybe_unreachable=False):
782
785
783 commit_cache = collections.OrderedDict()
786 commit_cache = collections.OrderedDict()
784 missing_requirements = False
787 missing_requirements = False
785
788
786 try:
789 try:
787 pre_load = ["author", "date", "message", "branch", "parents"]
790 pre_load = ["author", "date", "message", "branch", "parents"]
788
791
789 pull_request_commits = pull_request_at_ver.revisions
792 pull_request_commits = pull_request_at_ver.revisions
790 log.debug('Loading %s commits from %s',
793 log.debug('Loading %s commits from %s',
791 len(pull_request_commits), commits_source_repo)
794 len(pull_request_commits), commits_source_repo)
792
795
793 for rev in pull_request_commits:
796 for rev in pull_request_commits:
794 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
795 maybe_unreachable=maybe_unreachable)
798 maybe_unreachable=maybe_unreachable)
796 commit_cache[comm.raw_id] = comm
799 commit_cache[comm.raw_id] = comm
797
800
798 # Order here matters, we first need to get target, and then
801 # Order here matters, we first need to get target, and then
799 # the source
802 # the source
800 target_commit = commits_source_repo.get_commit(
803 target_commit = commits_source_repo.get_commit(
801 commit_id=safe_str(target_ref_id))
804 commit_id=safe_str(target_ref_id))
802
805
803 source_commit = commits_source_repo.get_commit(
806 source_commit = commits_source_repo.get_commit(
804 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
805 except CommitDoesNotExistError:
808 except CommitDoesNotExistError:
806 log.warning('Failed to get commit from `{}` repo'.format(
809 log.warning('Failed to get commit from `{}` repo'.format(
807 commits_source_repo), exc_info=True)
810 commits_source_repo), exc_info=True)
808 except RepositoryRequirementError:
811 except RepositoryRequirementError:
809 log.warning('Failed to get all required data from repo', exc_info=True)
812 log.warning('Failed to get all required data from repo', exc_info=True)
810 missing_requirements = True
813 missing_requirements = True
811
814
812 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
813
816
814 try:
817 try:
815 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
816 except Exception:
819 except Exception:
817 ancestor_commit = None
820 ancestor_commit = None
818
821
819 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
820
823
821 def assure_not_empty_repo(self):
824 def assure_not_empty_repo(self):
822 _ = self.request.translate
825 _ = self.request.translate
823
826
824 try:
827 try:
825 self.db_repo.scm_instance().get_commit()
828 self.db_repo.scm_instance().get_commit()
826 except EmptyRepositoryError:
829 except EmptyRepositoryError:
827 h.flash(h.literal(_('There are no commits yet')),
830 h.flash(h.literal(_('There are no commits yet')),
828 category='warning')
831 category='warning')
829 raise HTTPFound(
832 raise HTTPFound(
830 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
831
834
832 @LoginRequired()
835 @LoginRequired()
833 @NotAnonymous()
836 @NotAnonymous()
834 @HasRepoPermissionAnyDecorator(
837 @HasRepoPermissionAnyDecorator(
835 'repository.read', 'repository.write', 'repository.admin')
838 'repository.read', 'repository.write', 'repository.admin')
836 def pull_request_new(self):
839 def pull_request_new(self):
837 _ = self.request.translate
840 _ = self.request.translate
838 c = self.load_default_context()
841 c = self.load_default_context()
839
842
840 self.assure_not_empty_repo()
843 self.assure_not_empty_repo()
841 source_repo = self.db_repo
844 source_repo = self.db_repo
842
845
843 commit_id = self.request.GET.get('commit')
846 commit_id = self.request.GET.get('commit')
844 branch_ref = self.request.GET.get('branch')
847 branch_ref = self.request.GET.get('branch')
845 bookmark_ref = self.request.GET.get('bookmark')
848 bookmark_ref = self.request.GET.get('bookmark')
846
849
847 try:
850 try:
848 source_repo_data = PullRequestModel().generate_repo_data(
851 source_repo_data = PullRequestModel().generate_repo_data(
849 source_repo, commit_id=commit_id,
852 source_repo, commit_id=commit_id,
850 branch=branch_ref, bookmark=bookmark_ref,
853 branch=branch_ref, bookmark=bookmark_ref,
851 translator=self.request.translate)
854 translator=self.request.translate)
852 except CommitDoesNotExistError as e:
855 except CommitDoesNotExistError as e:
853 log.exception(e)
856 log.exception(e)
854 h.flash(_('Commit does not exist'), 'error')
857 h.flash(_('Commit does not exist'), 'error')
855 raise HTTPFound(
858 raise HTTPFound(
856 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
857
860
858 default_target_repo = source_repo
861 default_target_repo = source_repo
859
862
860 if source_repo.parent and c.has_origin_repo_read_perm:
863 if source_repo.parent and c.has_origin_repo_read_perm:
861 parent_vcs_obj = source_repo.parent.scm_instance()
864 parent_vcs_obj = source_repo.parent.scm_instance()
862 if parent_vcs_obj and not parent_vcs_obj.is_empty():
865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
863 # change default if we have a parent repo
866 # change default if we have a parent repo
864 default_target_repo = source_repo.parent
867 default_target_repo = source_repo.parent
865
868
866 target_repo_data = PullRequestModel().generate_repo_data(
869 target_repo_data = PullRequestModel().generate_repo_data(
867 default_target_repo, translator=self.request.translate)
870 default_target_repo, translator=self.request.translate)
868
871
869 selected_source_ref = source_repo_data['refs']['selected_ref']
872 selected_source_ref = source_repo_data['refs']['selected_ref']
870 title_source_ref = ''
873 title_source_ref = ''
871 if selected_source_ref:
874 if selected_source_ref:
872 title_source_ref = selected_source_ref.split(':', 2)[1]
875 title_source_ref = selected_source_ref.split(':', 2)[1]
873 c.default_title = PullRequestModel().generate_pullrequest_title(
876 c.default_title = PullRequestModel().generate_pullrequest_title(
874 source=source_repo.repo_name,
877 source=source_repo.repo_name,
875 source_ref=title_source_ref,
878 source_ref=title_source_ref,
876 target=default_target_repo.repo_name
879 target=default_target_repo.repo_name
877 )
880 )
878
881
879 c.default_repo_data = {
882 c.default_repo_data = {
880 'source_repo_name': source_repo.repo_name,
883 'source_repo_name': source_repo.repo_name,
881 'source_refs_json': json.dumps(source_repo_data),
884 'source_refs_json': json.dumps(source_repo_data),
882 'target_repo_name': default_target_repo.repo_name,
885 'target_repo_name': default_target_repo.repo_name,
883 'target_refs_json': json.dumps(target_repo_data),
886 'target_refs_json': json.dumps(target_repo_data),
884 }
887 }
885 c.default_source_ref = selected_source_ref
888 c.default_source_ref = selected_source_ref
886
889
887 return self._get_template_context(c)
890 return self._get_template_context(c)
888
891
889 @LoginRequired()
892 @LoginRequired()
890 @NotAnonymous()
893 @NotAnonymous()
891 @HasRepoPermissionAnyDecorator(
894 @HasRepoPermissionAnyDecorator(
892 'repository.read', 'repository.write', 'repository.admin')
895 'repository.read', 'repository.write', 'repository.admin')
893 def pull_request_repo_refs(self):
896 def pull_request_repo_refs(self):
894 self.load_default_context()
897 self.load_default_context()
895 target_repo_name = self.request.matchdict['target_repo_name']
898 target_repo_name = self.request.matchdict['target_repo_name']
896 repo = Repository.get_by_repo_name(target_repo_name)
899 repo = Repository.get_by_repo_name(target_repo_name)
897 if not repo:
900 if not repo:
898 raise HTTPNotFound()
901 raise HTTPNotFound()
899
902
900 target_perm = HasRepoPermissionAny(
903 target_perm = HasRepoPermissionAny(
901 'repository.read', 'repository.write', 'repository.admin')(
904 'repository.read', 'repository.write', 'repository.admin')(
902 target_repo_name)
905 target_repo_name)
903 if not target_perm:
906 if not target_perm:
904 raise HTTPNotFound()
907 raise HTTPNotFound()
905
908
906 return PullRequestModel().generate_repo_data(
909 return PullRequestModel().generate_repo_data(
907 repo, translator=self.request.translate)
910 repo, translator=self.request.translate)
908
911
909 @LoginRequired()
912 @LoginRequired()
910 @NotAnonymous()
913 @NotAnonymous()
911 @HasRepoPermissionAnyDecorator(
914 @HasRepoPermissionAnyDecorator(
912 'repository.read', 'repository.write', 'repository.admin')
915 'repository.read', 'repository.write', 'repository.admin')
913 def pullrequest_repo_targets(self):
916 def pullrequest_repo_targets(self):
914 _ = self.request.translate
917 _ = self.request.translate
915 filter_query = self.request.GET.get('query')
918 filter_query = self.request.GET.get('query')
916
919
917 # get the parents
920 # get the parents
918 parent_target_repos = []
921 parent_target_repos = []
919 if self.db_repo.parent:
922 if self.db_repo.parent:
920 parents_query = Repository.query() \
923 parents_query = Repository.query() \
921 .order_by(func.length(Repository.repo_name)) \
924 .order_by(func.length(Repository.repo_name)) \
922 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
925 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
923
926
924 if filter_query:
927 if filter_query:
925 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
928 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
926 parents_query = parents_query.filter(
929 parents_query = parents_query.filter(
927 Repository.repo_name.ilike(ilike_expression))
930 Repository.repo_name.ilike(ilike_expression))
928 parents = parents_query.limit(20).all()
931 parents = parents_query.limit(20).all()
929
932
930 for parent in parents:
933 for parent in parents:
931 parent_vcs_obj = parent.scm_instance()
934 parent_vcs_obj = parent.scm_instance()
932 if parent_vcs_obj and not parent_vcs_obj.is_empty():
935 if parent_vcs_obj and not parent_vcs_obj.is_empty():
933 parent_target_repos.append(parent)
936 parent_target_repos.append(parent)
934
937
935 # get other forks, and repo itself
938 # get other forks, and repo itself
936 query = Repository.query() \
939 query = Repository.query() \
937 .order_by(func.length(Repository.repo_name)) \
940 .order_by(func.length(Repository.repo_name)) \
938 .filter(
941 .filter(
939 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
942 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
940 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
943 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
941 ) \
944 ) \
942 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
945 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
943
946
944 if filter_query:
947 if filter_query:
945 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
948 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
946 query = query.filter(Repository.repo_name.ilike(ilike_expression))
949 query = query.filter(Repository.repo_name.ilike(ilike_expression))
947
950
948 limit = max(20 - len(parent_target_repos), 5) # not less then 5
951 limit = max(20 - len(parent_target_repos), 5) # not less then 5
949 target_repos = query.limit(limit).all()
952 target_repos = query.limit(limit).all()
950
953
951 all_target_repos = target_repos + parent_target_repos
954 all_target_repos = target_repos + parent_target_repos
952
955
953 repos = []
956 repos = []
954 # This checks permissions to the repositories
957 # This checks permissions to the repositories
955 for obj in ScmModel().get_repos(all_target_repos):
958 for obj in ScmModel().get_repos(all_target_repos):
956 repos.append({
959 repos.append({
957 'id': obj['name'],
960 'id': obj['name'],
958 'text': obj['name'],
961 'text': obj['name'],
959 'type': 'repo',
962 'type': 'repo',
960 'repo_id': obj['dbrepo']['repo_id'],
963 'repo_id': obj['dbrepo']['repo_id'],
961 'repo_type': obj['dbrepo']['repo_type'],
964 'repo_type': obj['dbrepo']['repo_type'],
962 'private': obj['dbrepo']['private'],
965 'private': obj['dbrepo']['private'],
963
966
964 })
967 })
965
968
966 data = {
969 data = {
967 'more': False,
970 'more': False,
968 'results': [{
971 'results': [{
969 'text': _('Repositories'),
972 'text': _('Repositories'),
970 'children': repos
973 'children': repos
971 }] if repos else []
974 }] if repos else []
972 }
975 }
973 return data
976 return data
974
977
975 @classmethod
978 @classmethod
976 def get_comment_ids(cls, post_data):
979 def get_comment_ids(cls, post_data):
977 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
980 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
978
981
979 @LoginRequired()
982 @LoginRequired()
980 @NotAnonymous()
983 @NotAnonymous()
981 @HasRepoPermissionAnyDecorator(
984 @HasRepoPermissionAnyDecorator(
982 'repository.read', 'repository.write', 'repository.admin')
985 'repository.read', 'repository.write', 'repository.admin')
983 def pullrequest_comments(self):
986 def pullrequest_comments(self):
984 self.load_default_context()
987 self.load_default_context()
985
988
986 pull_request = PullRequest.get_or_404(
989 pull_request = PullRequest.get_or_404(
987 self.request.matchdict['pull_request_id'])
990 self.request.matchdict['pull_request_id'])
988 pull_request_id = pull_request.pull_request_id
991 pull_request_id = pull_request.pull_request_id
989 version = self.request.GET.get('version')
992 version = self.request.GET.get('version')
990
993
991 _render = self.request.get_partial_renderer(
994 _render = self.request.get_partial_renderer(
992 'rhodecode:templates/base/sidebar.mako')
995 'rhodecode:templates/base/sidebar.mako')
993 c = _render.get_call_context()
996 c = _render.get_call_context()
994
997
995 (pull_request_latest,
998 (pull_request_latest,
996 pull_request_at_ver,
999 pull_request_at_ver,
997 pull_request_display_obj,
1000 pull_request_display_obj,
998 at_version) = PullRequestModel().get_pr_version(
1001 at_version) = PullRequestModel().get_pr_version(
999 pull_request_id, version=version)
1002 pull_request_id, version=version)
1000 versions = pull_request_display_obj.versions()
1003 versions = pull_request_display_obj.versions()
1001 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1004 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1002 c.versions = versions + [latest_ver]
1005 c.versions = versions + [latest_ver]
1003
1006
1004 c.at_version = at_version
1007 c.at_version = at_version
1005 c.at_version_num = (at_version
1008 c.at_version_num = (at_version
1006 if at_version and at_version != PullRequest.LATEST_VER
1009 if at_version and at_version != PullRequest.LATEST_VER
1007 else None)
1010 else None)
1008
1011
1009 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1012 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1010 all_comments = c.inline_comments_flat + c.comments
1013 all_comments = c.inline_comments_flat + c.comments
1011
1014
1012 existing_ids = self.get_comment_ids(self.request.POST)
1015 existing_ids = self.get_comment_ids(self.request.POST)
1013 return _render('comments_table', all_comments, len(all_comments),
1016 return _render('comments_table', all_comments, len(all_comments),
1014 existing_ids=existing_ids)
1017 existing_ids=existing_ids)
1015
1018
1016 @LoginRequired()
1019 @LoginRequired()
1017 @NotAnonymous()
1020 @NotAnonymous()
1018 @HasRepoPermissionAnyDecorator(
1021 @HasRepoPermissionAnyDecorator(
1019 'repository.read', 'repository.write', 'repository.admin')
1022 'repository.read', 'repository.write', 'repository.admin')
1020 def pullrequest_todos(self):
1023 def pullrequest_todos(self):
1021 self.load_default_context()
1024 self.load_default_context()
1022
1025
1023 pull_request = PullRequest.get_or_404(
1026 pull_request = PullRequest.get_or_404(
1024 self.request.matchdict['pull_request_id'])
1027 self.request.matchdict['pull_request_id'])
1025 pull_request_id = pull_request.pull_request_id
1028 pull_request_id = pull_request.pull_request_id
1026 version = self.request.GET.get('version')
1029 version = self.request.GET.get('version')
1027
1030
1028 _render = self.request.get_partial_renderer(
1031 _render = self.request.get_partial_renderer(
1029 'rhodecode:templates/base/sidebar.mako')
1032 'rhodecode:templates/base/sidebar.mako')
1030 c = _render.get_call_context()
1033 c = _render.get_call_context()
1031 (pull_request_latest,
1034 (pull_request_latest,
1032 pull_request_at_ver,
1035 pull_request_at_ver,
1033 pull_request_display_obj,
1036 pull_request_display_obj,
1034 at_version) = PullRequestModel().get_pr_version(
1037 at_version) = PullRequestModel().get_pr_version(
1035 pull_request_id, version=version)
1038 pull_request_id, version=version)
1036 versions = pull_request_display_obj.versions()
1039 versions = pull_request_display_obj.versions()
1037 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1040 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1038 c.versions = versions + [latest_ver]
1041 c.versions = versions + [latest_ver]
1039
1042
1040 c.at_version = at_version
1043 c.at_version = at_version
1041 c.at_version_num = (at_version
1044 c.at_version_num = (at_version
1042 if at_version and at_version != PullRequest.LATEST_VER
1045 if at_version and at_version != PullRequest.LATEST_VER
1043 else None)
1046 else None)
1044
1047
1045 c.unresolved_comments = CommentsModel() \
1048 c.unresolved_comments = CommentsModel() \
1046 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1049 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1047 c.resolved_comments = CommentsModel() \
1050 c.resolved_comments = CommentsModel() \
1048 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1051 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1049
1052
1050 all_comments = c.unresolved_comments + c.resolved_comments
1053 all_comments = c.unresolved_comments + c.resolved_comments
1051 existing_ids = self.get_comment_ids(self.request.POST)
1054 existing_ids = self.get_comment_ids(self.request.POST)
1052 return _render('comments_table', all_comments, len(c.unresolved_comments),
1055 return _render('comments_table', all_comments, len(c.unresolved_comments),
1053 todo_comments=True, existing_ids=existing_ids)
1056 todo_comments=True, existing_ids=existing_ids)
1054
1057
1055 @LoginRequired()
1058 @LoginRequired()
1056 @NotAnonymous()
1059 @NotAnonymous()
1057 @HasRepoPermissionAnyDecorator(
1060 @HasRepoPermissionAnyDecorator(
1058 'repository.read', 'repository.write', 'repository.admin')
1061 'repository.read', 'repository.write', 'repository.admin')
1059 def pullrequest_drafts(self):
1062 def pullrequest_drafts(self):
1060 self.load_default_context()
1063 self.load_default_context()
1061
1064
1062 pull_request = PullRequest.get_or_404(
1065 pull_request = PullRequest.get_or_404(
1063 self.request.matchdict['pull_request_id'])
1066 self.request.matchdict['pull_request_id'])
1064 pull_request_id = pull_request.pull_request_id
1067 pull_request_id = pull_request.pull_request_id
1065 version = self.request.GET.get('version')
1068 version = self.request.GET.get('version')
1066
1069
1067 _render = self.request.get_partial_renderer(
1070 _render = self.request.get_partial_renderer(
1068 'rhodecode:templates/base/sidebar.mako')
1071 'rhodecode:templates/base/sidebar.mako')
1069 c = _render.get_call_context()
1072 c = _render.get_call_context()
1070
1073
1071 (pull_request_latest,
1074 (pull_request_latest,
1072 pull_request_at_ver,
1075 pull_request_at_ver,
1073 pull_request_display_obj,
1076 pull_request_display_obj,
1074 at_version) = PullRequestModel().get_pr_version(
1077 at_version) = PullRequestModel().get_pr_version(
1075 pull_request_id, version=version)
1078 pull_request_id, version=version)
1076 versions = pull_request_display_obj.versions()
1079 versions = pull_request_display_obj.versions()
1077 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1080 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1078 c.versions = versions + [latest_ver]
1081 c.versions = versions + [latest_ver]
1079
1082
1080 c.at_version = at_version
1083 c.at_version = at_version
1081 c.at_version_num = (at_version
1084 c.at_version_num = (at_version
1082 if at_version and at_version != PullRequest.LATEST_VER
1085 if at_version and at_version != PullRequest.LATEST_VER
1083 else None)
1086 else None)
1084
1087
1085 c.draft_comments = CommentsModel() \
1088 c.draft_comments = CommentsModel() \
1086 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1089 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1087
1090
1088 all_comments = c.draft_comments
1091 all_comments = c.draft_comments
1089
1092
1090 existing_ids = self.get_comment_ids(self.request.POST)
1093 existing_ids = self.get_comment_ids(self.request.POST)
1091 return _render('comments_table', all_comments, len(all_comments),
1094 return _render('comments_table', all_comments, len(all_comments),
1092 existing_ids=existing_ids, draft_comments=True)
1095 existing_ids=existing_ids, draft_comments=True)
1093
1096
1094 @LoginRequired()
1097 @LoginRequired()
1095 @NotAnonymous()
1098 @NotAnonymous()
1096 @HasRepoPermissionAnyDecorator(
1099 @HasRepoPermissionAnyDecorator(
1097 'repository.read', 'repository.write', 'repository.admin')
1100 'repository.read', 'repository.write', 'repository.admin')
1098 @CSRFRequired()
1101 @CSRFRequired()
1099 def pull_request_create(self):
1102 def pull_request_create(self):
1100 _ = self.request.translate
1103 _ = self.request.translate
1101 self.assure_not_empty_repo()
1104 self.assure_not_empty_repo()
1102 self.load_default_context()
1105 self.load_default_context()
1103
1106
1104 controls = peppercorn.parse(self.request.POST.items())
1107 controls = peppercorn.parse(self.request.POST.items())
1105
1108
1106 try:
1109 try:
1107 form = PullRequestForm(
1110 form = PullRequestForm(
1108 self.request.translate, self.db_repo.repo_id)()
1111 self.request.translate, self.db_repo.repo_id)()
1109 _form = form.to_python(controls)
1112 _form = form.to_python(controls)
1110 except formencode.Invalid as errors:
1113 except formencode.Invalid as errors:
1111 if errors.error_dict.get('revisions'):
1114 if errors.error_dict.get('revisions'):
1112 msg = 'Revisions: %s' % errors.error_dict['revisions']
1115 msg = 'Revisions: %s' % errors.error_dict['revisions']
1113 elif errors.error_dict.get('pullrequest_title'):
1116 elif errors.error_dict.get('pullrequest_title'):
1114 msg = errors.error_dict.get('pullrequest_title')
1117 msg = errors.error_dict.get('pullrequest_title')
1115 else:
1118 else:
1116 msg = _('Error creating pull request: {}').format(errors)
1119 msg = _('Error creating pull request: {}').format(errors)
1117 log.exception(msg)
1120 log.exception(msg)
1118 h.flash(msg, 'error')
1121 h.flash(msg, 'error')
1119
1122
1120 # would rather just go back to form ...
1123 # would rather just go back to form ...
1121 raise HTTPFound(
1124 raise HTTPFound(
1122 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1123
1126
1124 source_repo = _form['source_repo']
1127 source_repo = _form['source_repo']
1125 source_ref = _form['source_ref']
1128 source_ref = _form['source_ref']
1126 target_repo = _form['target_repo']
1129 target_repo = _form['target_repo']
1127 target_ref = _form['target_ref']
1130 target_ref = _form['target_ref']
1128 commit_ids = _form['revisions'][::-1]
1131 commit_ids = _form['revisions'][::-1]
1129 common_ancestor_id = _form['common_ancestor']
1132 common_ancestor_id = _form['common_ancestor']
1130
1133
1131 # find the ancestor for this pr
1134 # find the ancestor for this pr
1132 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1135 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1133 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1136 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1134
1137
1135 if not (source_db_repo or target_db_repo):
1138 if not (source_db_repo or target_db_repo):
1136 h.flash(_('source_repo or target repo not found'), category='error')
1139 h.flash(_('source_repo or target repo not found'), category='error')
1137 raise HTTPFound(
1140 raise HTTPFound(
1138 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1139
1142
1140 # re-check permissions again here
1143 # re-check permissions again here
1141 # source_repo we must have read permissions
1144 # source_repo we must have read permissions
1142
1145
1143 source_perm = HasRepoPermissionAny(
1146 source_perm = HasRepoPermissionAny(
1144 'repository.read', 'repository.write', 'repository.admin')(
1147 'repository.read', 'repository.write', 'repository.admin')(
1145 source_db_repo.repo_name)
1148 source_db_repo.repo_name)
1146 if not source_perm:
1149 if not source_perm:
1147 msg = _('Not Enough permissions to source repo `{}`.'.format(
1150 msg = _('Not Enough permissions to source repo `{}`.'.format(
1148 source_db_repo.repo_name))
1151 source_db_repo.repo_name))
1149 h.flash(msg, category='error')
1152 h.flash(msg, category='error')
1150 # copy the args back to redirect
1153 # copy the args back to redirect
1151 org_query = self.request.GET.mixed()
1154 org_query = self.request.GET.mixed()
1152 raise HTTPFound(
1155 raise HTTPFound(
1153 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1156 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1154 _query=org_query))
1157 _query=org_query))
1155
1158
1156 # target repo we must have read permissions, and also later on
1159 # target repo we must have read permissions, and also later on
1157 # we want to check branch permissions here
1160 # we want to check branch permissions here
1158 target_perm = HasRepoPermissionAny(
1161 target_perm = HasRepoPermissionAny(
1159 'repository.read', 'repository.write', 'repository.admin')(
1162 'repository.read', 'repository.write', 'repository.admin')(
1160 target_db_repo.repo_name)
1163 target_db_repo.repo_name)
1161 if not target_perm:
1164 if not target_perm:
1162 msg = _('Not Enough permissions to target repo `{}`.'.format(
1165 msg = _('Not Enough permissions to target repo `{}`.'.format(
1163 target_db_repo.repo_name))
1166 target_db_repo.repo_name))
1164 h.flash(msg, category='error')
1167 h.flash(msg, category='error')
1165 # copy the args back to redirect
1168 # copy the args back to redirect
1166 org_query = self.request.GET.mixed()
1169 org_query = self.request.GET.mixed()
1167 raise HTTPFound(
1170 raise HTTPFound(
1168 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1171 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1169 _query=org_query))
1172 _query=org_query))
1170
1173
1171 source_scm = source_db_repo.scm_instance()
1174 source_scm = source_db_repo.scm_instance()
1172 target_scm = target_db_repo.scm_instance()
1175 target_scm = target_db_repo.scm_instance()
1173
1176
1174 source_ref_obj = unicode_to_reference(source_ref)
1177 source_ref_obj = unicode_to_reference(source_ref)
1175 target_ref_obj = unicode_to_reference(target_ref)
1178 target_ref_obj = unicode_to_reference(target_ref)
1176
1179
1177 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1180 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1178 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1181 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1179
1182
1180 ancestor = source_scm.get_common_ancestor(
1183 ancestor = source_scm.get_common_ancestor(
1181 source_commit.raw_id, target_commit.raw_id, target_scm)
1184 source_commit.raw_id, target_commit.raw_id, target_scm)
1182
1185
1183 # recalculate target ref based on ancestor
1186 # recalculate target ref based on ancestor
1184 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1187 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1185
1188
1186 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1189 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1187 PullRequestModel().get_reviewer_functions()
1190 PullRequestModel().get_reviewer_functions()
1188
1191
1189 # recalculate reviewers logic, to make sure we can validate this
1192 # recalculate reviewers logic, to make sure we can validate this
1190 reviewer_rules = get_default_reviewers_data(
1193 reviewer_rules = get_default_reviewers_data(
1191 self._rhodecode_db_user,
1194 self._rhodecode_db_user,
1192 source_db_repo,
1195 source_db_repo,
1193 source_ref_obj,
1196 source_ref_obj,
1194 target_db_repo,
1197 target_db_repo,
1195 target_ref_obj,
1198 target_ref_obj,
1196 include_diff_info=False)
1199 include_diff_info=False)
1197
1200
1198 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1201 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1199 observers = validate_observers(_form['observer_members'], reviewer_rules)
1202 observers = validate_observers(_form['observer_members'], reviewer_rules)
1200
1203
1201 pullrequest_title = _form['pullrequest_title']
1204 pullrequest_title = _form['pullrequest_title']
1202 title_source_ref = source_ref_obj.name
1205 title_source_ref = source_ref_obj.name
1203 if not pullrequest_title:
1206 if not pullrequest_title:
1204 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1207 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1205 source=source_repo,
1208 source=source_repo,
1206 source_ref=title_source_ref,
1209 source_ref=title_source_ref,
1207 target=target_repo
1210 target=target_repo
1208 )
1211 )
1209
1212
1210 description = _form['pullrequest_desc']
1213 description = _form['pullrequest_desc']
1211 description_renderer = _form['description_renderer']
1214 description_renderer = _form['description_renderer']
1212
1215
1213 try:
1216 try:
1214 pull_request = PullRequestModel().create(
1217 pull_request = PullRequestModel().create(
1215 created_by=self._rhodecode_user.user_id,
1218 created_by=self._rhodecode_user.user_id,
1216 source_repo=source_repo,
1219 source_repo=source_repo,
1217 source_ref=source_ref,
1220 source_ref=source_ref,
1218 target_repo=target_repo,
1221 target_repo=target_repo,
1219 target_ref=target_ref,
1222 target_ref=target_ref,
1220 revisions=commit_ids,
1223 revisions=commit_ids,
1221 common_ancestor_id=common_ancestor_id,
1224 common_ancestor_id=common_ancestor_id,
1222 reviewers=reviewers,
1225 reviewers=reviewers,
1223 observers=observers,
1226 observers=observers,
1224 title=pullrequest_title,
1227 title=pullrequest_title,
1225 description=description,
1228 description=description,
1226 description_renderer=description_renderer,
1229 description_renderer=description_renderer,
1227 reviewer_data=reviewer_rules,
1230 reviewer_data=reviewer_rules,
1228 auth_user=self._rhodecode_user
1231 auth_user=self._rhodecode_user
1229 )
1232 )
1230 Session().commit()
1233 Session().commit()
1231
1234
1232 h.flash(_('Successfully opened new pull request'),
1235 h.flash(_('Successfully opened new pull request'),
1233 category='success')
1236 category='success')
1234 except Exception:
1237 except Exception:
1235 msg = _('Error occurred during creation of this pull request.')
1238 msg = _('Error occurred during creation of this pull request.')
1236 log.exception(msg)
1239 log.exception(msg)
1237 h.flash(msg, category='error')
1240 h.flash(msg, category='error')
1238
1241
1239 # copy the args back to redirect
1242 # copy the args back to redirect
1240 org_query = self.request.GET.mixed()
1243 org_query = self.request.GET.mixed()
1241 raise HTTPFound(
1244 raise HTTPFound(
1242 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1245 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1243 _query=org_query))
1246 _query=org_query))
1244
1247
1245 raise HTTPFound(
1248 raise HTTPFound(
1246 h.route_path('pullrequest_show', repo_name=target_repo,
1249 h.route_path('pullrequest_show', repo_name=target_repo,
1247 pull_request_id=pull_request.pull_request_id))
1250 pull_request_id=pull_request.pull_request_id))
1248
1251
1249 @LoginRequired()
1252 @LoginRequired()
1250 @NotAnonymous()
1253 @NotAnonymous()
1251 @HasRepoPermissionAnyDecorator(
1254 @HasRepoPermissionAnyDecorator(
1252 'repository.read', 'repository.write', 'repository.admin')
1255 'repository.read', 'repository.write', 'repository.admin')
1253 @CSRFRequired()
1256 @CSRFRequired()
1254 def pull_request_update(self):
1257 def pull_request_update(self):
1255 pull_request = PullRequest.get_or_404(
1258 pull_request = PullRequest.get_or_404(
1256 self.request.matchdict['pull_request_id'])
1259 self.request.matchdict['pull_request_id'])
1257 _ = self.request.translate
1260 _ = self.request.translate
1258
1261
1259 c = self.load_default_context()
1262 c = self.load_default_context()
1260 redirect_url = None
1263 redirect_url = None
1261
1264
1262 if pull_request.is_closed():
1265 if pull_request.is_closed():
1263 log.debug('update: forbidden because pull request is closed')
1266 log.debug('update: forbidden because pull request is closed')
1264 msg = _(u'Cannot update closed pull requests.')
1267 msg = _(u'Cannot update closed pull requests.')
1265 h.flash(msg, category='error')
1268 h.flash(msg, category='error')
1266 return {'response': True,
1269 return {'response': True,
1267 'redirect_url': redirect_url}
1270 'redirect_url': redirect_url}
1268
1271
1269 is_state_changing = pull_request.is_state_changing()
1272 is_state_changing = pull_request.is_state_changing()
1270 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1273 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1271
1274
1272 # only owner or admin can update it
1275 # only owner or admin can update it
1273 allowed_to_update = PullRequestModel().check_user_update(
1276 allowed_to_update = PullRequestModel().check_user_update(
1274 pull_request, self._rhodecode_user)
1277 pull_request, self._rhodecode_user)
1275
1278
1276 if allowed_to_update:
1279 if allowed_to_update:
1277 controls = peppercorn.parse(self.request.POST.items())
1280 controls = peppercorn.parse(self.request.POST.items())
1278 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1281 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1279
1282
1280 if 'review_members' in controls:
1283 if 'review_members' in controls:
1281 self._update_reviewers(
1284 self._update_reviewers(
1282 c,
1285 c,
1283 pull_request, controls['review_members'],
1286 pull_request, controls['review_members'],
1284 pull_request.reviewer_data,
1287 pull_request.reviewer_data,
1285 PullRequestReviewers.ROLE_REVIEWER)
1288 PullRequestReviewers.ROLE_REVIEWER)
1286 elif 'observer_members' in controls:
1289 elif 'observer_members' in controls:
1287 self._update_reviewers(
1290 self._update_reviewers(
1288 c,
1291 c,
1289 pull_request, controls['observer_members'],
1292 pull_request, controls['observer_members'],
1290 pull_request.reviewer_data,
1293 pull_request.reviewer_data,
1291 PullRequestReviewers.ROLE_OBSERVER)
1294 PullRequestReviewers.ROLE_OBSERVER)
1292 elif str2bool(self.request.POST.get('update_commits', 'false')):
1295 elif str2bool(self.request.POST.get('update_commits', 'false')):
1293 if is_state_changing:
1296 if is_state_changing:
1294 log.debug('commits update: forbidden because pull request is in state %s',
1297 log.debug('commits update: forbidden because pull request is in state %s',
1295 pull_request.pull_request_state)
1298 pull_request.pull_request_state)
1296 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1299 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1297 u'Current state is: `{}`').format(
1300 u'Current state is: `{}`').format(
1298 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1301 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1299 h.flash(msg, category='error')
1302 h.flash(msg, category='error')
1300 return {'response': True,
1303 return {'response': True,
1301 'redirect_url': redirect_url}
1304 'redirect_url': redirect_url}
1302
1305
1303 self._update_commits(c, pull_request)
1306 self._update_commits(c, pull_request)
1304 if force_refresh:
1307 if force_refresh:
1305 redirect_url = h.route_path(
1308 redirect_url = h.route_path(
1306 'pullrequest_show', repo_name=self.db_repo_name,
1309 'pullrequest_show', repo_name=self.db_repo_name,
1307 pull_request_id=pull_request.pull_request_id,
1310 pull_request_id=pull_request.pull_request_id,
1308 _query={"force_refresh": 1})
1311 _query={"force_refresh": 1})
1309 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1312 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1310 self._edit_pull_request(pull_request)
1313 self._edit_pull_request(pull_request)
1311 else:
1314 else:
1312 log.error('Unhandled update data.')
1315 log.error('Unhandled update data.')
1313 raise HTTPBadRequest()
1316 raise HTTPBadRequest()
1314
1317
1315 return {'response': True,
1318 return {'response': True,
1316 'redirect_url': redirect_url}
1319 'redirect_url': redirect_url}
1317 raise HTTPForbidden()
1320 raise HTTPForbidden()
1318
1321
1319 def _edit_pull_request(self, pull_request):
1322 def _edit_pull_request(self, pull_request):
1320 """
1323 """
1321 Edit title and description
1324 Edit title and description
1322 """
1325 """
1323 _ = self.request.translate
1326 _ = self.request.translate
1324
1327
1325 try:
1328 try:
1326 PullRequestModel().edit(
1329 PullRequestModel().edit(
1327 pull_request,
1330 pull_request,
1328 self.request.POST.get('title'),
1331 self.request.POST.get('title'),
1329 self.request.POST.get('description'),
1332 self.request.POST.get('description'),
1330 self.request.POST.get('description_renderer'),
1333 self.request.POST.get('description_renderer'),
1331 self._rhodecode_user)
1334 self._rhodecode_user)
1332 except ValueError:
1335 except ValueError:
1333 msg = _(u'Cannot update closed pull requests.')
1336 msg = _(u'Cannot update closed pull requests.')
1334 h.flash(msg, category='error')
1337 h.flash(msg, category='error')
1335 return
1338 return
1336 else:
1339 else:
1337 Session().commit()
1340 Session().commit()
1338
1341
1339 msg = _(u'Pull request title & description updated.')
1342 msg = _(u'Pull request title & description updated.')
1340 h.flash(msg, category='success')
1343 h.flash(msg, category='success')
1341 return
1344 return
1342
1345
1343 def _update_commits(self, c, pull_request):
1346 def _update_commits(self, c, pull_request):
1344 _ = self.request.translate
1347 _ = self.request.translate
1345
1348
1346 with pull_request.set_state(PullRequest.STATE_UPDATING):
1349 with pull_request.set_state(PullRequest.STATE_UPDATING):
1347 resp = PullRequestModel().update_commits(
1350 resp = PullRequestModel().update_commits(
1348 pull_request, self._rhodecode_db_user)
1351 pull_request, self._rhodecode_db_user)
1349
1352
1350 if resp.executed:
1353 if resp.executed:
1351
1354
1352 if resp.target_changed and resp.source_changed:
1355 if resp.target_changed and resp.source_changed:
1353 changed = 'target and source repositories'
1356 changed = 'target and source repositories'
1354 elif resp.target_changed and not resp.source_changed:
1357 elif resp.target_changed and not resp.source_changed:
1355 changed = 'target repository'
1358 changed = 'target repository'
1356 elif not resp.target_changed and resp.source_changed:
1359 elif not resp.target_changed and resp.source_changed:
1357 changed = 'source repository'
1360 changed = 'source repository'
1358 else:
1361 else:
1359 changed = 'nothing'
1362 changed = 'nothing'
1360
1363
1361 msg = _(u'Pull request updated to "{source_commit_id}" with '
1364 msg = _(u'Pull request updated to "{source_commit_id}" with '
1362 u'{count_added} added, {count_removed} removed commits. '
1365 u'{count_added} added, {count_removed} removed commits. '
1363 u'Source of changes: {change_source}.')
1366 u'Source of changes: {change_source}.')
1364 msg = msg.format(
1367 msg = msg.format(
1365 source_commit_id=pull_request.source_ref_parts.commit_id,
1368 source_commit_id=pull_request.source_ref_parts.commit_id,
1366 count_added=len(resp.changes.added),
1369 count_added=len(resp.changes.added),
1367 count_removed=len(resp.changes.removed),
1370 count_removed=len(resp.changes.removed),
1368 change_source=changed)
1371 change_source=changed)
1369 h.flash(msg, category='success')
1372 h.flash(msg, category='success')
1370 channelstream.pr_update_channelstream_push(
1373 channelstream.pr_update_channelstream_push(
1371 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1372 else:
1375 else:
1373 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1376 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1374 warning_reasons = [
1377 warning_reasons = [
1375 UpdateFailureReason.NO_CHANGE,
1378 UpdateFailureReason.NO_CHANGE,
1376 UpdateFailureReason.WRONG_REF_TYPE,
1379 UpdateFailureReason.WRONG_REF_TYPE,
1377 ]
1380 ]
1378 category = 'warning' if resp.reason in warning_reasons else 'error'
1381 category = 'warning' if resp.reason in warning_reasons else 'error'
1379 h.flash(msg, category=category)
1382 h.flash(msg, category=category)
1380
1383
1381 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1384 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1382 _ = self.request.translate
1385 _ = self.request.translate
1383
1386
1384 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1387 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1385 PullRequestModel().get_reviewer_functions()
1388 PullRequestModel().get_reviewer_functions()
1386
1389
1387 if role == PullRequestReviewers.ROLE_REVIEWER:
1390 if role == PullRequestReviewers.ROLE_REVIEWER:
1388 try:
1391 try:
1389 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1392 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1390 except ValueError as e:
1393 except ValueError as e:
1391 log.error('Reviewers Validation: {}'.format(e))
1394 log.error('Reviewers Validation: {}'.format(e))
1392 h.flash(e, category='error')
1395 h.flash(e, category='error')
1393 return
1396 return
1394
1397
1395 old_calculated_status = pull_request.calculated_review_status()
1398 old_calculated_status = pull_request.calculated_review_status()
1396 PullRequestModel().update_reviewers(
1399 PullRequestModel().update_reviewers(
1397 pull_request, reviewers, self._rhodecode_db_user)
1400 pull_request, reviewers, self._rhodecode_db_user)
1398
1401
1399 Session().commit()
1402 Session().commit()
1400
1403
1401 msg = _('Pull request reviewers updated.')
1404 msg = _('Pull request reviewers updated.')
1402 h.flash(msg, category='success')
1405 h.flash(msg, category='success')
1403 channelstream.pr_update_channelstream_push(
1406 channelstream.pr_update_channelstream_push(
1404 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1405
1408
1406 # trigger status changed if change in reviewers changes the status
1409 # trigger status changed if change in reviewers changes the status
1407 calculated_status = pull_request.calculated_review_status()
1410 calculated_status = pull_request.calculated_review_status()
1408 if old_calculated_status != calculated_status:
1411 if old_calculated_status != calculated_status:
1409 PullRequestModel().trigger_pull_request_hook(
1412 PullRequestModel().trigger_pull_request_hook(
1410 pull_request, self._rhodecode_user, 'review_status_change',
1413 pull_request, self._rhodecode_user, 'review_status_change',
1411 data={'status': calculated_status})
1414 data={'status': calculated_status})
1412
1415
1413 elif role == PullRequestReviewers.ROLE_OBSERVER:
1416 elif role == PullRequestReviewers.ROLE_OBSERVER:
1414 try:
1417 try:
1415 observers = validate_observers(review_members, reviewer_rules)
1418 observers = validate_observers(review_members, reviewer_rules)
1416 except ValueError as e:
1419 except ValueError as e:
1417 log.error('Observers Validation: {}'.format(e))
1420 log.error('Observers Validation: {}'.format(e))
1418 h.flash(e, category='error')
1421 h.flash(e, category='error')
1419 return
1422 return
1420
1423
1421 PullRequestModel().update_observers(
1424 PullRequestModel().update_observers(
1422 pull_request, observers, self._rhodecode_db_user)
1425 pull_request, observers, self._rhodecode_db_user)
1423
1426
1424 Session().commit()
1427 Session().commit()
1425 msg = _('Pull request observers updated.')
1428 msg = _('Pull request observers updated.')
1426 h.flash(msg, category='success')
1429 h.flash(msg, category='success')
1427 channelstream.pr_update_channelstream_push(
1430 channelstream.pr_update_channelstream_push(
1428 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1431 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1429
1432
1430 @LoginRequired()
1433 @LoginRequired()
1431 @NotAnonymous()
1434 @NotAnonymous()
1432 @HasRepoPermissionAnyDecorator(
1435 @HasRepoPermissionAnyDecorator(
1433 'repository.read', 'repository.write', 'repository.admin')
1436 'repository.read', 'repository.write', 'repository.admin')
1434 @CSRFRequired()
1437 @CSRFRequired()
1435 def pull_request_merge(self):
1438 def pull_request_merge(self):
1436 """
1439 """
1437 Merge will perform a server-side merge of the specified
1440 Merge will perform a server-side merge of the specified
1438 pull request, if the pull request is approved and mergeable.
1441 pull request, if the pull request is approved and mergeable.
1439 After successful merging, the pull request is automatically
1442 After successful merging, the pull request is automatically
1440 closed, with a relevant comment.
1443 closed, with a relevant comment.
1441 """
1444 """
1442 pull_request = PullRequest.get_or_404(
1445 pull_request = PullRequest.get_or_404(
1443 self.request.matchdict['pull_request_id'])
1446 self.request.matchdict['pull_request_id'])
1444 _ = self.request.translate
1447 _ = self.request.translate
1445
1448
1446 if pull_request.is_state_changing():
1449 if pull_request.is_state_changing():
1447 log.debug('show: forbidden because pull request is in state %s',
1450 log.debug('show: forbidden because pull request is in state %s',
1448 pull_request.pull_request_state)
1451 pull_request.pull_request_state)
1449 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1452 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1450 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1453 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1451 pull_request.pull_request_state)
1454 pull_request.pull_request_state)
1452 h.flash(msg, category='error')
1455 h.flash(msg, category='error')
1453 raise HTTPFound(
1456 raise HTTPFound(
1454 h.route_path('pullrequest_show',
1457 h.route_path('pullrequest_show',
1455 repo_name=pull_request.target_repo.repo_name,
1458 repo_name=pull_request.target_repo.repo_name,
1456 pull_request_id=pull_request.pull_request_id))
1459 pull_request_id=pull_request.pull_request_id))
1457
1460
1458 self.load_default_context()
1461 self.load_default_context()
1459
1462
1460 with pull_request.set_state(PullRequest.STATE_UPDATING):
1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1461 check = MergeCheck.validate(
1464 check = MergeCheck.validate(
1462 pull_request, auth_user=self._rhodecode_user,
1465 pull_request, auth_user=self._rhodecode_user,
1463 translator=self.request.translate)
1466 translator=self.request.translate)
1464 merge_possible = not check.failed
1467 merge_possible = not check.failed
1465
1468
1466 for err_type, error_msg in check.errors:
1469 for err_type, error_msg in check.errors:
1467 h.flash(error_msg, category=err_type)
1470 h.flash(error_msg, category=err_type)
1468
1471
1469 if merge_possible:
1472 if merge_possible:
1470 log.debug("Pre-conditions checked, trying to merge.")
1473 log.debug("Pre-conditions checked, trying to merge.")
1471 extras = vcs_operation_context(
1474 extras = vcs_operation_context(
1472 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1475 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1473 username=self._rhodecode_db_user.username, action='push',
1476 username=self._rhodecode_db_user.username, action='push',
1474 scm=pull_request.target_repo.repo_type)
1477 scm=pull_request.target_repo.repo_type)
1475 with pull_request.set_state(PullRequest.STATE_UPDATING):
1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1476 self._merge_pull_request(
1479 self._merge_pull_request(
1477 pull_request, self._rhodecode_db_user, extras)
1480 pull_request, self._rhodecode_db_user, extras)
1478 else:
1481 else:
1479 log.debug("Pre-conditions failed, NOT merging.")
1482 log.debug("Pre-conditions failed, NOT merging.")
1480
1483
1481 raise HTTPFound(
1484 raise HTTPFound(
1482 h.route_path('pullrequest_show',
1485 h.route_path('pullrequest_show',
1483 repo_name=pull_request.target_repo.repo_name,
1486 repo_name=pull_request.target_repo.repo_name,
1484 pull_request_id=pull_request.pull_request_id))
1487 pull_request_id=pull_request.pull_request_id))
1485
1488
1486 def _merge_pull_request(self, pull_request, user, extras):
1489 def _merge_pull_request(self, pull_request, user, extras):
1487 _ = self.request.translate
1490 _ = self.request.translate
1488 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1491 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1489
1492
1490 if merge_resp.executed:
1493 if merge_resp.executed:
1491 log.debug("The merge was successful, closing the pull request.")
1494 log.debug("The merge was successful, closing the pull request.")
1492 PullRequestModel().close_pull_request(
1495 PullRequestModel().close_pull_request(
1493 pull_request.pull_request_id, user)
1496 pull_request.pull_request_id, user)
1494 Session().commit()
1497 Session().commit()
1495 msg = _('Pull request was successfully merged and closed.')
1498 msg = _('Pull request was successfully merged and closed.')
1496 h.flash(msg, category='success')
1499 h.flash(msg, category='success')
1497 else:
1500 else:
1498 log.debug(
1501 log.debug(
1499 "The merge was not successful. Merge response: %s", merge_resp)
1502 "The merge was not successful. Merge response: %s", merge_resp)
1500 msg = merge_resp.merge_status_message
1503 msg = merge_resp.merge_status_message
1501 h.flash(msg, category='error')
1504 h.flash(msg, category='error')
1502
1505
1503 @LoginRequired()
1506 @LoginRequired()
1504 @NotAnonymous()
1507 @NotAnonymous()
1505 @HasRepoPermissionAnyDecorator(
1508 @HasRepoPermissionAnyDecorator(
1506 'repository.read', 'repository.write', 'repository.admin')
1509 'repository.read', 'repository.write', 'repository.admin')
1507 @CSRFRequired()
1510 @CSRFRequired()
1508 def pull_request_delete(self):
1511 def pull_request_delete(self):
1509 _ = self.request.translate
1512 _ = self.request.translate
1510
1513
1511 pull_request = PullRequest.get_or_404(
1514 pull_request = PullRequest.get_or_404(
1512 self.request.matchdict['pull_request_id'])
1515 self.request.matchdict['pull_request_id'])
1513 self.load_default_context()
1516 self.load_default_context()
1514
1517
1515 pr_closed = pull_request.is_closed()
1518 pr_closed = pull_request.is_closed()
1516 allowed_to_delete = PullRequestModel().check_user_delete(
1519 allowed_to_delete = PullRequestModel().check_user_delete(
1517 pull_request, self._rhodecode_user) and not pr_closed
1520 pull_request, self._rhodecode_user) and not pr_closed
1518
1521
1519 # only owner can delete it !
1522 # only owner can delete it !
1520 if allowed_to_delete:
1523 if allowed_to_delete:
1521 PullRequestModel().delete(pull_request, self._rhodecode_user)
1524 PullRequestModel().delete(pull_request, self._rhodecode_user)
1522 Session().commit()
1525 Session().commit()
1523 h.flash(_('Successfully deleted pull request'),
1526 h.flash(_('Successfully deleted pull request'),
1524 category='success')
1527 category='success')
1525 raise HTTPFound(h.route_path('pullrequest_show_all',
1528 raise HTTPFound(h.route_path('pullrequest_show_all',
1526 repo_name=self.db_repo_name))
1529 repo_name=self.db_repo_name))
1527
1530
1528 log.warning('user %s tried to delete pull request without access',
1531 log.warning('user %s tried to delete pull request without access',
1529 self._rhodecode_user)
1532 self._rhodecode_user)
1530 raise HTTPNotFound()
1533 raise HTTPNotFound()
1531
1534
1532 def _pull_request_comments_create(self, pull_request, comments):
1535 def _pull_request_comments_create(self, pull_request, comments):
1533 _ = self.request.translate
1536 _ = self.request.translate
1534 data = {}
1537 data = {}
1535 if not comments:
1538 if not comments:
1536 return
1539 return
1537 pull_request_id = pull_request.pull_request_id
1540 pull_request_id = pull_request.pull_request_id
1538
1541
1539 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1542 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1540
1543
1541 for entry in comments:
1544 for entry in comments:
1542 c = self.load_default_context()
1545 c = self.load_default_context()
1543 comment_type = entry['comment_type']
1546 comment_type = entry['comment_type']
1544 text = entry['text']
1547 text = entry['text']
1545 status = entry['status']
1548 status = entry['status']
1546 is_draft = str2bool(entry['is_draft'])
1549 is_draft = str2bool(entry['is_draft'])
1547 resolves_comment_id = entry['resolves_comment_id']
1550 resolves_comment_id = entry['resolves_comment_id']
1548 close_pull_request = entry['close_pull_request']
1551 close_pull_request = entry['close_pull_request']
1549 f_path = entry['f_path']
1552 f_path = entry['f_path']
1550 line_no = entry['line']
1553 line_no = entry['line']
1551 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1554 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1552
1555
1553 # the logic here should work like following, if we submit close
1556 # the logic here should work like following, if we submit close
1554 # pr comment, use `close_pull_request_with_comment` function
1557 # pr comment, use `close_pull_request_with_comment` function
1555 # else handle regular comment logic
1558 # else handle regular comment logic
1556
1559
1557 if close_pull_request:
1560 if close_pull_request:
1558 # only owner or admin or person with write permissions
1561 # only owner or admin or person with write permissions
1559 allowed_to_close = PullRequestModel().check_user_update(
1562 allowed_to_close = PullRequestModel().check_user_update(
1560 pull_request, self._rhodecode_user)
1563 pull_request, self._rhodecode_user)
1561 if not allowed_to_close:
1564 if not allowed_to_close:
1562 log.debug('comment: forbidden because not allowed to close '
1565 log.debug('comment: forbidden because not allowed to close '
1563 'pull request %s', pull_request_id)
1566 'pull request %s', pull_request_id)
1564 raise HTTPForbidden()
1567 raise HTTPForbidden()
1565
1568
1566 # This also triggers `review_status_change`
1569 # This also triggers `review_status_change`
1567 comment, status = PullRequestModel().close_pull_request_with_comment(
1570 comment, status = PullRequestModel().close_pull_request_with_comment(
1568 pull_request, self._rhodecode_user, self.db_repo, message=text,
1571 pull_request, self._rhodecode_user, self.db_repo, message=text,
1569 auth_user=self._rhodecode_user)
1572 auth_user=self._rhodecode_user)
1570 Session().flush()
1573 Session().flush()
1571 is_inline = comment.is_inline
1574 is_inline = comment.is_inline
1572
1575
1573 PullRequestModel().trigger_pull_request_hook(
1576 PullRequestModel().trigger_pull_request_hook(
1574 pull_request, self._rhodecode_user, 'comment',
1577 pull_request, self._rhodecode_user, 'comment',
1575 data={'comment': comment})
1578 data={'comment': comment})
1576
1579
1577 else:
1580 else:
1578 # regular comment case, could be inline, or one with status.
1581 # regular comment case, could be inline, or one with status.
1579 # for that one we check also permissions
1582 # for that one we check also permissions
1580 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1583 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1581 allowed_to_change_status = PullRequestModel().check_user_change_status(
1584 allowed_to_change_status = PullRequestModel().check_user_change_status(
1582 pull_request, self._rhodecode_user) and not is_draft
1585 pull_request, self._rhodecode_user) and not is_draft
1583
1586
1584 if status and allowed_to_change_status:
1587 if status and allowed_to_change_status:
1585 message = (_('Status change %(transition_icon)s %(status)s')
1588 message = (_('Status change %(transition_icon)s %(status)s')
1586 % {'transition_icon': '>',
1589 % {'transition_icon': '>',
1587 'status': ChangesetStatus.get_status_lbl(status)})
1590 'status': ChangesetStatus.get_status_lbl(status)})
1588 text = text or message
1591 text = text or message
1589
1592
1590 comment = CommentsModel().create(
1593 comment = CommentsModel().create(
1591 text=text,
1594 text=text,
1592 repo=self.db_repo.repo_id,
1595 repo=self.db_repo.repo_id,
1593 user=self._rhodecode_user.user_id,
1596 user=self._rhodecode_user.user_id,
1594 pull_request=pull_request,
1597 pull_request=pull_request,
1595 f_path=f_path,
1598 f_path=f_path,
1596 line_no=line_no,
1599 line_no=line_no,
1597 status_change=(ChangesetStatus.get_status_lbl(status)
1600 status_change=(ChangesetStatus.get_status_lbl(status)
1598 if status and allowed_to_change_status else None),
1601 if status and allowed_to_change_status else None),
1599 status_change_type=(status
1602 status_change_type=(status
1600 if status and allowed_to_change_status else None),
1603 if status and allowed_to_change_status else None),
1601 comment_type=comment_type,
1604 comment_type=comment_type,
1602 is_draft=is_draft,
1605 is_draft=is_draft,
1603 resolves_comment_id=resolves_comment_id,
1606 resolves_comment_id=resolves_comment_id,
1604 auth_user=self._rhodecode_user,
1607 auth_user=self._rhodecode_user,
1605 send_email=not is_draft, # skip notification for draft comments
1608 send_email=not is_draft, # skip notification for draft comments
1606 )
1609 )
1607 is_inline = comment.is_inline
1610 is_inline = comment.is_inline
1608
1611
1609 if allowed_to_change_status:
1612 if allowed_to_change_status:
1610 # calculate old status before we change it
1613 # calculate old status before we change it
1611 old_calculated_status = pull_request.calculated_review_status()
1614 old_calculated_status = pull_request.calculated_review_status()
1612
1615
1613 # get status if set !
1616 # get status if set !
1614 if status:
1617 if status:
1615 ChangesetStatusModel().set_status(
1618 ChangesetStatusModel().set_status(
1616 self.db_repo.repo_id,
1619 self.db_repo.repo_id,
1617 status,
1620 status,
1618 self._rhodecode_user.user_id,
1621 self._rhodecode_user.user_id,
1619 comment,
1622 comment,
1620 pull_request=pull_request
1623 pull_request=pull_request
1621 )
1624 )
1622
1625
1623 Session().flush()
1626 Session().flush()
1624 # this is somehow required to get access to some relationship
1627 # this is somehow required to get access to some relationship
1625 # loaded on comment
1628 # loaded on comment
1626 Session().refresh(comment)
1629 Session().refresh(comment)
1627
1630
1628 # skip notifications for drafts
1631 # skip notifications for drafts
1629 if not is_draft:
1632 if not is_draft:
1630 PullRequestModel().trigger_pull_request_hook(
1633 PullRequestModel().trigger_pull_request_hook(
1631 pull_request, self._rhodecode_user, 'comment',
1634 pull_request, self._rhodecode_user, 'comment',
1632 data={'comment': comment})
1635 data={'comment': comment})
1633
1636
1634 # we now calculate the status of pull request, and based on that
1637 # we now calculate the status of pull request, and based on that
1635 # calculation we set the commits status
1638 # calculation we set the commits status
1636 calculated_status = pull_request.calculated_review_status()
1639 calculated_status = pull_request.calculated_review_status()
1637 if old_calculated_status != calculated_status:
1640 if old_calculated_status != calculated_status:
1638 PullRequestModel().trigger_pull_request_hook(
1641 PullRequestModel().trigger_pull_request_hook(
1639 pull_request, self._rhodecode_user, 'review_status_change',
1642 pull_request, self._rhodecode_user, 'review_status_change',
1640 data={'status': calculated_status})
1643 data={'status': calculated_status})
1641
1644
1642 comment_id = comment.comment_id
1645 comment_id = comment.comment_id
1643 data[comment_id] = {
1646 data[comment_id] = {
1644 'target_id': target_elem_id
1647 'target_id': target_elem_id
1645 }
1648 }
1646 Session().flush()
1649 Session().flush()
1647
1650
1648 c.co = comment
1651 c.co = comment
1649 c.at_version_num = None
1652 c.at_version_num = None
1650 c.is_new = True
1653 c.is_new = True
1651 rendered_comment = render(
1654 rendered_comment = render(
1652 'rhodecode:templates/changeset/changeset_comment_block.mako',
1655 'rhodecode:templates/changeset/changeset_comment_block.mako',
1653 self._get_template_context(c), self.request)
1656 self._get_template_context(c), self.request)
1654
1657
1655 data[comment_id].update(comment.get_dict())
1658 data[comment_id].update(comment.get_dict())
1656 data[comment_id].update({'rendered_text': rendered_comment})
1659 data[comment_id].update({'rendered_text': rendered_comment})
1657
1660
1658 Session().commit()
1661 Session().commit()
1659
1662
1660 # skip channelstream for draft comments
1663 # skip channelstream for draft comments
1661 if not all_drafts:
1664 if not all_drafts:
1662 comment_broadcast_channel = channelstream.comment_channel(
1665 comment_broadcast_channel = channelstream.comment_channel(
1663 self.db_repo_name, pull_request_obj=pull_request)
1666 self.db_repo_name, pull_request_obj=pull_request)
1664
1667
1665 comment_data = data
1668 comment_data = data
1666 posted_comment_type = 'inline' if is_inline else 'general'
1669 posted_comment_type = 'inline' if is_inline else 'general'
1667 if len(data) == 1:
1670 if len(data) == 1:
1668 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1671 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1669 else:
1672 else:
1670 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1673 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1671
1674
1672 channelstream.comment_channelstream_push(
1675 channelstream.comment_channelstream_push(
1673 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1676 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1674 comment_data=comment_data)
1677 comment_data=comment_data)
1675
1678
1676 return data
1679 return data
1677
1680
1678 @LoginRequired()
1681 @LoginRequired()
1679 @NotAnonymous()
1682 @NotAnonymous()
1680 @HasRepoPermissionAnyDecorator(
1683 @HasRepoPermissionAnyDecorator(
1681 'repository.read', 'repository.write', 'repository.admin')
1684 'repository.read', 'repository.write', 'repository.admin')
1682 @CSRFRequired()
1685 @CSRFRequired()
1683 def pull_request_comment_create(self):
1686 def pull_request_comment_create(self):
1684 _ = self.request.translate
1687 _ = self.request.translate
1685
1688
1686 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1689 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1687
1690
1688 if pull_request.is_closed():
1691 if pull_request.is_closed():
1689 log.debug('comment: forbidden because pull request is closed')
1692 log.debug('comment: forbidden because pull request is closed')
1690 raise HTTPForbidden()
1693 raise HTTPForbidden()
1691
1694
1692 allowed_to_comment = PullRequestModel().check_user_comment(
1695 allowed_to_comment = PullRequestModel().check_user_comment(
1693 pull_request, self._rhodecode_user)
1696 pull_request, self._rhodecode_user)
1694 if not allowed_to_comment:
1697 if not allowed_to_comment:
1695 log.debug('comment: forbidden because pull request is from forbidden repo')
1698 log.debug('comment: forbidden because pull request is from forbidden repo')
1696 raise HTTPForbidden()
1699 raise HTTPForbidden()
1697
1700
1698 comment_data = {
1701 comment_data = {
1699 'comment_type': self.request.POST.get('comment_type'),
1702 'comment_type': self.request.POST.get('comment_type'),
1700 'text': self.request.POST.get('text'),
1703 'text': self.request.POST.get('text'),
1701 'status': self.request.POST.get('changeset_status', None),
1704 'status': self.request.POST.get('changeset_status', None),
1702 'is_draft': self.request.POST.get('draft'),
1705 'is_draft': self.request.POST.get('draft'),
1703 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1706 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1704 'close_pull_request': self.request.POST.get('close_pull_request'),
1707 'close_pull_request': self.request.POST.get('close_pull_request'),
1705 'f_path': self.request.POST.get('f_path'),
1708 'f_path': self.request.POST.get('f_path'),
1706 'line': self.request.POST.get('line'),
1709 'line': self.request.POST.get('line'),
1707 }
1710 }
1708 data = self._pull_request_comments_create(pull_request, [comment_data])
1711 data = self._pull_request_comments_create(pull_request, [comment_data])
1709
1712
1710 return data
1713 return data
1711
1714
1712 @LoginRequired()
1715 @LoginRequired()
1713 @NotAnonymous()
1716 @NotAnonymous()
1714 @HasRepoPermissionAnyDecorator(
1717 @HasRepoPermissionAnyDecorator(
1715 'repository.read', 'repository.write', 'repository.admin')
1718 'repository.read', 'repository.write', 'repository.admin')
1716 @CSRFRequired()
1719 @CSRFRequired()
1717 def pull_request_comment_delete(self):
1720 def pull_request_comment_delete(self):
1718 pull_request = PullRequest.get_or_404(
1721 pull_request = PullRequest.get_or_404(
1719 self.request.matchdict['pull_request_id'])
1722 self.request.matchdict['pull_request_id'])
1720
1723
1721 comment = ChangesetComment.get_or_404(
1724 comment = ChangesetComment.get_or_404(
1722 self.request.matchdict['comment_id'])
1725 self.request.matchdict['comment_id'])
1723 comment_id = comment.comment_id
1726 comment_id = comment.comment_id
1724
1727
1725 if comment.immutable:
1728 if comment.immutable:
1726 # don't allow deleting comments that are immutable
1729 # don't allow deleting comments that are immutable
1727 raise HTTPForbidden()
1730 raise HTTPForbidden()
1728
1731
1729 if pull_request.is_closed():
1732 if pull_request.is_closed():
1730 log.debug('comment: forbidden because pull request is closed')
1733 log.debug('comment: forbidden because pull request is closed')
1731 raise HTTPForbidden()
1734 raise HTTPForbidden()
1732
1735
1733 if not comment:
1736 if not comment:
1734 log.debug('Comment with id:%s not found, skipping', comment_id)
1737 log.debug('Comment with id:%s not found, skipping', comment_id)
1735 # comment already deleted in another call probably
1738 # comment already deleted in another call probably
1736 return True
1739 return True
1737
1740
1738 if comment.pull_request.is_closed():
1741 if comment.pull_request.is_closed():
1739 # don't allow deleting comments on closed pull request
1742 # don't allow deleting comments on closed pull request
1740 raise HTTPForbidden()
1743 raise HTTPForbidden()
1741
1744
1742 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1745 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1743 super_admin = h.HasPermissionAny('hg.admin')()
1746 super_admin = h.HasPermissionAny('hg.admin')()
1744 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1747 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1745 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1748 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1746 comment_repo_admin = is_repo_admin and is_repo_comment
1749 comment_repo_admin = is_repo_admin and is_repo_comment
1747
1750
1748 if super_admin or comment_owner or comment_repo_admin:
1751 if super_admin or comment_owner or comment_repo_admin:
1749 old_calculated_status = comment.pull_request.calculated_review_status()
1752 old_calculated_status = comment.pull_request.calculated_review_status()
1750 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1753 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1751 Session().commit()
1754 Session().commit()
1752 calculated_status = comment.pull_request.calculated_review_status()
1755 calculated_status = comment.pull_request.calculated_review_status()
1753 if old_calculated_status != calculated_status:
1756 if old_calculated_status != calculated_status:
1754 PullRequestModel().trigger_pull_request_hook(
1757 PullRequestModel().trigger_pull_request_hook(
1755 comment.pull_request, self._rhodecode_user, 'review_status_change',
1758 comment.pull_request, self._rhodecode_user, 'review_status_change',
1756 data={'status': calculated_status})
1759 data={'status': calculated_status})
1757 return True
1760 return True
1758 else:
1761 else:
1759 log.warning('No permissions for user %s to delete comment_id: %s',
1762 log.warning('No permissions for user %s to delete comment_id: %s',
1760 self._rhodecode_db_user, comment_id)
1763 self._rhodecode_db_user, comment_id)
1761 raise HTTPNotFound()
1764 raise HTTPNotFound()
1762
1765
1763 @LoginRequired()
1766 @LoginRequired()
1764 @NotAnonymous()
1767 @NotAnonymous()
1765 @HasRepoPermissionAnyDecorator(
1768 @HasRepoPermissionAnyDecorator(
1766 'repository.read', 'repository.write', 'repository.admin')
1769 'repository.read', 'repository.write', 'repository.admin')
1767 @CSRFRequired()
1770 @CSRFRequired()
1768 def pull_request_comment_edit(self):
1771 def pull_request_comment_edit(self):
1769 self.load_default_context()
1772 self.load_default_context()
1770
1773
1771 pull_request = PullRequest.get_or_404(
1774 pull_request = PullRequest.get_or_404(
1772 self.request.matchdict['pull_request_id']
1775 self.request.matchdict['pull_request_id']
1773 )
1776 )
1774 comment = ChangesetComment.get_or_404(
1777 comment = ChangesetComment.get_or_404(
1775 self.request.matchdict['comment_id']
1778 self.request.matchdict['comment_id']
1776 )
1779 )
1777 comment_id = comment.comment_id
1780 comment_id = comment.comment_id
1778
1781
1779 if comment.immutable:
1782 if comment.immutable:
1780 # don't allow deleting comments that are immutable
1783 # don't allow deleting comments that are immutable
1781 raise HTTPForbidden()
1784 raise HTTPForbidden()
1782
1785
1783 if pull_request.is_closed():
1786 if pull_request.is_closed():
1784 log.debug('comment: forbidden because pull request is closed')
1787 log.debug('comment: forbidden because pull request is closed')
1785 raise HTTPForbidden()
1788 raise HTTPForbidden()
1786
1789
1787 if comment.pull_request.is_closed():
1790 if comment.pull_request.is_closed():
1788 # don't allow deleting comments on closed pull request
1791 # don't allow deleting comments on closed pull request
1789 raise HTTPForbidden()
1792 raise HTTPForbidden()
1790
1793
1791 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1794 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1792 super_admin = h.HasPermissionAny('hg.admin')()
1795 super_admin = h.HasPermissionAny('hg.admin')()
1793 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1796 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1794 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1797 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1795 comment_repo_admin = is_repo_admin and is_repo_comment
1798 comment_repo_admin = is_repo_admin and is_repo_comment
1796
1799
1797 if super_admin or comment_owner or comment_repo_admin:
1800 if super_admin or comment_owner or comment_repo_admin:
1798 text = self.request.POST.get('text')
1801 text = self.request.POST.get('text')
1799 version = self.request.POST.get('version')
1802 version = self.request.POST.get('version')
1800 if text == comment.text:
1803 if text == comment.text:
1801 log.warning(
1804 log.warning(
1802 'Comment(PR): '
1805 'Comment(PR): '
1803 'Trying to create new version '
1806 'Trying to create new version '
1804 'with the same comment body {}'.format(
1807 'with the same comment body {}'.format(
1805 comment_id,
1808 comment_id,
1806 )
1809 )
1807 )
1810 )
1808 raise HTTPNotFound()
1811 raise HTTPNotFound()
1809
1812
1810 if version.isdigit():
1813 if version.isdigit():
1811 version = int(version)
1814 version = int(version)
1812 else:
1815 else:
1813 log.warning(
1816 log.warning(
1814 'Comment(PR): Wrong version type {} {} '
1817 'Comment(PR): Wrong version type {} {} '
1815 'for comment {}'.format(
1818 'for comment {}'.format(
1816 version,
1819 version,
1817 type(version),
1820 type(version),
1818 comment_id,
1821 comment_id,
1819 )
1822 )
1820 )
1823 )
1821 raise HTTPNotFound()
1824 raise HTTPNotFound()
1822
1825
1823 try:
1826 try:
1824 comment_history = CommentsModel().edit(
1827 comment_history = CommentsModel().edit(
1825 comment_id=comment_id,
1828 comment_id=comment_id,
1826 text=text,
1829 text=text,
1827 auth_user=self._rhodecode_user,
1830 auth_user=self._rhodecode_user,
1828 version=version,
1831 version=version,
1829 )
1832 )
1830 except CommentVersionMismatch:
1833 except CommentVersionMismatch:
1831 raise HTTPConflict()
1834 raise HTTPConflict()
1832
1835
1833 if not comment_history:
1836 if not comment_history:
1834 raise HTTPNotFound()
1837 raise HTTPNotFound()
1835
1838
1836 Session().commit()
1839 Session().commit()
1837 if not comment.draft:
1840 if not comment.draft:
1838 PullRequestModel().trigger_pull_request_hook(
1841 PullRequestModel().trigger_pull_request_hook(
1839 pull_request, self._rhodecode_user, 'comment_edit',
1842 pull_request, self._rhodecode_user, 'comment_edit',
1840 data={'comment': comment})
1843 data={'comment': comment})
1841
1844
1842 return {
1845 return {
1843 'comment_history_id': comment_history.comment_history_id,
1846 'comment_history_id': comment_history.comment_history_id,
1844 'comment_id': comment.comment_id,
1847 'comment_id': comment.comment_id,
1845 'comment_version': comment_history.version,
1848 'comment_version': comment_history.version,
1846 'comment_author_username': comment_history.author.username,
1849 'comment_author_username': comment_history.author.username,
1847 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1850 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1848 'comment_created_on': h.age_component(comment_history.created_on,
1851 'comment_created_on': h.age_component(comment_history.created_on,
1849 time_is_local=True),
1852 time_is_local=True),
1850 }
1853 }
1851 else:
1854 else:
1852 log.warning('No permissions for user %s to edit comment_id: %s',
1855 log.warning('No permissions for user %s to edit comment_id: %s',
1853 self._rhodecode_db_user, comment_id)
1856 self._rhodecode_db_user, comment_id)
1854 raise HTTPNotFound()
1857 raise HTTPNotFound()
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,2233 +1,2247 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 log.debug('Obtaining commit authors from set of commits')
157 log.debug('Obtaining commit authors from set of commits')
158 _compare_data = target_scm.compare(
158 _compare_data = target_scm.compare(
159 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
160 pre_load=["author", "date", "message"]
160 pre_load=["author", "date", "message"]
161 )
161 )
162
162
163 for commit in _compare_data:
163 for commit in _compare_data:
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
166 serialized_commit = dict(
167 author=commit.author,
167 author=commit.author,
168 date=commit.date,
168 date=commit.date,
169 message=commit.message,
169 message=commit.message,
170 commit_id=commit.raw_id,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
171 raw_id=commit.raw_id
172 )
172 )
173 commits.append(serialized_commit)
173 commits.append(serialized_commit)
174 user = User.get_from_cs_author(serialized_commit['author'])
174 user = User.get_from_cs_author(serialized_commit['author'])
175 if user and user not in commit_authors:
175 if user and user not in commit_authors:
176 commit_authors.append(user)
176 commit_authors.append(user)
177
177
178 # lines
178 # lines
179 if get_authors:
179 if get_authors:
180 log.debug('Calculating authors of changed files')
180 log.debug('Calculating authors of changed files')
181 target_commit = source_repo.get_commit(ancestor_id)
181 target_commit = source_repo.get_commit(ancestor_id)
182
182
183 for fname, lines in changed_lines.items():
183 for fname, lines in changed_lines.items():
184
184
185 try:
185 try:
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
187 except Exception:
187 except Exception:
188 log.exception("Failed to load node with path %s", fname)
188 log.exception("Failed to load node with path %s", fname)
189 continue
189 continue
190
190
191 if not isinstance(node, FileNode):
191 if not isinstance(node, FileNode):
192 continue
192 continue
193
193
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 if node.is_binary:
195 if node.is_binary:
196 author = node.last_commit.author
196 author = node.last_commit.author
197 email = node.last_commit.author_email
197 email = node.last_commit.author_email
198
198
199 user = User.get_from_cs_author(author)
199 user = User.get_from_cs_author(author)
200 if user:
200 if user:
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
204
204
205 continue
205 continue
206
206
207 for annotation in node.annotate:
207 for annotation in node.annotate:
208 line_no, commit_id, get_commit_func, line_text = annotation
208 line_no, commit_id, get_commit_func, line_text = annotation
209 if line_no in lines:
209 if line_no in lines:
210 if commit_id not in _commit_cache:
210 if commit_id not in _commit_cache:
211 _commit_cache[commit_id] = get_commit_func()
211 _commit_cache[commit_id] = get_commit_func()
212 commit = _commit_cache[commit_id]
212 commit = _commit_cache[commit_id]
213 author = commit.author
213 author = commit.author
214 email = commit.author_email
214 email = commit.author_email
215 user = User.get_from_cs_author(author)
215 user = User.get_from_cs_author(author)
216 if user:
216 if user:
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
220
220
221 log.debug('Default reviewers processing finished')
221 log.debug('Default reviewers processing finished')
222
222
223 return {
223 return {
224 'commits': commits,
224 'commits': commits,
225 'files': all_files_changes,
225 'files': all_files_changes,
226 'stats': stats,
226 'stats': stats,
227 'ancestor': ancestor_id,
227 'ancestor': ancestor_id,
228 # original authors of modified files
228 # original authors of modified files
229 'original_authors': {
229 'original_authors': {
230 'users': user_counts,
230 'users': user_counts,
231 'authors': author_counts,
231 'authors': author_counts,
232 'emails': email_counts,
232 'emails': email_counts,
233 },
233 },
234 'commit_authors': commit_authors
234 'commit_authors': commit_authors
235 }
235 }
236
236
237
237
238 class PullRequestModel(BaseModel):
238 class PullRequestModel(BaseModel):
239
239
240 cls = PullRequest
240 cls = PullRequest
241
241
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
243
243
244 UPDATE_STATUS_MESSAGES = {
244 UPDATE_STATUS_MESSAGES = {
245 UpdateFailureReason.NONE: lazy_ugettext(
245 UpdateFailureReason.NONE: lazy_ugettext(
246 'Pull request update successful.'),
246 'Pull request update successful.'),
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
248 'Pull request update failed because of an unknown error.'),
248 'Pull request update failed because of an unknown error.'),
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
250 'No update needed because the source and target have not changed.'),
250 'No update needed because the source and target have not changed.'),
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
252 'Pull request cannot be updated because the reference type is '
252 'Pull request cannot be updated because the reference type is '
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
255 'This pull request cannot be updated because the target '
255 'This pull request cannot be updated because the target '
256 'reference is missing.'),
256 'reference is missing.'),
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
258 'This pull request cannot be updated because the source '
258 'This pull request cannot be updated because the source '
259 'reference is missing.'),
259 'reference is missing.'),
260 }
260 }
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
263
263
264 def __get_pull_request(self, pull_request):
264 def __get_pull_request(self, pull_request):
265 return self._get_instance((
265 return self._get_instance((
266 PullRequest, PullRequestVersion), pull_request)
266 PullRequest, PullRequestVersion), pull_request)
267
267
268 def _check_perms(self, perms, pull_request, user, api=False):
268 def _check_perms(self, perms, pull_request, user, api=False):
269 if not api:
269 if not api:
270 return h.HasRepoPermissionAny(*perms)(
270 return h.HasRepoPermissionAny(*perms)(
271 user=user, repo_name=pull_request.target_repo.repo_name)
271 user=user, repo_name=pull_request.target_repo.repo_name)
272 else:
272 else:
273 return h.HasRepoPermissionAnyApi(*perms)(
273 return h.HasRepoPermissionAnyApi(*perms)(
274 user=user, repo_name=pull_request.target_repo.repo_name)
274 user=user, repo_name=pull_request.target_repo.repo_name)
275
275
276 def check_user_read(self, pull_request, user, api=False):
276 def check_user_read(self, pull_request, user, api=False):
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
278 return self._check_perms(_perms, pull_request, user, api)
278 return self._check_perms(_perms, pull_request, user, api)
279
279
280 def check_user_merge(self, pull_request, user, api=False):
280 def check_user_merge(self, pull_request, user, api=False):
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
282 return self._check_perms(_perms, pull_request, user, api)
282 return self._check_perms(_perms, pull_request, user, api)
283
283
284 def check_user_update(self, pull_request, user, api=False):
284 def check_user_update(self, pull_request, user, api=False):
285 owner = user.user_id == pull_request.user_id
285 owner = user.user_id == pull_request.user_id
286 return self.check_user_merge(pull_request, user, api) or owner
286 return self.check_user_merge(pull_request, user, api) or owner
287
287
288 def check_user_delete(self, pull_request, user):
288 def check_user_delete(self, pull_request, user):
289 owner = user.user_id == pull_request.user_id
289 owner = user.user_id == pull_request.user_id
290 _perms = ('repository.admin',)
290 _perms = ('repository.admin',)
291 return self._check_perms(_perms, pull_request, user) or owner
291 return self._check_perms(_perms, pull_request, user) or owner
292
292
293 def is_user_reviewer(self, pull_request, user):
293 def is_user_reviewer(self, pull_request, user):
294 return user.user_id in [
294 return user.user_id in [
295 x.user_id for x in
295 x.user_id for x in
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 if x.user
297 if x.user
298 ]
298 ]
299
299
300 def check_user_change_status(self, pull_request, user, api=False):
300 def check_user_change_status(self, pull_request, user, api=False):
301 return self.check_user_update(pull_request, user, api) \
301 return self.check_user_update(pull_request, user, api) \
302 or self.is_user_reviewer(pull_request, user)
302 or self.is_user_reviewer(pull_request, user)
303
303
304 def check_user_comment(self, pull_request, user):
304 def check_user_comment(self, pull_request, user):
305 owner = user.user_id == pull_request.user_id
305 owner = user.user_id == pull_request.user_id
306 return self.check_user_read(pull_request, user) or owner
306 return self.check_user_read(pull_request, user) or owner
307
307
308 def get(self, pull_request):
308 def get(self, pull_request):
309 return self.__get_pull_request(pull_request)
309 return self.__get_pull_request(pull_request)
310
310
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
312 statuses=None, opened_by=None, order_by=None,
312 statuses=None, opened_by=None, order_by=None,
313 order_dir='desc', only_created=False):
313 order_dir='desc', only_created=False):
314 repo = None
314 repo = None
315 if repo_name:
315 if repo_name:
316 repo = self._get_repo(repo_name)
316 repo = self._get_repo(repo_name)
317
317
318 q = PullRequest.query()
318 q = PullRequest.query()
319
319
320 if search_q:
320 if search_q:
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
322 q = q.join(User)
322 q = q.join(User)
323 q = q.filter(or_(
323 q = q.filter(or_(
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
325 User.username.ilike(like_expression),
325 User.username.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
328 ))
328 ))
329
329
330 # source or target
330 # source or target
331 if repo and source:
331 if repo and source:
332 q = q.filter(PullRequest.source_repo == repo)
332 q = q.filter(PullRequest.source_repo == repo)
333 elif repo:
333 elif repo:
334 q = q.filter(PullRequest.target_repo == repo)
334 q = q.filter(PullRequest.target_repo == repo)
335
335
336 # closed,opened
336 # closed,opened
337 if statuses:
337 if statuses:
338 q = q.filter(PullRequest.status.in_(statuses))
338 q = q.filter(PullRequest.status.in_(statuses))
339
339
340 # opened by filter
340 # opened by filter
341 if opened_by:
341 if opened_by:
342 q = q.filter(PullRequest.user_id.in_(opened_by))
342 q = q.filter(PullRequest.user_id.in_(opened_by))
343
343
344 # only get those that are in "created" state
344 # only get those that are in "created" state
345 if only_created:
345 if only_created:
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
347
347
348 if order_by:
348 if order_by:
349 order_map = {
349 order_map = {
350 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
352 'title': PullRequest.title,
352 'title': PullRequest.title,
353 'updated_on_raw': PullRequest.updated_on,
353 'updated_on_raw': PullRequest.updated_on,
354 'target_repo': PullRequest.target_repo_id
354 'target_repo': PullRequest.target_repo_id
355 }
355 }
356 if order_dir == 'asc':
356 if order_dir == 'asc':
357 q = q.order_by(order_map[order_by].asc())
357 q = q.order_by(order_map[order_by].asc())
358 else:
358 else:
359 q = q.order_by(order_map[order_by].desc())
359 q = q.order_by(order_map[order_by].desc())
360
360
361 return q
361 return q
362
362
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
364 opened_by=None):
364 opened_by=None):
365 """
365 """
366 Count the number of pull requests for a specific repository.
366 Count the number of pull requests for a specific repository.
367
367
368 :param repo_name: target or source repo
368 :param repo_name: target or source repo
369 :param search_q: filter by text
369 :param search_q: filter by text
370 :param source: boolean flag to specify if repo_name refers to source
370 :param source: boolean flag to specify if repo_name refers to source
371 :param statuses: list of pull request statuses
371 :param statuses: list of pull request statuses
372 :param opened_by: author user of the pull request
372 :param opened_by: author user of the pull request
373 :returns: int number of pull requests
373 :returns: int number of pull requests
374 """
374 """
375 q = self._prepare_get_all_query(
375 q = self._prepare_get_all_query(
376 repo_name, search_q=search_q, source=source, statuses=statuses,
376 repo_name, search_q=search_q, source=source, statuses=statuses,
377 opened_by=opened_by)
377 opened_by=opened_by)
378
378
379 return q.count()
379 return q.count()
380
380
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
383 """
383 """
384 Get all pull requests for a specific repository.
384 Get all pull requests for a specific repository.
385
385
386 :param repo_name: target or source repo
386 :param repo_name: target or source repo
387 :param search_q: filter by text
387 :param search_q: filter by text
388 :param source: boolean flag to specify if repo_name refers to source
388 :param source: boolean flag to specify if repo_name refers to source
389 :param statuses: list of pull request statuses
389 :param statuses: list of pull request statuses
390 :param opened_by: author user of the pull request
390 :param opened_by: author user of the pull request
391 :param offset: pagination offset
391 :param offset: pagination offset
392 :param length: length of returned list
392 :param length: length of returned list
393 :param order_by: order of the returned list
393 :param order_by: order of the returned list
394 :param order_dir: 'asc' or 'desc' ordering direction
394 :param order_dir: 'asc' or 'desc' ordering direction
395 :returns: list of pull requests
395 :returns: list of pull requests
396 """
396 """
397 q = self._prepare_get_all_query(
397 q = self._prepare_get_all_query(
398 repo_name, search_q=search_q, source=source, statuses=statuses,
398 repo_name, search_q=search_q, source=source, statuses=statuses,
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
409 opened_by=None):
409 opened_by=None):
410 """
410 """
411 Count the number of pull requests for a specific repository that are
411 Count the number of pull requests for a specific repository that are
412 awaiting review.
412 awaiting review.
413
413
414 :param repo_name: target or source repo
414 :param repo_name: target or source repo
415 :param search_q: filter by text
415 :param search_q: filter by text
416 :param source: boolean flag to specify if repo_name refers to source
416 :param source: boolean flag to specify if repo_name refers to source
417 :param statuses: list of pull request statuses
417 :param statuses: list of pull request statuses
418 :param opened_by: author user of the pull request
418 :param opened_by: author user of the pull request
419 :returns: int number of pull requests
419 :returns: int number of pull requests
420 """
420 """
421 pull_requests = self.get_awaiting_review(
421 pull_requests = self.get_awaiting_review(
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
423
423
424 return len(pull_requests)
424 return len(pull_requests)
425
425
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
427 opened_by=None, offset=0, length=None,
427 opened_by=None, offset=0, length=None,
428 order_by=None, order_dir='desc'):
428 order_by=None, order_dir='desc'):
429 """
429 """
430 Get all pull requests for a specific repository that are awaiting
430 Get all pull requests for a specific repository that are awaiting
431 review.
431 review.
432
432
433 :param repo_name: target or source repo
433 :param repo_name: target or source repo
434 :param search_q: filter by text
434 :param search_q: filter by text
435 :param source: boolean flag to specify if repo_name refers to source
435 :param source: boolean flag to specify if repo_name refers to source
436 :param statuses: list of pull request statuses
436 :param statuses: list of pull request statuses
437 :param opened_by: author user of the pull request
437 :param opened_by: author user of the pull request
438 :param offset: pagination offset
438 :param offset: pagination offset
439 :param length: length of returned list
439 :param length: length of returned list
440 :param order_by: order of the returned list
440 :param order_by: order of the returned list
441 :param order_dir: 'asc' or 'desc' ordering direction
441 :param order_dir: 'asc' or 'desc' ordering direction
442 :returns: list of pull requests
442 :returns: list of pull requests
443 """
443 """
444 pull_requests = self.get_all(
444 pull_requests = self.get_all(
445 repo_name, search_q=search_q, source=source, statuses=statuses,
445 repo_name, search_q=search_q, source=source, statuses=statuses,
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
447
447
448 _filtered_pull_requests = []
448 _filtered_pull_requests = []
449 for pr in pull_requests:
449 for pr in pull_requests:
450 status = pr.calculated_review_status()
450 status = pr.calculated_review_status()
451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
452 ChangesetStatus.STATUS_UNDER_REVIEW]:
452 ChangesetStatus.STATUS_UNDER_REVIEW]:
453 _filtered_pull_requests.append(pr)
453 _filtered_pull_requests.append(pr)
454 if length:
454 if length:
455 return _filtered_pull_requests[offset:offset+length]
455 return _filtered_pull_requests[offset:offset+length]
456 else:
456 else:
457 return _filtered_pull_requests
457 return _filtered_pull_requests
458
458
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
460 opened_by=None, user_id=None):
460 opened_by=None, user_id=None):
461 """
461 """
462 Count the number of pull requests for a specific repository that are
462 Count the number of pull requests for a specific repository that are
463 awaiting review from a specific user.
463 awaiting review from a specific user.
464
464
465 :param repo_name: target or source repo
465 :param repo_name: target or source repo
466 :param search_q: filter by text
466 :param search_q: filter by text
467 :param source: boolean flag to specify if repo_name refers to source
467 :param source: boolean flag to specify if repo_name refers to source
468 :param statuses: list of pull request statuses
468 :param statuses: list of pull request statuses
469 :param opened_by: author user of the pull request
469 :param opened_by: author user of the pull request
470 :param user_id: reviewer user of the pull request
470 :param user_id: reviewer user of the pull request
471 :returns: int number of pull requests
471 :returns: int number of pull requests
472 """
472 """
473 pull_requests = self.get_awaiting_my_review(
473 pull_requests = self.get_awaiting_my_review(
474 repo_name, search_q=search_q, source=source, statuses=statuses,
474 repo_name, search_q=search_q, source=source, statuses=statuses,
475 opened_by=opened_by, user_id=user_id)
475 opened_by=opened_by, user_id=user_id)
476
476
477 return len(pull_requests)
477 return len(pull_requests)
478
478
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
480 opened_by=None, user_id=None, offset=0,
480 opened_by=None, user_id=None, offset=0,
481 length=None, order_by=None, order_dir='desc'):
481 length=None, order_by=None, order_dir='desc'):
482 """
482 """
483 Get all pull requests for a specific repository that are awaiting
483 Get all pull requests for a specific repository that are awaiting
484 review from a specific user.
484 review from a specific user.
485
485
486 :param repo_name: target or source repo
486 :param repo_name: target or source repo
487 :param search_q: filter by text
487 :param search_q: filter by text
488 :param source: boolean flag to specify if repo_name refers to source
488 :param source: boolean flag to specify if repo_name refers to source
489 :param statuses: list of pull request statuses
489 :param statuses: list of pull request statuses
490 :param opened_by: author user of the pull request
490 :param opened_by: author user of the pull request
491 :param user_id: reviewer user of the pull request
491 :param user_id: reviewer user of the pull request
492 :param offset: pagination offset
492 :param offset: pagination offset
493 :param length: length of returned list
493 :param length: length of returned list
494 :param order_by: order of the returned list
494 :param order_by: order of the returned list
495 :param order_dir: 'asc' or 'desc' ordering direction
495 :param order_dir: 'asc' or 'desc' ordering direction
496 :returns: list of pull requests
496 :returns: list of pull requests
497 """
497 """
498 pull_requests = self.get_all(
498 pull_requests = self.get_all(
499 repo_name, search_q=search_q, source=source, statuses=statuses,
499 repo_name, search_q=search_q, source=source, statuses=statuses,
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
501
501
502 _my = PullRequestModel().get_not_reviewed(user_id)
502 _my = PullRequestModel().get_not_reviewed(user_id)
503 my_participation = []
503 my_participation = []
504 for pr in pull_requests:
504 for pr in pull_requests:
505 if pr in _my:
505 if pr in _my:
506 my_participation.append(pr)
506 my_participation.append(pr)
507 _filtered_pull_requests = my_participation
507 _filtered_pull_requests = my_participation
508 if length:
508 if length:
509 return _filtered_pull_requests[offset:offset+length]
509 return _filtered_pull_requests[offset:offset+length]
510 else:
510 else:
511 return _filtered_pull_requests
511 return _filtered_pull_requests
512
512
513 def get_not_reviewed(self, user_id):
513 def get_not_reviewed(self, user_id):
514 return [
514 return [
515 x.pull_request for x in PullRequestReviewers.query().filter(
515 x.pull_request for x in PullRequestReviewers.query().filter(
516 PullRequestReviewers.user_id == user_id).all()
516 PullRequestReviewers.user_id == user_id).all()
517 ]
517 ]
518
518
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
520 order_by=None, order_dir='desc'):
520 order_by=None, order_dir='desc'):
521 q = PullRequest.query()
521 q = PullRequest.query()
522 if user_id:
522 if user_id:
523 reviewers_subquery = Session().query(
523 reviewers_subquery = Session().query(
524 PullRequestReviewers.pull_request_id).filter(
524 PullRequestReviewers.pull_request_id).filter(
525 PullRequestReviewers.user_id == user_id).subquery()
525 PullRequestReviewers.user_id == user_id).subquery()
526 user_filter = or_(
526 user_filter = or_(
527 PullRequest.user_id == user_id,
527 PullRequest.user_id == user_id,
528 PullRequest.pull_request_id.in_(reviewers_subquery)
528 PullRequest.pull_request_id.in_(reviewers_subquery)
529 )
529 )
530 q = PullRequest.query().filter(user_filter)
530 q = PullRequest.query().filter(user_filter)
531
531
532 # closed,opened
532 # closed,opened
533 if statuses:
533 if statuses:
534 q = q.filter(PullRequest.status.in_(statuses))
534 q = q.filter(PullRequest.status.in_(statuses))
535
535
536 if query:
536 if query:
537 like_expression = u'%{}%'.format(safe_unicode(query))
537 like_expression = u'%{}%'.format(safe_unicode(query))
538 q = q.join(User)
538 q = q.join(User)
539 q = q.filter(or_(
539 q = q.filter(or_(
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
541 User.username.ilike(like_expression),
541 User.username.ilike(like_expression),
542 PullRequest.title.ilike(like_expression),
542 PullRequest.title.ilike(like_expression),
543 PullRequest.description.ilike(like_expression),
543 PullRequest.description.ilike(like_expression),
544 ))
544 ))
545 if order_by:
545 if order_by:
546 order_map = {
546 order_map = {
547 'name_raw': PullRequest.pull_request_id,
547 'name_raw': PullRequest.pull_request_id,
548 'title': PullRequest.title,
548 'title': PullRequest.title,
549 'updated_on_raw': PullRequest.updated_on,
549 'updated_on_raw': PullRequest.updated_on,
550 'target_repo': PullRequest.target_repo_id
550 'target_repo': PullRequest.target_repo_id
551 }
551 }
552 if order_dir == 'asc':
552 if order_dir == 'asc':
553 q = q.order_by(order_map[order_by].asc())
553 q = q.order_by(order_map[order_by].asc())
554 else:
554 else:
555 q = q.order_by(order_map[order_by].desc())
555 q = q.order_by(order_map[order_by].desc())
556
556
557 return q
557 return q
558
558
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
561 return q.count()
561 return q.count()
562
562
563 def get_im_participating_in(
563 def get_im_participating_in(
564 self, user_id=None, statuses=None, query='', offset=0,
564 self, user_id=None, statuses=None, query='', offset=0,
565 length=None, order_by=None, order_dir='desc'):
565 length=None, order_by=None, order_dir='desc'):
566 """
566 """
567 Get all Pull requests that i'm participating in, or i have opened
567 Get all Pull requests that i'm participating in, or i have opened
568 """
568 """
569
569
570 q = self._prepare_participating_query(
570 q = self._prepare_participating_query(
571 user_id, statuses=statuses, query=query, order_by=order_by,
571 user_id, statuses=statuses, query=query, order_by=order_by,
572 order_dir=order_dir)
572 order_dir=order_dir)
573
573
574 if length:
574 if length:
575 pull_requests = q.limit(length).offset(offset).all()
575 pull_requests = q.limit(length).offset(offset).all()
576 else:
576 else:
577 pull_requests = q.all()
577 pull_requests = q.all()
578
578
579 return pull_requests
579 return pull_requests
580
580
581 def get_versions(self, pull_request):
581 def get_versions(self, pull_request):
582 """
582 """
583 returns version of pull request sorted by ID descending
583 returns version of pull request sorted by ID descending
584 """
584 """
585 return PullRequestVersion.query()\
585 return PullRequestVersion.query()\
586 .filter(PullRequestVersion.pull_request == pull_request)\
586 .filter(PullRequestVersion.pull_request == pull_request)\
587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
588 .all()
588 .all()
589
589
590 def get_pr_version(self, pull_request_id, version=None):
590 def get_pr_version(self, pull_request_id, version=None):
591 at_version = None
591 at_version = None
592
592
593 if version and version == 'latest':
593 if version and version == 'latest':
594 pull_request_ver = PullRequest.get(pull_request_id)
594 pull_request_ver = PullRequest.get(pull_request_id)
595 pull_request_obj = pull_request_ver
595 pull_request_obj = pull_request_ver
596 _org_pull_request_obj = pull_request_obj
596 _org_pull_request_obj = pull_request_obj
597 at_version = 'latest'
597 at_version = 'latest'
598 elif version:
598 elif version:
599 pull_request_ver = PullRequestVersion.get_or_404(version)
599 pull_request_ver = PullRequestVersion.get_or_404(version)
600 pull_request_obj = pull_request_ver
600 pull_request_obj = pull_request_ver
601 _org_pull_request_obj = pull_request_ver.pull_request
601 _org_pull_request_obj = pull_request_ver.pull_request
602 at_version = pull_request_ver.pull_request_version_id
602 at_version = pull_request_ver.pull_request_version_id
603 else:
603 else:
604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
605 pull_request_id)
605 pull_request_id)
606
606
607 pull_request_display_obj = PullRequest.get_pr_display_object(
607 pull_request_display_obj = PullRequest.get_pr_display_object(
608 pull_request_obj, _org_pull_request_obj)
608 pull_request_obj, _org_pull_request_obj)
609
609
610 return _org_pull_request_obj, pull_request_obj, \
610 return _org_pull_request_obj, pull_request_obj, \
611 pull_request_display_obj, at_version
611 pull_request_display_obj, at_version
612
612
613 def pr_commits_versions(self, versions):
614 """
615 Maps the pull-request commits into all known PR versions. This way we can obtain
616 each pr version the commit was introduced in.
617 """
618 commit_versions = collections.defaultdict(list)
619 num_versions = [x.pull_request_version_id for x in versions]
620 for ver in versions:
621 for commit_id in ver.revisions:
622 ver_idx = ChangesetComment.get_index_from_version(
623 ver.pull_request_version_id, num_versions=num_versions)
624 commit_versions[commit_id].append(ver_idx)
625 return commit_versions
626
613 def create(self, created_by, source_repo, source_ref, target_repo,
627 def create(self, created_by, source_repo, source_ref, target_repo,
614 target_ref, revisions, reviewers, observers, title, description=None,
628 target_ref, revisions, reviewers, observers, title, description=None,
615 common_ancestor_id=None,
629 common_ancestor_id=None,
616 description_renderer=None,
630 description_renderer=None,
617 reviewer_data=None, translator=None, auth_user=None):
631 reviewer_data=None, translator=None, auth_user=None):
618 translator = translator or get_current_request().translate
632 translator = translator or get_current_request().translate
619
633
620 created_by_user = self._get_user(created_by)
634 created_by_user = self._get_user(created_by)
621 auth_user = auth_user or created_by_user.AuthUser()
635 auth_user = auth_user or created_by_user.AuthUser()
622 source_repo = self._get_repo(source_repo)
636 source_repo = self._get_repo(source_repo)
623 target_repo = self._get_repo(target_repo)
637 target_repo = self._get_repo(target_repo)
624
638
625 pull_request = PullRequest()
639 pull_request = PullRequest()
626 pull_request.source_repo = source_repo
640 pull_request.source_repo = source_repo
627 pull_request.source_ref = source_ref
641 pull_request.source_ref = source_ref
628 pull_request.target_repo = target_repo
642 pull_request.target_repo = target_repo
629 pull_request.target_ref = target_ref
643 pull_request.target_ref = target_ref
630 pull_request.revisions = revisions
644 pull_request.revisions = revisions
631 pull_request.title = title
645 pull_request.title = title
632 pull_request.description = description
646 pull_request.description = description
633 pull_request.description_renderer = description_renderer
647 pull_request.description_renderer = description_renderer
634 pull_request.author = created_by_user
648 pull_request.author = created_by_user
635 pull_request.reviewer_data = reviewer_data
649 pull_request.reviewer_data = reviewer_data
636 pull_request.pull_request_state = pull_request.STATE_CREATING
650 pull_request.pull_request_state = pull_request.STATE_CREATING
637 pull_request.common_ancestor_id = common_ancestor_id
651 pull_request.common_ancestor_id = common_ancestor_id
638
652
639 Session().add(pull_request)
653 Session().add(pull_request)
640 Session().flush()
654 Session().flush()
641
655
642 reviewer_ids = set()
656 reviewer_ids = set()
643 # members / reviewers
657 # members / reviewers
644 for reviewer_object in reviewers:
658 for reviewer_object in reviewers:
645 user_id, reasons, mandatory, role, rules = reviewer_object
659 user_id, reasons, mandatory, role, rules = reviewer_object
646 user = self._get_user(user_id)
660 user = self._get_user(user_id)
647
661
648 # skip duplicates
662 # skip duplicates
649 if user.user_id in reviewer_ids:
663 if user.user_id in reviewer_ids:
650 continue
664 continue
651
665
652 reviewer_ids.add(user.user_id)
666 reviewer_ids.add(user.user_id)
653
667
654 reviewer = PullRequestReviewers()
668 reviewer = PullRequestReviewers()
655 reviewer.user = user
669 reviewer.user = user
656 reviewer.pull_request = pull_request
670 reviewer.pull_request = pull_request
657 reviewer.reasons = reasons
671 reviewer.reasons = reasons
658 reviewer.mandatory = mandatory
672 reviewer.mandatory = mandatory
659 reviewer.role = role
673 reviewer.role = role
660
674
661 # NOTE(marcink): pick only first rule for now
675 # NOTE(marcink): pick only first rule for now
662 rule_id = list(rules)[0] if rules else None
676 rule_id = list(rules)[0] if rules else None
663 rule = RepoReviewRule.get(rule_id) if rule_id else None
677 rule = RepoReviewRule.get(rule_id) if rule_id else None
664 if rule:
678 if rule:
665 review_group = rule.user_group_vote_rule(user_id)
679 review_group = rule.user_group_vote_rule(user_id)
666 # we check if this particular reviewer is member of a voting group
680 # we check if this particular reviewer is member of a voting group
667 if review_group:
681 if review_group:
668 # NOTE(marcink):
682 # NOTE(marcink):
669 # can be that user is member of more but we pick the first same,
683 # can be that user is member of more but we pick the first same,
670 # same as default reviewers algo
684 # same as default reviewers algo
671 review_group = review_group[0]
685 review_group = review_group[0]
672
686
673 rule_data = {
687 rule_data = {
674 'rule_name':
688 'rule_name':
675 rule.review_rule_name,
689 rule.review_rule_name,
676 'rule_user_group_entry_id':
690 'rule_user_group_entry_id':
677 review_group.repo_review_rule_users_group_id,
691 review_group.repo_review_rule_users_group_id,
678 'rule_user_group_name':
692 'rule_user_group_name':
679 review_group.users_group.users_group_name,
693 review_group.users_group.users_group_name,
680 'rule_user_group_members':
694 'rule_user_group_members':
681 [x.user.username for x in review_group.users_group.members],
695 [x.user.username for x in review_group.users_group.members],
682 'rule_user_group_members_id':
696 'rule_user_group_members_id':
683 [x.user.user_id for x in review_group.users_group.members],
697 [x.user.user_id for x in review_group.users_group.members],
684 }
698 }
685 # e.g {'vote_rule': -1, 'mandatory': True}
699 # e.g {'vote_rule': -1, 'mandatory': True}
686 rule_data.update(review_group.rule_data())
700 rule_data.update(review_group.rule_data())
687
701
688 reviewer.rule_data = rule_data
702 reviewer.rule_data = rule_data
689
703
690 Session().add(reviewer)
704 Session().add(reviewer)
691 Session().flush()
705 Session().flush()
692
706
693 for observer_object in observers:
707 for observer_object in observers:
694 user_id, reasons, mandatory, role, rules = observer_object
708 user_id, reasons, mandatory, role, rules = observer_object
695 user = self._get_user(user_id)
709 user = self._get_user(user_id)
696
710
697 # skip duplicates from reviewers
711 # skip duplicates from reviewers
698 if user.user_id in reviewer_ids:
712 if user.user_id in reviewer_ids:
699 continue
713 continue
700
714
701 #reviewer_ids.add(user.user_id)
715 #reviewer_ids.add(user.user_id)
702
716
703 observer = PullRequestReviewers()
717 observer = PullRequestReviewers()
704 observer.user = user
718 observer.user = user
705 observer.pull_request = pull_request
719 observer.pull_request = pull_request
706 observer.reasons = reasons
720 observer.reasons = reasons
707 observer.mandatory = mandatory
721 observer.mandatory = mandatory
708 observer.role = role
722 observer.role = role
709
723
710 # NOTE(marcink): pick only first rule for now
724 # NOTE(marcink): pick only first rule for now
711 rule_id = list(rules)[0] if rules else None
725 rule_id = list(rules)[0] if rules else None
712 rule = RepoReviewRule.get(rule_id) if rule_id else None
726 rule = RepoReviewRule.get(rule_id) if rule_id else None
713 if rule:
727 if rule:
714 # TODO(marcink): do we need this for observers ??
728 # TODO(marcink): do we need this for observers ??
715 pass
729 pass
716
730
717 Session().add(observer)
731 Session().add(observer)
718 Session().flush()
732 Session().flush()
719
733
720 # Set approval status to "Under Review" for all commits which are
734 # Set approval status to "Under Review" for all commits which are
721 # part of this pull request.
735 # part of this pull request.
722 ChangesetStatusModel().set_status(
736 ChangesetStatusModel().set_status(
723 repo=target_repo,
737 repo=target_repo,
724 status=ChangesetStatus.STATUS_UNDER_REVIEW,
738 status=ChangesetStatus.STATUS_UNDER_REVIEW,
725 user=created_by_user,
739 user=created_by_user,
726 pull_request=pull_request
740 pull_request=pull_request
727 )
741 )
728 # we commit early at this point. This has to do with a fact
742 # we commit early at this point. This has to do with a fact
729 # that before queries do some row-locking. And because of that
743 # that before queries do some row-locking. And because of that
730 # we need to commit and finish transaction before below validate call
744 # we need to commit and finish transaction before below validate call
731 # that for large repos could be long resulting in long row locks
745 # that for large repos could be long resulting in long row locks
732 Session().commit()
746 Session().commit()
733
747
734 # prepare workspace, and run initial merge simulation. Set state during that
748 # prepare workspace, and run initial merge simulation. Set state during that
735 # operation
749 # operation
736 pull_request = PullRequest.get(pull_request.pull_request_id)
750 pull_request = PullRequest.get(pull_request.pull_request_id)
737
751
738 # set as merging, for merge simulation, and if finished to created so we mark
752 # set as merging, for merge simulation, and if finished to created so we mark
739 # simulation is working fine
753 # simulation is working fine
740 with pull_request.set_state(PullRequest.STATE_MERGING,
754 with pull_request.set_state(PullRequest.STATE_MERGING,
741 final_state=PullRequest.STATE_CREATED) as state_obj:
755 final_state=PullRequest.STATE_CREATED) as state_obj:
742 MergeCheck.validate(
756 MergeCheck.validate(
743 pull_request, auth_user=auth_user, translator=translator)
757 pull_request, auth_user=auth_user, translator=translator)
744
758
745 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
759 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
746 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
760 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
747
761
748 creation_data = pull_request.get_api_data(with_merge_state=False)
762 creation_data = pull_request.get_api_data(with_merge_state=False)
749 self._log_audit_action(
763 self._log_audit_action(
750 'repo.pull_request.create', {'data': creation_data},
764 'repo.pull_request.create', {'data': creation_data},
751 auth_user, pull_request)
765 auth_user, pull_request)
752
766
753 return pull_request
767 return pull_request
754
768
755 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
769 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
756 pull_request = self.__get_pull_request(pull_request)
770 pull_request = self.__get_pull_request(pull_request)
757 target_scm = pull_request.target_repo.scm_instance()
771 target_scm = pull_request.target_repo.scm_instance()
758 if action == 'create':
772 if action == 'create':
759 trigger_hook = hooks_utils.trigger_create_pull_request_hook
773 trigger_hook = hooks_utils.trigger_create_pull_request_hook
760 elif action == 'merge':
774 elif action == 'merge':
761 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
775 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
762 elif action == 'close':
776 elif action == 'close':
763 trigger_hook = hooks_utils.trigger_close_pull_request_hook
777 trigger_hook = hooks_utils.trigger_close_pull_request_hook
764 elif action == 'review_status_change':
778 elif action == 'review_status_change':
765 trigger_hook = hooks_utils.trigger_review_pull_request_hook
779 trigger_hook = hooks_utils.trigger_review_pull_request_hook
766 elif action == 'update':
780 elif action == 'update':
767 trigger_hook = hooks_utils.trigger_update_pull_request_hook
781 trigger_hook = hooks_utils.trigger_update_pull_request_hook
768 elif action == 'comment':
782 elif action == 'comment':
769 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
783 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
770 elif action == 'comment_edit':
784 elif action == 'comment_edit':
771 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
785 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
772 else:
786 else:
773 return
787 return
774
788
775 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
789 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
776 pull_request, action, trigger_hook)
790 pull_request, action, trigger_hook)
777 trigger_hook(
791 trigger_hook(
778 username=user.username,
792 username=user.username,
779 repo_name=pull_request.target_repo.repo_name,
793 repo_name=pull_request.target_repo.repo_name,
780 repo_type=target_scm.alias,
794 repo_type=target_scm.alias,
781 pull_request=pull_request,
795 pull_request=pull_request,
782 data=data)
796 data=data)
783
797
784 def _get_commit_ids(self, pull_request):
798 def _get_commit_ids(self, pull_request):
785 """
799 """
786 Return the commit ids of the merged pull request.
800 Return the commit ids of the merged pull request.
787
801
788 This method is not dealing correctly yet with the lack of autoupdates
802 This method is not dealing correctly yet with the lack of autoupdates
789 nor with the implicit target updates.
803 nor with the implicit target updates.
790 For example: if a commit in the source repo is already in the target it
804 For example: if a commit in the source repo is already in the target it
791 will be reported anyways.
805 will be reported anyways.
792 """
806 """
793 merge_rev = pull_request.merge_rev
807 merge_rev = pull_request.merge_rev
794 if merge_rev is None:
808 if merge_rev is None:
795 raise ValueError('This pull request was not merged yet')
809 raise ValueError('This pull request was not merged yet')
796
810
797 commit_ids = list(pull_request.revisions)
811 commit_ids = list(pull_request.revisions)
798 if merge_rev not in commit_ids:
812 if merge_rev not in commit_ids:
799 commit_ids.append(merge_rev)
813 commit_ids.append(merge_rev)
800
814
801 return commit_ids
815 return commit_ids
802
816
803 def merge_repo(self, pull_request, user, extras):
817 def merge_repo(self, pull_request, user, extras):
804 log.debug("Merging pull request %s", pull_request.pull_request_id)
818 log.debug("Merging pull request %s", pull_request.pull_request_id)
805 extras['user_agent'] = 'internal-merge'
819 extras['user_agent'] = 'internal-merge'
806 merge_state = self._merge_pull_request(pull_request, user, extras)
820 merge_state = self._merge_pull_request(pull_request, user, extras)
807 if merge_state.executed:
821 if merge_state.executed:
808 log.debug("Merge was successful, updating the pull request comments.")
822 log.debug("Merge was successful, updating the pull request comments.")
809 self._comment_and_close_pr(pull_request, user, merge_state)
823 self._comment_and_close_pr(pull_request, user, merge_state)
810
824
811 self._log_audit_action(
825 self._log_audit_action(
812 'repo.pull_request.merge',
826 'repo.pull_request.merge',
813 {'merge_state': merge_state.__dict__},
827 {'merge_state': merge_state.__dict__},
814 user, pull_request)
828 user, pull_request)
815
829
816 else:
830 else:
817 log.warn("Merge failed, not updating the pull request.")
831 log.warn("Merge failed, not updating the pull request.")
818 return merge_state
832 return merge_state
819
833
820 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
834 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
821 target_vcs = pull_request.target_repo.scm_instance()
835 target_vcs = pull_request.target_repo.scm_instance()
822 source_vcs = pull_request.source_repo.scm_instance()
836 source_vcs = pull_request.source_repo.scm_instance()
823
837
824 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
838 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
825 pr_id=pull_request.pull_request_id,
839 pr_id=pull_request.pull_request_id,
826 pr_title=pull_request.title,
840 pr_title=pull_request.title,
827 source_repo=source_vcs.name,
841 source_repo=source_vcs.name,
828 source_ref_name=pull_request.source_ref_parts.name,
842 source_ref_name=pull_request.source_ref_parts.name,
829 target_repo=target_vcs.name,
843 target_repo=target_vcs.name,
830 target_ref_name=pull_request.target_ref_parts.name,
844 target_ref_name=pull_request.target_ref_parts.name,
831 )
845 )
832
846
833 workspace_id = self._workspace_id(pull_request)
847 workspace_id = self._workspace_id(pull_request)
834 repo_id = pull_request.target_repo.repo_id
848 repo_id = pull_request.target_repo.repo_id
835 use_rebase = self._use_rebase_for_merging(pull_request)
849 use_rebase = self._use_rebase_for_merging(pull_request)
836 close_branch = self._close_branch_before_merging(pull_request)
850 close_branch = self._close_branch_before_merging(pull_request)
837 user_name = self._user_name_for_merging(pull_request, user)
851 user_name = self._user_name_for_merging(pull_request, user)
838
852
839 target_ref = self._refresh_reference(
853 target_ref = self._refresh_reference(
840 pull_request.target_ref_parts, target_vcs)
854 pull_request.target_ref_parts, target_vcs)
841
855
842 callback_daemon, extras = prepare_callback_daemon(
856 callback_daemon, extras = prepare_callback_daemon(
843 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
857 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
844 host=vcs_settings.HOOKS_HOST,
858 host=vcs_settings.HOOKS_HOST,
845 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
859 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
846
860
847 with callback_daemon:
861 with callback_daemon:
848 # TODO: johbo: Implement a clean way to run a config_override
862 # TODO: johbo: Implement a clean way to run a config_override
849 # for a single call.
863 # for a single call.
850 target_vcs.config.set(
864 target_vcs.config.set(
851 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
865 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
852
866
853 merge_state = target_vcs.merge(
867 merge_state = target_vcs.merge(
854 repo_id, workspace_id, target_ref, source_vcs,
868 repo_id, workspace_id, target_ref, source_vcs,
855 pull_request.source_ref_parts,
869 pull_request.source_ref_parts,
856 user_name=user_name, user_email=user.email,
870 user_name=user_name, user_email=user.email,
857 message=message, use_rebase=use_rebase,
871 message=message, use_rebase=use_rebase,
858 close_branch=close_branch)
872 close_branch=close_branch)
859 return merge_state
873 return merge_state
860
874
861 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
875 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
862 pull_request.merge_rev = merge_state.merge_ref.commit_id
876 pull_request.merge_rev = merge_state.merge_ref.commit_id
863 pull_request.updated_on = datetime.datetime.now()
877 pull_request.updated_on = datetime.datetime.now()
864 close_msg = close_msg or 'Pull request merged and closed'
878 close_msg = close_msg or 'Pull request merged and closed'
865
879
866 CommentsModel().create(
880 CommentsModel().create(
867 text=safe_unicode(close_msg),
881 text=safe_unicode(close_msg),
868 repo=pull_request.target_repo.repo_id,
882 repo=pull_request.target_repo.repo_id,
869 user=user.user_id,
883 user=user.user_id,
870 pull_request=pull_request.pull_request_id,
884 pull_request=pull_request.pull_request_id,
871 f_path=None,
885 f_path=None,
872 line_no=None,
886 line_no=None,
873 closing_pr=True
887 closing_pr=True
874 )
888 )
875
889
876 Session().add(pull_request)
890 Session().add(pull_request)
877 Session().flush()
891 Session().flush()
878 # TODO: paris: replace invalidation with less radical solution
892 # TODO: paris: replace invalidation with less radical solution
879 ScmModel().mark_for_invalidation(
893 ScmModel().mark_for_invalidation(
880 pull_request.target_repo.repo_name)
894 pull_request.target_repo.repo_name)
881 self.trigger_pull_request_hook(pull_request, user, 'merge')
895 self.trigger_pull_request_hook(pull_request, user, 'merge')
882
896
883 def has_valid_update_type(self, pull_request):
897 def has_valid_update_type(self, pull_request):
884 source_ref_type = pull_request.source_ref_parts.type
898 source_ref_type = pull_request.source_ref_parts.type
885 return source_ref_type in self.REF_TYPES
899 return source_ref_type in self.REF_TYPES
886
900
887 def get_flow_commits(self, pull_request):
901 def get_flow_commits(self, pull_request):
888
902
889 # source repo
903 # source repo
890 source_ref_name = pull_request.source_ref_parts.name
904 source_ref_name = pull_request.source_ref_parts.name
891 source_ref_type = pull_request.source_ref_parts.type
905 source_ref_type = pull_request.source_ref_parts.type
892 source_ref_id = pull_request.source_ref_parts.commit_id
906 source_ref_id = pull_request.source_ref_parts.commit_id
893 source_repo = pull_request.source_repo.scm_instance()
907 source_repo = pull_request.source_repo.scm_instance()
894
908
895 try:
909 try:
896 if source_ref_type in self.REF_TYPES:
910 if source_ref_type in self.REF_TYPES:
897 source_commit = source_repo.get_commit(source_ref_name)
911 source_commit = source_repo.get_commit(source_ref_name)
898 else:
912 else:
899 source_commit = source_repo.get_commit(source_ref_id)
913 source_commit = source_repo.get_commit(source_ref_id)
900 except CommitDoesNotExistError:
914 except CommitDoesNotExistError:
901 raise SourceRefMissing()
915 raise SourceRefMissing()
902
916
903 # target repo
917 # target repo
904 target_ref_name = pull_request.target_ref_parts.name
918 target_ref_name = pull_request.target_ref_parts.name
905 target_ref_type = pull_request.target_ref_parts.type
919 target_ref_type = pull_request.target_ref_parts.type
906 target_ref_id = pull_request.target_ref_parts.commit_id
920 target_ref_id = pull_request.target_ref_parts.commit_id
907 target_repo = pull_request.target_repo.scm_instance()
921 target_repo = pull_request.target_repo.scm_instance()
908
922
909 try:
923 try:
910 if target_ref_type in self.REF_TYPES:
924 if target_ref_type in self.REF_TYPES:
911 target_commit = target_repo.get_commit(target_ref_name)
925 target_commit = target_repo.get_commit(target_ref_name)
912 else:
926 else:
913 target_commit = target_repo.get_commit(target_ref_id)
927 target_commit = target_repo.get_commit(target_ref_id)
914 except CommitDoesNotExistError:
928 except CommitDoesNotExistError:
915 raise TargetRefMissing()
929 raise TargetRefMissing()
916
930
917 return source_commit, target_commit
931 return source_commit, target_commit
918
932
919 def update_commits(self, pull_request, updating_user):
933 def update_commits(self, pull_request, updating_user):
920 """
934 """
921 Get the updated list of commits for the pull request
935 Get the updated list of commits for the pull request
922 and return the new pull request version and the list
936 and return the new pull request version and the list
923 of commits processed by this update action
937 of commits processed by this update action
924
938
925 updating_user is the user_object who triggered the update
939 updating_user is the user_object who triggered the update
926 """
940 """
927 pull_request = self.__get_pull_request(pull_request)
941 pull_request = self.__get_pull_request(pull_request)
928 source_ref_type = pull_request.source_ref_parts.type
942 source_ref_type = pull_request.source_ref_parts.type
929 source_ref_name = pull_request.source_ref_parts.name
943 source_ref_name = pull_request.source_ref_parts.name
930 source_ref_id = pull_request.source_ref_parts.commit_id
944 source_ref_id = pull_request.source_ref_parts.commit_id
931
945
932 target_ref_type = pull_request.target_ref_parts.type
946 target_ref_type = pull_request.target_ref_parts.type
933 target_ref_name = pull_request.target_ref_parts.name
947 target_ref_name = pull_request.target_ref_parts.name
934 target_ref_id = pull_request.target_ref_parts.commit_id
948 target_ref_id = pull_request.target_ref_parts.commit_id
935
949
936 if not self.has_valid_update_type(pull_request):
950 if not self.has_valid_update_type(pull_request):
937 log.debug("Skipping update of pull request %s due to ref type: %s",
951 log.debug("Skipping update of pull request %s due to ref type: %s",
938 pull_request, source_ref_type)
952 pull_request, source_ref_type)
939 return UpdateResponse(
953 return UpdateResponse(
940 executed=False,
954 executed=False,
941 reason=UpdateFailureReason.WRONG_REF_TYPE,
955 reason=UpdateFailureReason.WRONG_REF_TYPE,
942 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
956 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
943 source_changed=False, target_changed=False)
957 source_changed=False, target_changed=False)
944
958
945 try:
959 try:
946 source_commit, target_commit = self.get_flow_commits(pull_request)
960 source_commit, target_commit = self.get_flow_commits(pull_request)
947 except SourceRefMissing:
961 except SourceRefMissing:
948 return UpdateResponse(
962 return UpdateResponse(
949 executed=False,
963 executed=False,
950 reason=UpdateFailureReason.MISSING_SOURCE_REF,
964 reason=UpdateFailureReason.MISSING_SOURCE_REF,
951 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
965 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
952 source_changed=False, target_changed=False)
966 source_changed=False, target_changed=False)
953 except TargetRefMissing:
967 except TargetRefMissing:
954 return UpdateResponse(
968 return UpdateResponse(
955 executed=False,
969 executed=False,
956 reason=UpdateFailureReason.MISSING_TARGET_REF,
970 reason=UpdateFailureReason.MISSING_TARGET_REF,
957 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
971 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
958 source_changed=False, target_changed=False)
972 source_changed=False, target_changed=False)
959
973
960 source_changed = source_ref_id != source_commit.raw_id
974 source_changed = source_ref_id != source_commit.raw_id
961 target_changed = target_ref_id != target_commit.raw_id
975 target_changed = target_ref_id != target_commit.raw_id
962
976
963 if not (source_changed or target_changed):
977 if not (source_changed or target_changed):
964 log.debug("Nothing changed in pull request %s", pull_request)
978 log.debug("Nothing changed in pull request %s", pull_request)
965 return UpdateResponse(
979 return UpdateResponse(
966 executed=False,
980 executed=False,
967 reason=UpdateFailureReason.NO_CHANGE,
981 reason=UpdateFailureReason.NO_CHANGE,
968 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
982 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
969 source_changed=target_changed, target_changed=source_changed)
983 source_changed=target_changed, target_changed=source_changed)
970
984
971 change_in_found = 'target repo' if target_changed else 'source repo'
985 change_in_found = 'target repo' if target_changed else 'source repo'
972 log.debug('Updating pull request because of change in %s detected',
986 log.debug('Updating pull request because of change in %s detected',
973 change_in_found)
987 change_in_found)
974
988
975 # Finally there is a need for an update, in case of source change
989 # Finally there is a need for an update, in case of source change
976 # we create a new version, else just an update
990 # we create a new version, else just an update
977 if source_changed:
991 if source_changed:
978 pull_request_version = self._create_version_from_snapshot(pull_request)
992 pull_request_version = self._create_version_from_snapshot(pull_request)
979 self._link_comments_to_version(pull_request_version)
993 self._link_comments_to_version(pull_request_version)
980 else:
994 else:
981 try:
995 try:
982 ver = pull_request.versions[-1]
996 ver = pull_request.versions[-1]
983 except IndexError:
997 except IndexError:
984 ver = None
998 ver = None
985
999
986 pull_request.pull_request_version_id = \
1000 pull_request.pull_request_version_id = \
987 ver.pull_request_version_id if ver else None
1001 ver.pull_request_version_id if ver else None
988 pull_request_version = pull_request
1002 pull_request_version = pull_request
989
1003
990 source_repo = pull_request.source_repo.scm_instance()
1004 source_repo = pull_request.source_repo.scm_instance()
991 target_repo = pull_request.target_repo.scm_instance()
1005 target_repo = pull_request.target_repo.scm_instance()
992
1006
993 # re-compute commit ids
1007 # re-compute commit ids
994 old_commit_ids = pull_request.revisions
1008 old_commit_ids = pull_request.revisions
995 pre_load = ["author", "date", "message", "branch"]
1009 pre_load = ["author", "date", "message", "branch"]
996 commit_ranges = target_repo.compare(
1010 commit_ranges = target_repo.compare(
997 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1011 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
998 pre_load=pre_load)
1012 pre_load=pre_load)
999
1013
1000 target_ref = target_commit.raw_id
1014 target_ref = target_commit.raw_id
1001 source_ref = source_commit.raw_id
1015 source_ref = source_commit.raw_id
1002 ancestor_commit_id = target_repo.get_common_ancestor(
1016 ancestor_commit_id = target_repo.get_common_ancestor(
1003 target_ref, source_ref, source_repo)
1017 target_ref, source_ref, source_repo)
1004
1018
1005 if not ancestor_commit_id:
1019 if not ancestor_commit_id:
1006 raise ValueError(
1020 raise ValueError(
1007 'cannot calculate diff info without a common ancestor. '
1021 'cannot calculate diff info without a common ancestor. '
1008 'Make sure both repositories are related, and have a common forking commit.')
1022 'Make sure both repositories are related, and have a common forking commit.')
1009
1023
1010 pull_request.common_ancestor_id = ancestor_commit_id
1024 pull_request.common_ancestor_id = ancestor_commit_id
1011
1025
1012 pull_request.source_ref = '%s:%s:%s' % (
1026 pull_request.source_ref = '%s:%s:%s' % (
1013 source_ref_type, source_ref_name, source_commit.raw_id)
1027 source_ref_type, source_ref_name, source_commit.raw_id)
1014 pull_request.target_ref = '%s:%s:%s' % (
1028 pull_request.target_ref = '%s:%s:%s' % (
1015 target_ref_type, target_ref_name, ancestor_commit_id)
1029 target_ref_type, target_ref_name, ancestor_commit_id)
1016
1030
1017 pull_request.revisions = [
1031 pull_request.revisions = [
1018 commit.raw_id for commit in reversed(commit_ranges)]
1032 commit.raw_id for commit in reversed(commit_ranges)]
1019 pull_request.updated_on = datetime.datetime.now()
1033 pull_request.updated_on = datetime.datetime.now()
1020 Session().add(pull_request)
1034 Session().add(pull_request)
1021 new_commit_ids = pull_request.revisions
1035 new_commit_ids = pull_request.revisions
1022
1036
1023 old_diff_data, new_diff_data = self._generate_update_diffs(
1037 old_diff_data, new_diff_data = self._generate_update_diffs(
1024 pull_request, pull_request_version)
1038 pull_request, pull_request_version)
1025
1039
1026 # calculate commit and file changes
1040 # calculate commit and file changes
1027 commit_changes = self._calculate_commit_id_changes(
1041 commit_changes = self._calculate_commit_id_changes(
1028 old_commit_ids, new_commit_ids)
1042 old_commit_ids, new_commit_ids)
1029 file_changes = self._calculate_file_changes(
1043 file_changes = self._calculate_file_changes(
1030 old_diff_data, new_diff_data)
1044 old_diff_data, new_diff_data)
1031
1045
1032 # set comments as outdated if DIFFS changed
1046 # set comments as outdated if DIFFS changed
1033 CommentsModel().outdate_comments(
1047 CommentsModel().outdate_comments(
1034 pull_request, old_diff_data=old_diff_data,
1048 pull_request, old_diff_data=old_diff_data,
1035 new_diff_data=new_diff_data)
1049 new_diff_data=new_diff_data)
1036
1050
1037 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1051 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1038 file_node_changes = (
1052 file_node_changes = (
1039 file_changes.added or file_changes.modified or file_changes.removed)
1053 file_changes.added or file_changes.modified or file_changes.removed)
1040 pr_has_changes = valid_commit_changes or file_node_changes
1054 pr_has_changes = valid_commit_changes or file_node_changes
1041
1055
1042 # Add an automatic comment to the pull request, in case
1056 # Add an automatic comment to the pull request, in case
1043 # anything has changed
1057 # anything has changed
1044 if pr_has_changes:
1058 if pr_has_changes:
1045 update_comment = CommentsModel().create(
1059 update_comment = CommentsModel().create(
1046 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1060 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1047 repo=pull_request.target_repo,
1061 repo=pull_request.target_repo,
1048 user=pull_request.author,
1062 user=pull_request.author,
1049 pull_request=pull_request,
1063 pull_request=pull_request,
1050 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1064 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1051
1065
1052 # Update status to "Under Review" for added commits
1066 # Update status to "Under Review" for added commits
1053 for commit_id in commit_changes.added:
1067 for commit_id in commit_changes.added:
1054 ChangesetStatusModel().set_status(
1068 ChangesetStatusModel().set_status(
1055 repo=pull_request.source_repo,
1069 repo=pull_request.source_repo,
1056 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1070 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1057 comment=update_comment,
1071 comment=update_comment,
1058 user=pull_request.author,
1072 user=pull_request.author,
1059 pull_request=pull_request,
1073 pull_request=pull_request,
1060 revision=commit_id)
1074 revision=commit_id)
1061
1075
1062 # send update email to users
1076 # send update email to users
1063 try:
1077 try:
1064 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1078 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1065 ancestor_commit_id=ancestor_commit_id,
1079 ancestor_commit_id=ancestor_commit_id,
1066 commit_changes=commit_changes,
1080 commit_changes=commit_changes,
1067 file_changes=file_changes)
1081 file_changes=file_changes)
1068 except Exception:
1082 except Exception:
1069 log.exception('Failed to send email notification to users')
1083 log.exception('Failed to send email notification to users')
1070
1084
1071 log.debug(
1085 log.debug(
1072 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1086 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1073 'removed_ids: %s', pull_request.pull_request_id,
1087 'removed_ids: %s', pull_request.pull_request_id,
1074 commit_changes.added, commit_changes.common, commit_changes.removed)
1088 commit_changes.added, commit_changes.common, commit_changes.removed)
1075 log.debug(
1089 log.debug(
1076 'Updated pull request with the following file changes: %s',
1090 'Updated pull request with the following file changes: %s',
1077 file_changes)
1091 file_changes)
1078
1092
1079 log.info(
1093 log.info(
1080 "Updated pull request %s from commit %s to commit %s, "
1094 "Updated pull request %s from commit %s to commit %s, "
1081 "stored new version %s of this pull request.",
1095 "stored new version %s of this pull request.",
1082 pull_request.pull_request_id, source_ref_id,
1096 pull_request.pull_request_id, source_ref_id,
1083 pull_request.source_ref_parts.commit_id,
1097 pull_request.source_ref_parts.commit_id,
1084 pull_request_version.pull_request_version_id)
1098 pull_request_version.pull_request_version_id)
1085 Session().commit()
1099 Session().commit()
1086 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1100 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1087
1101
1088 return UpdateResponse(
1102 return UpdateResponse(
1089 executed=True, reason=UpdateFailureReason.NONE,
1103 executed=True, reason=UpdateFailureReason.NONE,
1090 old=pull_request, new=pull_request_version,
1104 old=pull_request, new=pull_request_version,
1091 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1105 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1092 source_changed=source_changed, target_changed=target_changed)
1106 source_changed=source_changed, target_changed=target_changed)
1093
1107
1094 def _create_version_from_snapshot(self, pull_request):
1108 def _create_version_from_snapshot(self, pull_request):
1095 version = PullRequestVersion()
1109 version = PullRequestVersion()
1096 version.title = pull_request.title
1110 version.title = pull_request.title
1097 version.description = pull_request.description
1111 version.description = pull_request.description
1098 version.status = pull_request.status
1112 version.status = pull_request.status
1099 version.pull_request_state = pull_request.pull_request_state
1113 version.pull_request_state = pull_request.pull_request_state
1100 version.created_on = datetime.datetime.now()
1114 version.created_on = datetime.datetime.now()
1101 version.updated_on = pull_request.updated_on
1115 version.updated_on = pull_request.updated_on
1102 version.user_id = pull_request.user_id
1116 version.user_id = pull_request.user_id
1103 version.source_repo = pull_request.source_repo
1117 version.source_repo = pull_request.source_repo
1104 version.source_ref = pull_request.source_ref
1118 version.source_ref = pull_request.source_ref
1105 version.target_repo = pull_request.target_repo
1119 version.target_repo = pull_request.target_repo
1106 version.target_ref = pull_request.target_ref
1120 version.target_ref = pull_request.target_ref
1107
1121
1108 version._last_merge_source_rev = pull_request._last_merge_source_rev
1122 version._last_merge_source_rev = pull_request._last_merge_source_rev
1109 version._last_merge_target_rev = pull_request._last_merge_target_rev
1123 version._last_merge_target_rev = pull_request._last_merge_target_rev
1110 version.last_merge_status = pull_request.last_merge_status
1124 version.last_merge_status = pull_request.last_merge_status
1111 version.last_merge_metadata = pull_request.last_merge_metadata
1125 version.last_merge_metadata = pull_request.last_merge_metadata
1112 version.shadow_merge_ref = pull_request.shadow_merge_ref
1126 version.shadow_merge_ref = pull_request.shadow_merge_ref
1113 version.merge_rev = pull_request.merge_rev
1127 version.merge_rev = pull_request.merge_rev
1114 version.reviewer_data = pull_request.reviewer_data
1128 version.reviewer_data = pull_request.reviewer_data
1115
1129
1116 version.revisions = pull_request.revisions
1130 version.revisions = pull_request.revisions
1117 version.common_ancestor_id = pull_request.common_ancestor_id
1131 version.common_ancestor_id = pull_request.common_ancestor_id
1118 version.pull_request = pull_request
1132 version.pull_request = pull_request
1119 Session().add(version)
1133 Session().add(version)
1120 Session().flush()
1134 Session().flush()
1121
1135
1122 return version
1136 return version
1123
1137
1124 def _generate_update_diffs(self, pull_request, pull_request_version):
1138 def _generate_update_diffs(self, pull_request, pull_request_version):
1125
1139
1126 diff_context = (
1140 diff_context = (
1127 self.DIFF_CONTEXT +
1141 self.DIFF_CONTEXT +
1128 CommentsModel.needed_extra_diff_context())
1142 CommentsModel.needed_extra_diff_context())
1129 hide_whitespace_changes = False
1143 hide_whitespace_changes = False
1130 source_repo = pull_request_version.source_repo
1144 source_repo = pull_request_version.source_repo
1131 source_ref_id = pull_request_version.source_ref_parts.commit_id
1145 source_ref_id = pull_request_version.source_ref_parts.commit_id
1132 target_ref_id = pull_request_version.target_ref_parts.commit_id
1146 target_ref_id = pull_request_version.target_ref_parts.commit_id
1133 old_diff = self._get_diff_from_pr_or_version(
1147 old_diff = self._get_diff_from_pr_or_version(
1134 source_repo, source_ref_id, target_ref_id,
1148 source_repo, source_ref_id, target_ref_id,
1135 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1149 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1136
1150
1137 source_repo = pull_request.source_repo
1151 source_repo = pull_request.source_repo
1138 source_ref_id = pull_request.source_ref_parts.commit_id
1152 source_ref_id = pull_request.source_ref_parts.commit_id
1139 target_ref_id = pull_request.target_ref_parts.commit_id
1153 target_ref_id = pull_request.target_ref_parts.commit_id
1140
1154
1141 new_diff = self._get_diff_from_pr_or_version(
1155 new_diff = self._get_diff_from_pr_or_version(
1142 source_repo, source_ref_id, target_ref_id,
1156 source_repo, source_ref_id, target_ref_id,
1143 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1157 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1144
1158
1145 old_diff_data = diffs.DiffProcessor(old_diff)
1159 old_diff_data = diffs.DiffProcessor(old_diff)
1146 old_diff_data.prepare()
1160 old_diff_data.prepare()
1147 new_diff_data = diffs.DiffProcessor(new_diff)
1161 new_diff_data = diffs.DiffProcessor(new_diff)
1148 new_diff_data.prepare()
1162 new_diff_data.prepare()
1149
1163
1150 return old_diff_data, new_diff_data
1164 return old_diff_data, new_diff_data
1151
1165
1152 def _link_comments_to_version(self, pull_request_version):
1166 def _link_comments_to_version(self, pull_request_version):
1153 """
1167 """
1154 Link all unlinked comments of this pull request to the given version.
1168 Link all unlinked comments of this pull request to the given version.
1155
1169
1156 :param pull_request_version: The `PullRequestVersion` to which
1170 :param pull_request_version: The `PullRequestVersion` to which
1157 the comments shall be linked.
1171 the comments shall be linked.
1158
1172
1159 """
1173 """
1160 pull_request = pull_request_version.pull_request
1174 pull_request = pull_request_version.pull_request
1161 comments = ChangesetComment.query()\
1175 comments = ChangesetComment.query()\
1162 .filter(
1176 .filter(
1163 # TODO: johbo: Should we query for the repo at all here?
1177 # TODO: johbo: Should we query for the repo at all here?
1164 # Pending decision on how comments of PRs are to be related
1178 # Pending decision on how comments of PRs are to be related
1165 # to either the source repo, the target repo or no repo at all.
1179 # to either the source repo, the target repo or no repo at all.
1166 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1180 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1167 ChangesetComment.pull_request == pull_request,
1181 ChangesetComment.pull_request == pull_request,
1168 ChangesetComment.pull_request_version == None)\
1182 ChangesetComment.pull_request_version == None)\
1169 .order_by(ChangesetComment.comment_id.asc())
1183 .order_by(ChangesetComment.comment_id.asc())
1170
1184
1171 # TODO: johbo: Find out why this breaks if it is done in a bulk
1185 # TODO: johbo: Find out why this breaks if it is done in a bulk
1172 # operation.
1186 # operation.
1173 for comment in comments:
1187 for comment in comments:
1174 comment.pull_request_version_id = (
1188 comment.pull_request_version_id = (
1175 pull_request_version.pull_request_version_id)
1189 pull_request_version.pull_request_version_id)
1176 Session().add(comment)
1190 Session().add(comment)
1177
1191
1178 def _calculate_commit_id_changes(self, old_ids, new_ids):
1192 def _calculate_commit_id_changes(self, old_ids, new_ids):
1179 added = [x for x in new_ids if x not in old_ids]
1193 added = [x for x in new_ids if x not in old_ids]
1180 common = [x for x in new_ids if x in old_ids]
1194 common = [x for x in new_ids if x in old_ids]
1181 removed = [x for x in old_ids if x not in new_ids]
1195 removed = [x for x in old_ids if x not in new_ids]
1182 total = new_ids
1196 total = new_ids
1183 return ChangeTuple(added, common, removed, total)
1197 return ChangeTuple(added, common, removed, total)
1184
1198
1185 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1199 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1186
1200
1187 old_files = OrderedDict()
1201 old_files = OrderedDict()
1188 for diff_data in old_diff_data.parsed_diff:
1202 for diff_data in old_diff_data.parsed_diff:
1189 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1203 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1190
1204
1191 added_files = []
1205 added_files = []
1192 modified_files = []
1206 modified_files = []
1193 removed_files = []
1207 removed_files = []
1194 for diff_data in new_diff_data.parsed_diff:
1208 for diff_data in new_diff_data.parsed_diff:
1195 new_filename = diff_data['filename']
1209 new_filename = diff_data['filename']
1196 new_hash = md5_safe(diff_data['raw_diff'])
1210 new_hash = md5_safe(diff_data['raw_diff'])
1197
1211
1198 old_hash = old_files.get(new_filename)
1212 old_hash = old_files.get(new_filename)
1199 if not old_hash:
1213 if not old_hash:
1200 # file is not present in old diff, we have to figure out from parsed diff
1214 # file is not present in old diff, we have to figure out from parsed diff
1201 # operation ADD/REMOVE
1215 # operation ADD/REMOVE
1202 operations_dict = diff_data['stats']['ops']
1216 operations_dict = diff_data['stats']['ops']
1203 if diffs.DEL_FILENODE in operations_dict:
1217 if diffs.DEL_FILENODE in operations_dict:
1204 removed_files.append(new_filename)
1218 removed_files.append(new_filename)
1205 else:
1219 else:
1206 added_files.append(new_filename)
1220 added_files.append(new_filename)
1207 else:
1221 else:
1208 if new_hash != old_hash:
1222 if new_hash != old_hash:
1209 modified_files.append(new_filename)
1223 modified_files.append(new_filename)
1210 # now remove a file from old, since we have seen it already
1224 # now remove a file from old, since we have seen it already
1211 del old_files[new_filename]
1225 del old_files[new_filename]
1212
1226
1213 # removed files is when there are present in old, but not in NEW,
1227 # removed files is when there are present in old, but not in NEW,
1214 # since we remove old files that are present in new diff, left-overs
1228 # since we remove old files that are present in new diff, left-overs
1215 # if any should be the removed files
1229 # if any should be the removed files
1216 removed_files.extend(old_files.keys())
1230 removed_files.extend(old_files.keys())
1217
1231
1218 return FileChangeTuple(added_files, modified_files, removed_files)
1232 return FileChangeTuple(added_files, modified_files, removed_files)
1219
1233
1220 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1234 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1221 """
1235 """
1222 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1236 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1223 so it's always looking the same disregarding on which default
1237 so it's always looking the same disregarding on which default
1224 renderer system is using.
1238 renderer system is using.
1225
1239
1226 :param ancestor_commit_id: ancestor raw_id
1240 :param ancestor_commit_id: ancestor raw_id
1227 :param changes: changes named tuple
1241 :param changes: changes named tuple
1228 :param file_changes: file changes named tuple
1242 :param file_changes: file changes named tuple
1229
1243
1230 """
1244 """
1231 new_status = ChangesetStatus.get_status_lbl(
1245 new_status = ChangesetStatus.get_status_lbl(
1232 ChangesetStatus.STATUS_UNDER_REVIEW)
1246 ChangesetStatus.STATUS_UNDER_REVIEW)
1233
1247
1234 changed_files = (
1248 changed_files = (
1235 file_changes.added + file_changes.modified + file_changes.removed)
1249 file_changes.added + file_changes.modified + file_changes.removed)
1236
1250
1237 params = {
1251 params = {
1238 'under_review_label': new_status,
1252 'under_review_label': new_status,
1239 'added_commits': changes.added,
1253 'added_commits': changes.added,
1240 'removed_commits': changes.removed,
1254 'removed_commits': changes.removed,
1241 'changed_files': changed_files,
1255 'changed_files': changed_files,
1242 'added_files': file_changes.added,
1256 'added_files': file_changes.added,
1243 'modified_files': file_changes.modified,
1257 'modified_files': file_changes.modified,
1244 'removed_files': file_changes.removed,
1258 'removed_files': file_changes.removed,
1245 'ancestor_commit_id': ancestor_commit_id
1259 'ancestor_commit_id': ancestor_commit_id
1246 }
1260 }
1247 renderer = RstTemplateRenderer()
1261 renderer = RstTemplateRenderer()
1248 return renderer.render('pull_request_update.mako', **params)
1262 return renderer.render('pull_request_update.mako', **params)
1249
1263
1250 def edit(self, pull_request, title, description, description_renderer, user):
1264 def edit(self, pull_request, title, description, description_renderer, user):
1251 pull_request = self.__get_pull_request(pull_request)
1265 pull_request = self.__get_pull_request(pull_request)
1252 old_data = pull_request.get_api_data(with_merge_state=False)
1266 old_data = pull_request.get_api_data(with_merge_state=False)
1253 if pull_request.is_closed():
1267 if pull_request.is_closed():
1254 raise ValueError('This pull request is closed')
1268 raise ValueError('This pull request is closed')
1255 if title:
1269 if title:
1256 pull_request.title = title
1270 pull_request.title = title
1257 pull_request.description = description
1271 pull_request.description = description
1258 pull_request.updated_on = datetime.datetime.now()
1272 pull_request.updated_on = datetime.datetime.now()
1259 pull_request.description_renderer = description_renderer
1273 pull_request.description_renderer = description_renderer
1260 Session().add(pull_request)
1274 Session().add(pull_request)
1261 self._log_audit_action(
1275 self._log_audit_action(
1262 'repo.pull_request.edit', {'old_data': old_data},
1276 'repo.pull_request.edit', {'old_data': old_data},
1263 user, pull_request)
1277 user, pull_request)
1264
1278
1265 def update_reviewers(self, pull_request, reviewer_data, user):
1279 def update_reviewers(self, pull_request, reviewer_data, user):
1266 """
1280 """
1267 Update the reviewers in the pull request
1281 Update the reviewers in the pull request
1268
1282
1269 :param pull_request: the pr to update
1283 :param pull_request: the pr to update
1270 :param reviewer_data: list of tuples
1284 :param reviewer_data: list of tuples
1271 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1285 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1272 :param user: current use who triggers this action
1286 :param user: current use who triggers this action
1273 """
1287 """
1274
1288
1275 pull_request = self.__get_pull_request(pull_request)
1289 pull_request = self.__get_pull_request(pull_request)
1276 if pull_request.is_closed():
1290 if pull_request.is_closed():
1277 raise ValueError('This pull request is closed')
1291 raise ValueError('This pull request is closed')
1278
1292
1279 reviewers = {}
1293 reviewers = {}
1280 for user_id, reasons, mandatory, role, rules in reviewer_data:
1294 for user_id, reasons, mandatory, role, rules in reviewer_data:
1281 if isinstance(user_id, (int, compat.string_types)):
1295 if isinstance(user_id, (int, compat.string_types)):
1282 user_id = self._get_user(user_id).user_id
1296 user_id = self._get_user(user_id).user_id
1283 reviewers[user_id] = {
1297 reviewers[user_id] = {
1284 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1298 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1285
1299
1286 reviewers_ids = set(reviewers.keys())
1300 reviewers_ids = set(reviewers.keys())
1287 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1301 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1288 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1302 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1289
1303
1290 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1304 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1291
1305
1292 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1306 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1293 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1307 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1294
1308
1295 log.debug("Adding %s reviewers", ids_to_add)
1309 log.debug("Adding %s reviewers", ids_to_add)
1296 log.debug("Removing %s reviewers", ids_to_remove)
1310 log.debug("Removing %s reviewers", ids_to_remove)
1297 changed = False
1311 changed = False
1298 added_audit_reviewers = []
1312 added_audit_reviewers = []
1299 removed_audit_reviewers = []
1313 removed_audit_reviewers = []
1300
1314
1301 for uid in ids_to_add:
1315 for uid in ids_to_add:
1302 changed = True
1316 changed = True
1303 _usr = self._get_user(uid)
1317 _usr = self._get_user(uid)
1304 reviewer = PullRequestReviewers()
1318 reviewer = PullRequestReviewers()
1305 reviewer.user = _usr
1319 reviewer.user = _usr
1306 reviewer.pull_request = pull_request
1320 reviewer.pull_request = pull_request
1307 reviewer.reasons = reviewers[uid]['reasons']
1321 reviewer.reasons = reviewers[uid]['reasons']
1308 # NOTE(marcink): mandatory shouldn't be changed now
1322 # NOTE(marcink): mandatory shouldn't be changed now
1309 # reviewer.mandatory = reviewers[uid]['reasons']
1323 # reviewer.mandatory = reviewers[uid]['reasons']
1310 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1324 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1311 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1325 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1312 Session().add(reviewer)
1326 Session().add(reviewer)
1313 added_audit_reviewers.append(reviewer.get_dict())
1327 added_audit_reviewers.append(reviewer.get_dict())
1314
1328
1315 for uid in ids_to_remove:
1329 for uid in ids_to_remove:
1316 changed = True
1330 changed = True
1317 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1331 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1318 # This is an edge case that handles previous state of having the same reviewer twice.
1332 # This is an edge case that handles previous state of having the same reviewer twice.
1319 # this CAN happen due to the lack of DB checks
1333 # this CAN happen due to the lack of DB checks
1320 reviewers = PullRequestReviewers.query()\
1334 reviewers = PullRequestReviewers.query()\
1321 .filter(PullRequestReviewers.user_id == uid,
1335 .filter(PullRequestReviewers.user_id == uid,
1322 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1336 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1323 PullRequestReviewers.pull_request == pull_request)\
1337 PullRequestReviewers.pull_request == pull_request)\
1324 .all()
1338 .all()
1325
1339
1326 for obj in reviewers:
1340 for obj in reviewers:
1327 added_audit_reviewers.append(obj.get_dict())
1341 added_audit_reviewers.append(obj.get_dict())
1328 Session().delete(obj)
1342 Session().delete(obj)
1329
1343
1330 if changed:
1344 if changed:
1331 Session().expire_all()
1345 Session().expire_all()
1332 pull_request.updated_on = datetime.datetime.now()
1346 pull_request.updated_on = datetime.datetime.now()
1333 Session().add(pull_request)
1347 Session().add(pull_request)
1334
1348
1335 # finally store audit logs
1349 # finally store audit logs
1336 for user_data in added_audit_reviewers:
1350 for user_data in added_audit_reviewers:
1337 self._log_audit_action(
1351 self._log_audit_action(
1338 'repo.pull_request.reviewer.add', {'data': user_data},
1352 'repo.pull_request.reviewer.add', {'data': user_data},
1339 user, pull_request)
1353 user, pull_request)
1340 for user_data in removed_audit_reviewers:
1354 for user_data in removed_audit_reviewers:
1341 self._log_audit_action(
1355 self._log_audit_action(
1342 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1356 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1343 user, pull_request)
1357 user, pull_request)
1344
1358
1345 self.notify_reviewers(pull_request, ids_to_add, user)
1359 self.notify_reviewers(pull_request, ids_to_add, user)
1346 return ids_to_add, ids_to_remove
1360 return ids_to_add, ids_to_remove
1347
1361
1348 def update_observers(self, pull_request, observer_data, user):
1362 def update_observers(self, pull_request, observer_data, user):
1349 """
1363 """
1350 Update the observers in the pull request
1364 Update the observers in the pull request
1351
1365
1352 :param pull_request: the pr to update
1366 :param pull_request: the pr to update
1353 :param observer_data: list of tuples
1367 :param observer_data: list of tuples
1354 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1368 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1355 :param user: current use who triggers this action
1369 :param user: current use who triggers this action
1356 """
1370 """
1357 pull_request = self.__get_pull_request(pull_request)
1371 pull_request = self.__get_pull_request(pull_request)
1358 if pull_request.is_closed():
1372 if pull_request.is_closed():
1359 raise ValueError('This pull request is closed')
1373 raise ValueError('This pull request is closed')
1360
1374
1361 observers = {}
1375 observers = {}
1362 for user_id, reasons, mandatory, role, rules in observer_data:
1376 for user_id, reasons, mandatory, role, rules in observer_data:
1363 if isinstance(user_id, (int, compat.string_types)):
1377 if isinstance(user_id, (int, compat.string_types)):
1364 user_id = self._get_user(user_id).user_id
1378 user_id = self._get_user(user_id).user_id
1365 observers[user_id] = {
1379 observers[user_id] = {
1366 'reasons': reasons, 'observers': mandatory, 'role': role}
1380 'reasons': reasons, 'observers': mandatory, 'role': role}
1367
1381
1368 observers_ids = set(observers.keys())
1382 observers_ids = set(observers.keys())
1369 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1383 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1370 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1384 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1371
1385
1372 current_observers_ids = set([x.user.user_id for x in current_observers])
1386 current_observers_ids = set([x.user.user_id for x in current_observers])
1373
1387
1374 ids_to_add = observers_ids.difference(current_observers_ids)
1388 ids_to_add = observers_ids.difference(current_observers_ids)
1375 ids_to_remove = current_observers_ids.difference(observers_ids)
1389 ids_to_remove = current_observers_ids.difference(observers_ids)
1376
1390
1377 log.debug("Adding %s observer", ids_to_add)
1391 log.debug("Adding %s observer", ids_to_add)
1378 log.debug("Removing %s observer", ids_to_remove)
1392 log.debug("Removing %s observer", ids_to_remove)
1379 changed = False
1393 changed = False
1380 added_audit_observers = []
1394 added_audit_observers = []
1381 removed_audit_observers = []
1395 removed_audit_observers = []
1382
1396
1383 for uid in ids_to_add:
1397 for uid in ids_to_add:
1384 changed = True
1398 changed = True
1385 _usr = self._get_user(uid)
1399 _usr = self._get_user(uid)
1386 observer = PullRequestReviewers()
1400 observer = PullRequestReviewers()
1387 observer.user = _usr
1401 observer.user = _usr
1388 observer.pull_request = pull_request
1402 observer.pull_request = pull_request
1389 observer.reasons = observers[uid]['reasons']
1403 observer.reasons = observers[uid]['reasons']
1390 # NOTE(marcink): mandatory shouldn't be changed now
1404 # NOTE(marcink): mandatory shouldn't be changed now
1391 # observer.mandatory = observer[uid]['reasons']
1405 # observer.mandatory = observer[uid]['reasons']
1392
1406
1393 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1407 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1394 observer.role = PullRequestReviewers.ROLE_OBSERVER
1408 observer.role = PullRequestReviewers.ROLE_OBSERVER
1395 Session().add(observer)
1409 Session().add(observer)
1396 added_audit_observers.append(observer.get_dict())
1410 added_audit_observers.append(observer.get_dict())
1397
1411
1398 for uid in ids_to_remove:
1412 for uid in ids_to_remove:
1399 changed = True
1413 changed = True
1400 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1414 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1401 # This is an edge case that handles previous state of having the same reviewer twice.
1415 # This is an edge case that handles previous state of having the same reviewer twice.
1402 # this CAN happen due to the lack of DB checks
1416 # this CAN happen due to the lack of DB checks
1403 observers = PullRequestReviewers.query()\
1417 observers = PullRequestReviewers.query()\
1404 .filter(PullRequestReviewers.user_id == uid,
1418 .filter(PullRequestReviewers.user_id == uid,
1405 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1419 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1406 PullRequestReviewers.pull_request == pull_request)\
1420 PullRequestReviewers.pull_request == pull_request)\
1407 .all()
1421 .all()
1408
1422
1409 for obj in observers:
1423 for obj in observers:
1410 added_audit_observers.append(obj.get_dict())
1424 added_audit_observers.append(obj.get_dict())
1411 Session().delete(obj)
1425 Session().delete(obj)
1412
1426
1413 if changed:
1427 if changed:
1414 Session().expire_all()
1428 Session().expire_all()
1415 pull_request.updated_on = datetime.datetime.now()
1429 pull_request.updated_on = datetime.datetime.now()
1416 Session().add(pull_request)
1430 Session().add(pull_request)
1417
1431
1418 # finally store audit logs
1432 # finally store audit logs
1419 for user_data in added_audit_observers:
1433 for user_data in added_audit_observers:
1420 self._log_audit_action(
1434 self._log_audit_action(
1421 'repo.pull_request.observer.add', {'data': user_data},
1435 'repo.pull_request.observer.add', {'data': user_data},
1422 user, pull_request)
1436 user, pull_request)
1423 for user_data in removed_audit_observers:
1437 for user_data in removed_audit_observers:
1424 self._log_audit_action(
1438 self._log_audit_action(
1425 'repo.pull_request.observer.delete', {'old_data': user_data},
1439 'repo.pull_request.observer.delete', {'old_data': user_data},
1426 user, pull_request)
1440 user, pull_request)
1427
1441
1428 self.notify_observers(pull_request, ids_to_add, user)
1442 self.notify_observers(pull_request, ids_to_add, user)
1429 return ids_to_add, ids_to_remove
1443 return ids_to_add, ids_to_remove
1430
1444
1431 def get_url(self, pull_request, request=None, permalink=False):
1445 def get_url(self, pull_request, request=None, permalink=False):
1432 if not request:
1446 if not request:
1433 request = get_current_request()
1447 request = get_current_request()
1434
1448
1435 if permalink:
1449 if permalink:
1436 return request.route_url(
1450 return request.route_url(
1437 'pull_requests_global',
1451 'pull_requests_global',
1438 pull_request_id=pull_request.pull_request_id,)
1452 pull_request_id=pull_request.pull_request_id,)
1439 else:
1453 else:
1440 return request.route_url('pullrequest_show',
1454 return request.route_url('pullrequest_show',
1441 repo_name=safe_str(pull_request.target_repo.repo_name),
1455 repo_name=safe_str(pull_request.target_repo.repo_name),
1442 pull_request_id=pull_request.pull_request_id,)
1456 pull_request_id=pull_request.pull_request_id,)
1443
1457
1444 def get_shadow_clone_url(self, pull_request, request=None):
1458 def get_shadow_clone_url(self, pull_request, request=None):
1445 """
1459 """
1446 Returns qualified url pointing to the shadow repository. If this pull
1460 Returns qualified url pointing to the shadow repository. If this pull
1447 request is closed there is no shadow repository and ``None`` will be
1461 request is closed there is no shadow repository and ``None`` will be
1448 returned.
1462 returned.
1449 """
1463 """
1450 if pull_request.is_closed():
1464 if pull_request.is_closed():
1451 return None
1465 return None
1452 else:
1466 else:
1453 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1467 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1454 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1468 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1455
1469
1456 def _notify_reviewers(self, pull_request, user_ids, role, user):
1470 def _notify_reviewers(self, pull_request, user_ids, role, user):
1457 # notification to reviewers/observers
1471 # notification to reviewers/observers
1458 if not user_ids:
1472 if not user_ids:
1459 return
1473 return
1460
1474
1461 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1475 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1462
1476
1463 pull_request_obj = pull_request
1477 pull_request_obj = pull_request
1464 # get the current participants of this pull request
1478 # get the current participants of this pull request
1465 recipients = user_ids
1479 recipients = user_ids
1466 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1480 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1467
1481
1468 pr_source_repo = pull_request_obj.source_repo
1482 pr_source_repo = pull_request_obj.source_repo
1469 pr_target_repo = pull_request_obj.target_repo
1483 pr_target_repo = pull_request_obj.target_repo
1470
1484
1471 pr_url = h.route_url('pullrequest_show',
1485 pr_url = h.route_url('pullrequest_show',
1472 repo_name=pr_target_repo.repo_name,
1486 repo_name=pr_target_repo.repo_name,
1473 pull_request_id=pull_request_obj.pull_request_id,)
1487 pull_request_id=pull_request_obj.pull_request_id,)
1474
1488
1475 # set some variables for email notification
1489 # set some variables for email notification
1476 pr_target_repo_url = h.route_url(
1490 pr_target_repo_url = h.route_url(
1477 'repo_summary', repo_name=pr_target_repo.repo_name)
1491 'repo_summary', repo_name=pr_target_repo.repo_name)
1478
1492
1479 pr_source_repo_url = h.route_url(
1493 pr_source_repo_url = h.route_url(
1480 'repo_summary', repo_name=pr_source_repo.repo_name)
1494 'repo_summary', repo_name=pr_source_repo.repo_name)
1481
1495
1482 # pull request specifics
1496 # pull request specifics
1483 pull_request_commits = [
1497 pull_request_commits = [
1484 (x.raw_id, x.message)
1498 (x.raw_id, x.message)
1485 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1499 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1486
1500
1487 current_rhodecode_user = user
1501 current_rhodecode_user = user
1488 kwargs = {
1502 kwargs = {
1489 'user': current_rhodecode_user,
1503 'user': current_rhodecode_user,
1490 'pull_request_author': pull_request.author,
1504 'pull_request_author': pull_request.author,
1491 'pull_request': pull_request_obj,
1505 'pull_request': pull_request_obj,
1492 'pull_request_commits': pull_request_commits,
1506 'pull_request_commits': pull_request_commits,
1493
1507
1494 'pull_request_target_repo': pr_target_repo,
1508 'pull_request_target_repo': pr_target_repo,
1495 'pull_request_target_repo_url': pr_target_repo_url,
1509 'pull_request_target_repo_url': pr_target_repo_url,
1496
1510
1497 'pull_request_source_repo': pr_source_repo,
1511 'pull_request_source_repo': pr_source_repo,
1498 'pull_request_source_repo_url': pr_source_repo_url,
1512 'pull_request_source_repo_url': pr_source_repo_url,
1499
1513
1500 'pull_request_url': pr_url,
1514 'pull_request_url': pr_url,
1501 'thread_ids': [pr_url],
1515 'thread_ids': [pr_url],
1502 'user_role': role
1516 'user_role': role
1503 }
1517 }
1504
1518
1505 # create notification objects, and emails
1519 # create notification objects, and emails
1506 NotificationModel().create(
1520 NotificationModel().create(
1507 created_by=current_rhodecode_user,
1521 created_by=current_rhodecode_user,
1508 notification_subject='', # Filled in based on the notification_type
1522 notification_subject='', # Filled in based on the notification_type
1509 notification_body='', # Filled in based on the notification_type
1523 notification_body='', # Filled in based on the notification_type
1510 notification_type=notification_type,
1524 notification_type=notification_type,
1511 recipients=recipients,
1525 recipients=recipients,
1512 email_kwargs=kwargs,
1526 email_kwargs=kwargs,
1513 )
1527 )
1514
1528
1515 def notify_reviewers(self, pull_request, reviewers_ids, user):
1529 def notify_reviewers(self, pull_request, reviewers_ids, user):
1516 return self._notify_reviewers(pull_request, reviewers_ids,
1530 return self._notify_reviewers(pull_request, reviewers_ids,
1517 PullRequestReviewers.ROLE_REVIEWER, user)
1531 PullRequestReviewers.ROLE_REVIEWER, user)
1518
1532
1519 def notify_observers(self, pull_request, observers_ids, user):
1533 def notify_observers(self, pull_request, observers_ids, user):
1520 return self._notify_reviewers(pull_request, observers_ids,
1534 return self._notify_reviewers(pull_request, observers_ids,
1521 PullRequestReviewers.ROLE_OBSERVER, user)
1535 PullRequestReviewers.ROLE_OBSERVER, user)
1522
1536
1523 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1537 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1524 commit_changes, file_changes):
1538 commit_changes, file_changes):
1525
1539
1526 updating_user_id = updating_user.user_id
1540 updating_user_id = updating_user.user_id
1527 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1541 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1528 # NOTE(marcink): send notification to all other users except to
1542 # NOTE(marcink): send notification to all other users except to
1529 # person who updated the PR
1543 # person who updated the PR
1530 recipients = reviewers.difference(set([updating_user_id]))
1544 recipients = reviewers.difference(set([updating_user_id]))
1531
1545
1532 log.debug('Notify following recipients about pull-request update %s', recipients)
1546 log.debug('Notify following recipients about pull-request update %s', recipients)
1533
1547
1534 pull_request_obj = pull_request
1548 pull_request_obj = pull_request
1535
1549
1536 # send email about the update
1550 # send email about the update
1537 changed_files = (
1551 changed_files = (
1538 file_changes.added + file_changes.modified + file_changes.removed)
1552 file_changes.added + file_changes.modified + file_changes.removed)
1539
1553
1540 pr_source_repo = pull_request_obj.source_repo
1554 pr_source_repo = pull_request_obj.source_repo
1541 pr_target_repo = pull_request_obj.target_repo
1555 pr_target_repo = pull_request_obj.target_repo
1542
1556
1543 pr_url = h.route_url('pullrequest_show',
1557 pr_url = h.route_url('pullrequest_show',
1544 repo_name=pr_target_repo.repo_name,
1558 repo_name=pr_target_repo.repo_name,
1545 pull_request_id=pull_request_obj.pull_request_id,)
1559 pull_request_id=pull_request_obj.pull_request_id,)
1546
1560
1547 # set some variables for email notification
1561 # set some variables for email notification
1548 pr_target_repo_url = h.route_url(
1562 pr_target_repo_url = h.route_url(
1549 'repo_summary', repo_name=pr_target_repo.repo_name)
1563 'repo_summary', repo_name=pr_target_repo.repo_name)
1550
1564
1551 pr_source_repo_url = h.route_url(
1565 pr_source_repo_url = h.route_url(
1552 'repo_summary', repo_name=pr_source_repo.repo_name)
1566 'repo_summary', repo_name=pr_source_repo.repo_name)
1553
1567
1554 email_kwargs = {
1568 email_kwargs = {
1555 'date': datetime.datetime.now(),
1569 'date': datetime.datetime.now(),
1556 'updating_user': updating_user,
1570 'updating_user': updating_user,
1557
1571
1558 'pull_request': pull_request_obj,
1572 'pull_request': pull_request_obj,
1559
1573
1560 'pull_request_target_repo': pr_target_repo,
1574 'pull_request_target_repo': pr_target_repo,
1561 'pull_request_target_repo_url': pr_target_repo_url,
1575 'pull_request_target_repo_url': pr_target_repo_url,
1562
1576
1563 'pull_request_source_repo': pr_source_repo,
1577 'pull_request_source_repo': pr_source_repo,
1564 'pull_request_source_repo_url': pr_source_repo_url,
1578 'pull_request_source_repo_url': pr_source_repo_url,
1565
1579
1566 'pull_request_url': pr_url,
1580 'pull_request_url': pr_url,
1567
1581
1568 'ancestor_commit_id': ancestor_commit_id,
1582 'ancestor_commit_id': ancestor_commit_id,
1569 'added_commits': commit_changes.added,
1583 'added_commits': commit_changes.added,
1570 'removed_commits': commit_changes.removed,
1584 'removed_commits': commit_changes.removed,
1571 'changed_files': changed_files,
1585 'changed_files': changed_files,
1572 'added_files': file_changes.added,
1586 'added_files': file_changes.added,
1573 'modified_files': file_changes.modified,
1587 'modified_files': file_changes.modified,
1574 'removed_files': file_changes.removed,
1588 'removed_files': file_changes.removed,
1575 'thread_ids': [pr_url],
1589 'thread_ids': [pr_url],
1576 }
1590 }
1577
1591
1578 # create notification objects, and emails
1592 # create notification objects, and emails
1579 NotificationModel().create(
1593 NotificationModel().create(
1580 created_by=updating_user,
1594 created_by=updating_user,
1581 notification_subject='', # Filled in based on the notification_type
1595 notification_subject='', # Filled in based on the notification_type
1582 notification_body='', # Filled in based on the notification_type
1596 notification_body='', # Filled in based on the notification_type
1583 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1597 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1584 recipients=recipients,
1598 recipients=recipients,
1585 email_kwargs=email_kwargs,
1599 email_kwargs=email_kwargs,
1586 )
1600 )
1587
1601
1588 def delete(self, pull_request, user=None):
1602 def delete(self, pull_request, user=None):
1589 if not user:
1603 if not user:
1590 user = getattr(get_current_rhodecode_user(), 'username', None)
1604 user = getattr(get_current_rhodecode_user(), 'username', None)
1591
1605
1592 pull_request = self.__get_pull_request(pull_request)
1606 pull_request = self.__get_pull_request(pull_request)
1593 old_data = pull_request.get_api_data(with_merge_state=False)
1607 old_data = pull_request.get_api_data(with_merge_state=False)
1594 self._cleanup_merge_workspace(pull_request)
1608 self._cleanup_merge_workspace(pull_request)
1595 self._log_audit_action(
1609 self._log_audit_action(
1596 'repo.pull_request.delete', {'old_data': old_data},
1610 'repo.pull_request.delete', {'old_data': old_data},
1597 user, pull_request)
1611 user, pull_request)
1598 Session().delete(pull_request)
1612 Session().delete(pull_request)
1599
1613
1600 def close_pull_request(self, pull_request, user):
1614 def close_pull_request(self, pull_request, user):
1601 pull_request = self.__get_pull_request(pull_request)
1615 pull_request = self.__get_pull_request(pull_request)
1602 self._cleanup_merge_workspace(pull_request)
1616 self._cleanup_merge_workspace(pull_request)
1603 pull_request.status = PullRequest.STATUS_CLOSED
1617 pull_request.status = PullRequest.STATUS_CLOSED
1604 pull_request.updated_on = datetime.datetime.now()
1618 pull_request.updated_on = datetime.datetime.now()
1605 Session().add(pull_request)
1619 Session().add(pull_request)
1606 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1620 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1607
1621
1608 pr_data = pull_request.get_api_data(with_merge_state=False)
1622 pr_data = pull_request.get_api_data(with_merge_state=False)
1609 self._log_audit_action(
1623 self._log_audit_action(
1610 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1624 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1611
1625
1612 def close_pull_request_with_comment(
1626 def close_pull_request_with_comment(
1613 self, pull_request, user, repo, message=None, auth_user=None):
1627 self, pull_request, user, repo, message=None, auth_user=None):
1614
1628
1615 pull_request_review_status = pull_request.calculated_review_status()
1629 pull_request_review_status = pull_request.calculated_review_status()
1616
1630
1617 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1631 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1618 # approved only if we have voting consent
1632 # approved only if we have voting consent
1619 status = ChangesetStatus.STATUS_APPROVED
1633 status = ChangesetStatus.STATUS_APPROVED
1620 else:
1634 else:
1621 status = ChangesetStatus.STATUS_REJECTED
1635 status = ChangesetStatus.STATUS_REJECTED
1622 status_lbl = ChangesetStatus.get_status_lbl(status)
1636 status_lbl = ChangesetStatus.get_status_lbl(status)
1623
1637
1624 default_message = (
1638 default_message = (
1625 'Closing with status change {transition_icon} {status}.'
1639 'Closing with status change {transition_icon} {status}.'
1626 ).format(transition_icon='>', status=status_lbl)
1640 ).format(transition_icon='>', status=status_lbl)
1627 text = message or default_message
1641 text = message or default_message
1628
1642
1629 # create a comment, and link it to new status
1643 # create a comment, and link it to new status
1630 comment = CommentsModel().create(
1644 comment = CommentsModel().create(
1631 text=text,
1645 text=text,
1632 repo=repo.repo_id,
1646 repo=repo.repo_id,
1633 user=user.user_id,
1647 user=user.user_id,
1634 pull_request=pull_request.pull_request_id,
1648 pull_request=pull_request.pull_request_id,
1635 status_change=status_lbl,
1649 status_change=status_lbl,
1636 status_change_type=status,
1650 status_change_type=status,
1637 closing_pr=True,
1651 closing_pr=True,
1638 auth_user=auth_user,
1652 auth_user=auth_user,
1639 )
1653 )
1640
1654
1641 # calculate old status before we change it
1655 # calculate old status before we change it
1642 old_calculated_status = pull_request.calculated_review_status()
1656 old_calculated_status = pull_request.calculated_review_status()
1643 ChangesetStatusModel().set_status(
1657 ChangesetStatusModel().set_status(
1644 repo.repo_id,
1658 repo.repo_id,
1645 status,
1659 status,
1646 user.user_id,
1660 user.user_id,
1647 comment=comment,
1661 comment=comment,
1648 pull_request=pull_request.pull_request_id
1662 pull_request=pull_request.pull_request_id
1649 )
1663 )
1650
1664
1651 Session().flush()
1665 Session().flush()
1652
1666
1653 self.trigger_pull_request_hook(pull_request, user, 'comment',
1667 self.trigger_pull_request_hook(pull_request, user, 'comment',
1654 data={'comment': comment})
1668 data={'comment': comment})
1655
1669
1656 # we now calculate the status of pull request again, and based on that
1670 # we now calculate the status of pull request again, and based on that
1657 # calculation trigger status change. This might happen in cases
1671 # calculation trigger status change. This might happen in cases
1658 # that non-reviewer admin closes a pr, which means his vote doesn't
1672 # that non-reviewer admin closes a pr, which means his vote doesn't
1659 # change the status, while if he's a reviewer this might change it.
1673 # change the status, while if he's a reviewer this might change it.
1660 calculated_status = pull_request.calculated_review_status()
1674 calculated_status = pull_request.calculated_review_status()
1661 if old_calculated_status != calculated_status:
1675 if old_calculated_status != calculated_status:
1662 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1676 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1663 data={'status': calculated_status})
1677 data={'status': calculated_status})
1664
1678
1665 # finally close the PR
1679 # finally close the PR
1666 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1680 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1667
1681
1668 return comment, status
1682 return comment, status
1669
1683
1670 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1684 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1671 _ = translator or get_current_request().translate
1685 _ = translator or get_current_request().translate
1672
1686
1673 if not self._is_merge_enabled(pull_request):
1687 if not self._is_merge_enabled(pull_request):
1674 return None, False, _('Server-side pull request merging is disabled.')
1688 return None, False, _('Server-side pull request merging is disabled.')
1675
1689
1676 if pull_request.is_closed():
1690 if pull_request.is_closed():
1677 return None, False, _('This pull request is closed.')
1691 return None, False, _('This pull request is closed.')
1678
1692
1679 merge_possible, msg = self._check_repo_requirements(
1693 merge_possible, msg = self._check_repo_requirements(
1680 target=pull_request.target_repo, source=pull_request.source_repo,
1694 target=pull_request.target_repo, source=pull_request.source_repo,
1681 translator=_)
1695 translator=_)
1682 if not merge_possible:
1696 if not merge_possible:
1683 return None, merge_possible, msg
1697 return None, merge_possible, msg
1684
1698
1685 try:
1699 try:
1686 merge_response = self._try_merge(
1700 merge_response = self._try_merge(
1687 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1701 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1688 log.debug("Merge response: %s", merge_response)
1702 log.debug("Merge response: %s", merge_response)
1689 return merge_response, merge_response.possible, merge_response.merge_status_message
1703 return merge_response, merge_response.possible, merge_response.merge_status_message
1690 except NotImplementedError:
1704 except NotImplementedError:
1691 return None, False, _('Pull request merging is not supported.')
1705 return None, False, _('Pull request merging is not supported.')
1692
1706
1693 def _check_repo_requirements(self, target, source, translator):
1707 def _check_repo_requirements(self, target, source, translator):
1694 """
1708 """
1695 Check if `target` and `source` have compatible requirements.
1709 Check if `target` and `source` have compatible requirements.
1696
1710
1697 Currently this is just checking for largefiles.
1711 Currently this is just checking for largefiles.
1698 """
1712 """
1699 _ = translator
1713 _ = translator
1700 target_has_largefiles = self._has_largefiles(target)
1714 target_has_largefiles = self._has_largefiles(target)
1701 source_has_largefiles = self._has_largefiles(source)
1715 source_has_largefiles = self._has_largefiles(source)
1702 merge_possible = True
1716 merge_possible = True
1703 message = u''
1717 message = u''
1704
1718
1705 if target_has_largefiles != source_has_largefiles:
1719 if target_has_largefiles != source_has_largefiles:
1706 merge_possible = False
1720 merge_possible = False
1707 if source_has_largefiles:
1721 if source_has_largefiles:
1708 message = _(
1722 message = _(
1709 'Target repository large files support is disabled.')
1723 'Target repository large files support is disabled.')
1710 else:
1724 else:
1711 message = _(
1725 message = _(
1712 'Source repository large files support is disabled.')
1726 'Source repository large files support is disabled.')
1713
1727
1714 return merge_possible, message
1728 return merge_possible, message
1715
1729
1716 def _has_largefiles(self, repo):
1730 def _has_largefiles(self, repo):
1717 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1731 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1718 'extensions', 'largefiles')
1732 'extensions', 'largefiles')
1719 return largefiles_ui and largefiles_ui[0].active
1733 return largefiles_ui and largefiles_ui[0].active
1720
1734
1721 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1735 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1722 """
1736 """
1723 Try to merge the pull request and return the merge status.
1737 Try to merge the pull request and return the merge status.
1724 """
1738 """
1725 log.debug(
1739 log.debug(
1726 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1740 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1727 pull_request.pull_request_id, force_shadow_repo_refresh)
1741 pull_request.pull_request_id, force_shadow_repo_refresh)
1728 target_vcs = pull_request.target_repo.scm_instance()
1742 target_vcs = pull_request.target_repo.scm_instance()
1729 # Refresh the target reference.
1743 # Refresh the target reference.
1730 try:
1744 try:
1731 target_ref = self._refresh_reference(
1745 target_ref = self._refresh_reference(
1732 pull_request.target_ref_parts, target_vcs)
1746 pull_request.target_ref_parts, target_vcs)
1733 except CommitDoesNotExistError:
1747 except CommitDoesNotExistError:
1734 merge_state = MergeResponse(
1748 merge_state = MergeResponse(
1735 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1749 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1736 metadata={'target_ref': pull_request.target_ref_parts})
1750 metadata={'target_ref': pull_request.target_ref_parts})
1737 return merge_state
1751 return merge_state
1738
1752
1739 target_locked = pull_request.target_repo.locked
1753 target_locked = pull_request.target_repo.locked
1740 if target_locked and target_locked[0]:
1754 if target_locked and target_locked[0]:
1741 locked_by = 'user:{}'.format(target_locked[0])
1755 locked_by = 'user:{}'.format(target_locked[0])
1742 log.debug("The target repository is locked by %s.", locked_by)
1756 log.debug("The target repository is locked by %s.", locked_by)
1743 merge_state = MergeResponse(
1757 merge_state = MergeResponse(
1744 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1758 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1745 metadata={'locked_by': locked_by})
1759 metadata={'locked_by': locked_by})
1746 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1760 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1747 pull_request, target_ref):
1761 pull_request, target_ref):
1748 log.debug("Refreshing the merge status of the repository.")
1762 log.debug("Refreshing the merge status of the repository.")
1749 merge_state = self._refresh_merge_state(
1763 merge_state = self._refresh_merge_state(
1750 pull_request, target_vcs, target_ref)
1764 pull_request, target_vcs, target_ref)
1751 else:
1765 else:
1752 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1766 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1753 metadata = {
1767 metadata = {
1754 'unresolved_files': '',
1768 'unresolved_files': '',
1755 'target_ref': pull_request.target_ref_parts,
1769 'target_ref': pull_request.target_ref_parts,
1756 'source_ref': pull_request.source_ref_parts,
1770 'source_ref': pull_request.source_ref_parts,
1757 }
1771 }
1758 if pull_request.last_merge_metadata:
1772 if pull_request.last_merge_metadata:
1759 metadata.update(pull_request.last_merge_metadata_parsed)
1773 metadata.update(pull_request.last_merge_metadata_parsed)
1760
1774
1761 if not possible and target_ref.type == 'branch':
1775 if not possible and target_ref.type == 'branch':
1762 # NOTE(marcink): case for mercurial multiple heads on branch
1776 # NOTE(marcink): case for mercurial multiple heads on branch
1763 heads = target_vcs._heads(target_ref.name)
1777 heads = target_vcs._heads(target_ref.name)
1764 if len(heads) != 1:
1778 if len(heads) != 1:
1765 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1779 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1766 metadata.update({
1780 metadata.update({
1767 'heads': heads
1781 'heads': heads
1768 })
1782 })
1769
1783
1770 merge_state = MergeResponse(
1784 merge_state = MergeResponse(
1771 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1785 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1772
1786
1773 return merge_state
1787 return merge_state
1774
1788
1775 def _refresh_reference(self, reference, vcs_repository):
1789 def _refresh_reference(self, reference, vcs_repository):
1776 if reference.type in self.UPDATABLE_REF_TYPES:
1790 if reference.type in self.UPDATABLE_REF_TYPES:
1777 name_or_id = reference.name
1791 name_or_id = reference.name
1778 else:
1792 else:
1779 name_or_id = reference.commit_id
1793 name_or_id = reference.commit_id
1780
1794
1781 refreshed_commit = vcs_repository.get_commit(name_or_id)
1795 refreshed_commit = vcs_repository.get_commit(name_or_id)
1782 refreshed_reference = Reference(
1796 refreshed_reference = Reference(
1783 reference.type, reference.name, refreshed_commit.raw_id)
1797 reference.type, reference.name, refreshed_commit.raw_id)
1784 return refreshed_reference
1798 return refreshed_reference
1785
1799
1786 def _needs_merge_state_refresh(self, pull_request, target_reference):
1800 def _needs_merge_state_refresh(self, pull_request, target_reference):
1787 return not(
1801 return not(
1788 pull_request.revisions and
1802 pull_request.revisions and
1789 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1803 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1790 target_reference.commit_id == pull_request._last_merge_target_rev)
1804 target_reference.commit_id == pull_request._last_merge_target_rev)
1791
1805
1792 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1806 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1793 workspace_id = self._workspace_id(pull_request)
1807 workspace_id = self._workspace_id(pull_request)
1794 source_vcs = pull_request.source_repo.scm_instance()
1808 source_vcs = pull_request.source_repo.scm_instance()
1795 repo_id = pull_request.target_repo.repo_id
1809 repo_id = pull_request.target_repo.repo_id
1796 use_rebase = self._use_rebase_for_merging(pull_request)
1810 use_rebase = self._use_rebase_for_merging(pull_request)
1797 close_branch = self._close_branch_before_merging(pull_request)
1811 close_branch = self._close_branch_before_merging(pull_request)
1798 merge_state = target_vcs.merge(
1812 merge_state = target_vcs.merge(
1799 repo_id, workspace_id,
1813 repo_id, workspace_id,
1800 target_reference, source_vcs, pull_request.source_ref_parts,
1814 target_reference, source_vcs, pull_request.source_ref_parts,
1801 dry_run=True, use_rebase=use_rebase,
1815 dry_run=True, use_rebase=use_rebase,
1802 close_branch=close_branch)
1816 close_branch=close_branch)
1803
1817
1804 # Do not store the response if there was an unknown error.
1818 # Do not store the response if there was an unknown error.
1805 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1819 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1806 pull_request._last_merge_source_rev = \
1820 pull_request._last_merge_source_rev = \
1807 pull_request.source_ref_parts.commit_id
1821 pull_request.source_ref_parts.commit_id
1808 pull_request._last_merge_target_rev = target_reference.commit_id
1822 pull_request._last_merge_target_rev = target_reference.commit_id
1809 pull_request.last_merge_status = merge_state.failure_reason
1823 pull_request.last_merge_status = merge_state.failure_reason
1810 pull_request.last_merge_metadata = merge_state.metadata
1824 pull_request.last_merge_metadata = merge_state.metadata
1811
1825
1812 pull_request.shadow_merge_ref = merge_state.merge_ref
1826 pull_request.shadow_merge_ref = merge_state.merge_ref
1813 Session().add(pull_request)
1827 Session().add(pull_request)
1814 Session().commit()
1828 Session().commit()
1815
1829
1816 return merge_state
1830 return merge_state
1817
1831
1818 def _workspace_id(self, pull_request):
1832 def _workspace_id(self, pull_request):
1819 workspace_id = 'pr-%s' % pull_request.pull_request_id
1833 workspace_id = 'pr-%s' % pull_request.pull_request_id
1820 return workspace_id
1834 return workspace_id
1821
1835
1822 def generate_repo_data(self, repo, commit_id=None, branch=None,
1836 def generate_repo_data(self, repo, commit_id=None, branch=None,
1823 bookmark=None, translator=None):
1837 bookmark=None, translator=None):
1824 from rhodecode.model.repo import RepoModel
1838 from rhodecode.model.repo import RepoModel
1825
1839
1826 all_refs, selected_ref = \
1840 all_refs, selected_ref = \
1827 self._get_repo_pullrequest_sources(
1841 self._get_repo_pullrequest_sources(
1828 repo.scm_instance(), commit_id=commit_id,
1842 repo.scm_instance(), commit_id=commit_id,
1829 branch=branch, bookmark=bookmark, translator=translator)
1843 branch=branch, bookmark=bookmark, translator=translator)
1830
1844
1831 refs_select2 = []
1845 refs_select2 = []
1832 for element in all_refs:
1846 for element in all_refs:
1833 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1847 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1834 refs_select2.append({'text': element[1], 'children': children})
1848 refs_select2.append({'text': element[1], 'children': children})
1835
1849
1836 return {
1850 return {
1837 'user': {
1851 'user': {
1838 'user_id': repo.user.user_id,
1852 'user_id': repo.user.user_id,
1839 'username': repo.user.username,
1853 'username': repo.user.username,
1840 'firstname': repo.user.first_name,
1854 'firstname': repo.user.first_name,
1841 'lastname': repo.user.last_name,
1855 'lastname': repo.user.last_name,
1842 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1856 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1843 },
1857 },
1844 'name': repo.repo_name,
1858 'name': repo.repo_name,
1845 'link': RepoModel().get_url(repo),
1859 'link': RepoModel().get_url(repo),
1846 'description': h.chop_at_smart(repo.description_safe, '\n'),
1860 'description': h.chop_at_smart(repo.description_safe, '\n'),
1847 'refs': {
1861 'refs': {
1848 'all_refs': all_refs,
1862 'all_refs': all_refs,
1849 'selected_ref': selected_ref,
1863 'selected_ref': selected_ref,
1850 'select2_refs': refs_select2
1864 'select2_refs': refs_select2
1851 }
1865 }
1852 }
1866 }
1853
1867
1854 def generate_pullrequest_title(self, source, source_ref, target):
1868 def generate_pullrequest_title(self, source, source_ref, target):
1855 return u'{source}#{at_ref} to {target}'.format(
1869 return u'{source}#{at_ref} to {target}'.format(
1856 source=source,
1870 source=source,
1857 at_ref=source_ref,
1871 at_ref=source_ref,
1858 target=target,
1872 target=target,
1859 )
1873 )
1860
1874
1861 def _cleanup_merge_workspace(self, pull_request):
1875 def _cleanup_merge_workspace(self, pull_request):
1862 # Merging related cleanup
1876 # Merging related cleanup
1863 repo_id = pull_request.target_repo.repo_id
1877 repo_id = pull_request.target_repo.repo_id
1864 target_scm = pull_request.target_repo.scm_instance()
1878 target_scm = pull_request.target_repo.scm_instance()
1865 workspace_id = self._workspace_id(pull_request)
1879 workspace_id = self._workspace_id(pull_request)
1866
1880
1867 try:
1881 try:
1868 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1882 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1869 except NotImplementedError:
1883 except NotImplementedError:
1870 pass
1884 pass
1871
1885
1872 def _get_repo_pullrequest_sources(
1886 def _get_repo_pullrequest_sources(
1873 self, repo, commit_id=None, branch=None, bookmark=None,
1887 self, repo, commit_id=None, branch=None, bookmark=None,
1874 translator=None):
1888 translator=None):
1875 """
1889 """
1876 Return a structure with repo's interesting commits, suitable for
1890 Return a structure with repo's interesting commits, suitable for
1877 the selectors in pullrequest controller
1891 the selectors in pullrequest controller
1878
1892
1879 :param commit_id: a commit that must be in the list somehow
1893 :param commit_id: a commit that must be in the list somehow
1880 and selected by default
1894 and selected by default
1881 :param branch: a branch that must be in the list and selected
1895 :param branch: a branch that must be in the list and selected
1882 by default - even if closed
1896 by default - even if closed
1883 :param bookmark: a bookmark that must be in the list and selected
1897 :param bookmark: a bookmark that must be in the list and selected
1884 """
1898 """
1885 _ = translator or get_current_request().translate
1899 _ = translator or get_current_request().translate
1886
1900
1887 commit_id = safe_str(commit_id) if commit_id else None
1901 commit_id = safe_str(commit_id) if commit_id else None
1888 branch = safe_unicode(branch) if branch else None
1902 branch = safe_unicode(branch) if branch else None
1889 bookmark = safe_unicode(bookmark) if bookmark else None
1903 bookmark = safe_unicode(bookmark) if bookmark else None
1890
1904
1891 selected = None
1905 selected = None
1892
1906
1893 # order matters: first source that has commit_id in it will be selected
1907 # order matters: first source that has commit_id in it will be selected
1894 sources = []
1908 sources = []
1895 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1909 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1896 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1910 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1897
1911
1898 if commit_id:
1912 if commit_id:
1899 ref_commit = (h.short_id(commit_id), commit_id)
1913 ref_commit = (h.short_id(commit_id), commit_id)
1900 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1914 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1901
1915
1902 sources.append(
1916 sources.append(
1903 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1917 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1904 )
1918 )
1905
1919
1906 groups = []
1920 groups = []
1907
1921
1908 for group_key, ref_list, group_name, match in sources:
1922 for group_key, ref_list, group_name, match in sources:
1909 group_refs = []
1923 group_refs = []
1910 for ref_name, ref_id in ref_list:
1924 for ref_name, ref_id in ref_list:
1911 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1925 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1912 group_refs.append((ref_key, ref_name))
1926 group_refs.append((ref_key, ref_name))
1913
1927
1914 if not selected:
1928 if not selected:
1915 if set([commit_id, match]) & set([ref_id, ref_name]):
1929 if set([commit_id, match]) & set([ref_id, ref_name]):
1916 selected = ref_key
1930 selected = ref_key
1917
1931
1918 if group_refs:
1932 if group_refs:
1919 groups.append((group_refs, group_name))
1933 groups.append((group_refs, group_name))
1920
1934
1921 if not selected:
1935 if not selected:
1922 ref = commit_id or branch or bookmark
1936 ref = commit_id or branch or bookmark
1923 if ref:
1937 if ref:
1924 raise CommitDoesNotExistError(
1938 raise CommitDoesNotExistError(
1925 u'No commit refs could be found matching: {}'.format(ref))
1939 u'No commit refs could be found matching: {}'.format(ref))
1926 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1940 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1927 selected = u'branch:{}:{}'.format(
1941 selected = u'branch:{}:{}'.format(
1928 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1942 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1929 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1943 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1930 )
1944 )
1931 elif repo.commit_ids:
1945 elif repo.commit_ids:
1932 # make the user select in this case
1946 # make the user select in this case
1933 selected = None
1947 selected = None
1934 else:
1948 else:
1935 raise EmptyRepositoryError()
1949 raise EmptyRepositoryError()
1936 return groups, selected
1950 return groups, selected
1937
1951
1938 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1952 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1939 hide_whitespace_changes, diff_context):
1953 hide_whitespace_changes, diff_context):
1940
1954
1941 return self._get_diff_from_pr_or_version(
1955 return self._get_diff_from_pr_or_version(
1942 source_repo, source_ref_id, target_ref_id,
1956 source_repo, source_ref_id, target_ref_id,
1943 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1957 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1944
1958
1945 def _get_diff_from_pr_or_version(
1959 def _get_diff_from_pr_or_version(
1946 self, source_repo, source_ref_id, target_ref_id,
1960 self, source_repo, source_ref_id, target_ref_id,
1947 hide_whitespace_changes, diff_context):
1961 hide_whitespace_changes, diff_context):
1948
1962
1949 target_commit = source_repo.get_commit(
1963 target_commit = source_repo.get_commit(
1950 commit_id=safe_str(target_ref_id))
1964 commit_id=safe_str(target_ref_id))
1951 source_commit = source_repo.get_commit(
1965 source_commit = source_repo.get_commit(
1952 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1966 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1953 if isinstance(source_repo, Repository):
1967 if isinstance(source_repo, Repository):
1954 vcs_repo = source_repo.scm_instance()
1968 vcs_repo = source_repo.scm_instance()
1955 else:
1969 else:
1956 vcs_repo = source_repo
1970 vcs_repo = source_repo
1957
1971
1958 # TODO: johbo: In the context of an update, we cannot reach
1972 # TODO: johbo: In the context of an update, we cannot reach
1959 # the old commit anymore with our normal mechanisms. It needs
1973 # the old commit anymore with our normal mechanisms. It needs
1960 # some sort of special support in the vcs layer to avoid this
1974 # some sort of special support in the vcs layer to avoid this
1961 # workaround.
1975 # workaround.
1962 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1976 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1963 vcs_repo.alias == 'git'):
1977 vcs_repo.alias == 'git'):
1964 source_commit.raw_id = safe_str(source_ref_id)
1978 source_commit.raw_id = safe_str(source_ref_id)
1965
1979
1966 log.debug('calculating diff between '
1980 log.debug('calculating diff between '
1967 'source_ref:%s and target_ref:%s for repo `%s`',
1981 'source_ref:%s and target_ref:%s for repo `%s`',
1968 target_ref_id, source_ref_id,
1982 target_ref_id, source_ref_id,
1969 safe_unicode(vcs_repo.path))
1983 safe_unicode(vcs_repo.path))
1970
1984
1971 vcs_diff = vcs_repo.get_diff(
1985 vcs_diff = vcs_repo.get_diff(
1972 commit1=target_commit, commit2=source_commit,
1986 commit1=target_commit, commit2=source_commit,
1973 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1987 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1974 return vcs_diff
1988 return vcs_diff
1975
1989
1976 def _is_merge_enabled(self, pull_request):
1990 def _is_merge_enabled(self, pull_request):
1977 return self._get_general_setting(
1991 return self._get_general_setting(
1978 pull_request, 'rhodecode_pr_merge_enabled')
1992 pull_request, 'rhodecode_pr_merge_enabled')
1979
1993
1980 def _use_rebase_for_merging(self, pull_request):
1994 def _use_rebase_for_merging(self, pull_request):
1981 repo_type = pull_request.target_repo.repo_type
1995 repo_type = pull_request.target_repo.repo_type
1982 if repo_type == 'hg':
1996 if repo_type == 'hg':
1983 return self._get_general_setting(
1997 return self._get_general_setting(
1984 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1998 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1985 elif repo_type == 'git':
1999 elif repo_type == 'git':
1986 return self._get_general_setting(
2000 return self._get_general_setting(
1987 pull_request, 'rhodecode_git_use_rebase_for_merging')
2001 pull_request, 'rhodecode_git_use_rebase_for_merging')
1988
2002
1989 return False
2003 return False
1990
2004
1991 def _user_name_for_merging(self, pull_request, user):
2005 def _user_name_for_merging(self, pull_request, user):
1992 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2006 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1993 if env_user_name_attr and hasattr(user, env_user_name_attr):
2007 if env_user_name_attr and hasattr(user, env_user_name_attr):
1994 user_name_attr = env_user_name_attr
2008 user_name_attr = env_user_name_attr
1995 else:
2009 else:
1996 user_name_attr = 'short_contact'
2010 user_name_attr = 'short_contact'
1997
2011
1998 user_name = getattr(user, user_name_attr)
2012 user_name = getattr(user, user_name_attr)
1999 return user_name
2013 return user_name
2000
2014
2001 def _close_branch_before_merging(self, pull_request):
2015 def _close_branch_before_merging(self, pull_request):
2002 repo_type = pull_request.target_repo.repo_type
2016 repo_type = pull_request.target_repo.repo_type
2003 if repo_type == 'hg':
2017 if repo_type == 'hg':
2004 return self._get_general_setting(
2018 return self._get_general_setting(
2005 pull_request, 'rhodecode_hg_close_branch_before_merging')
2019 pull_request, 'rhodecode_hg_close_branch_before_merging')
2006 elif repo_type == 'git':
2020 elif repo_type == 'git':
2007 return self._get_general_setting(
2021 return self._get_general_setting(
2008 pull_request, 'rhodecode_git_close_branch_before_merging')
2022 pull_request, 'rhodecode_git_close_branch_before_merging')
2009
2023
2010 return False
2024 return False
2011
2025
2012 def _get_general_setting(self, pull_request, settings_key, default=False):
2026 def _get_general_setting(self, pull_request, settings_key, default=False):
2013 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2027 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2014 settings = settings_model.get_general_settings()
2028 settings = settings_model.get_general_settings()
2015 return settings.get(settings_key, default)
2029 return settings.get(settings_key, default)
2016
2030
2017 def _log_audit_action(self, action, action_data, user, pull_request):
2031 def _log_audit_action(self, action, action_data, user, pull_request):
2018 audit_logger.store(
2032 audit_logger.store(
2019 action=action,
2033 action=action,
2020 action_data=action_data,
2034 action_data=action_data,
2021 user=user,
2035 user=user,
2022 repo=pull_request.target_repo)
2036 repo=pull_request.target_repo)
2023
2037
2024 def get_reviewer_functions(self):
2038 def get_reviewer_functions(self):
2025 """
2039 """
2026 Fetches functions for validation and fetching default reviewers.
2040 Fetches functions for validation and fetching default reviewers.
2027 If available we use the EE package, else we fallback to CE
2041 If available we use the EE package, else we fallback to CE
2028 package functions
2042 package functions
2029 """
2043 """
2030 try:
2044 try:
2031 from rc_reviewers.utils import get_default_reviewers_data
2045 from rc_reviewers.utils import get_default_reviewers_data
2032 from rc_reviewers.utils import validate_default_reviewers
2046 from rc_reviewers.utils import validate_default_reviewers
2033 from rc_reviewers.utils import validate_observers
2047 from rc_reviewers.utils import validate_observers
2034 except ImportError:
2048 except ImportError:
2035 from rhodecode.apps.repository.utils import get_default_reviewers_data
2049 from rhodecode.apps.repository.utils import get_default_reviewers_data
2036 from rhodecode.apps.repository.utils import validate_default_reviewers
2050 from rhodecode.apps.repository.utils import validate_default_reviewers
2037 from rhodecode.apps.repository.utils import validate_observers
2051 from rhodecode.apps.repository.utils import validate_observers
2038
2052
2039 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2053 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2040
2054
2041
2055
2042 class MergeCheck(object):
2056 class MergeCheck(object):
2043 """
2057 """
2044 Perform Merge Checks and returns a check object which stores information
2058 Perform Merge Checks and returns a check object which stores information
2045 about merge errors, and merge conditions
2059 about merge errors, and merge conditions
2046 """
2060 """
2047 TODO_CHECK = 'todo'
2061 TODO_CHECK = 'todo'
2048 PERM_CHECK = 'perm'
2062 PERM_CHECK = 'perm'
2049 REVIEW_CHECK = 'review'
2063 REVIEW_CHECK = 'review'
2050 MERGE_CHECK = 'merge'
2064 MERGE_CHECK = 'merge'
2051 WIP_CHECK = 'wip'
2065 WIP_CHECK = 'wip'
2052
2066
2053 def __init__(self):
2067 def __init__(self):
2054 self.review_status = None
2068 self.review_status = None
2055 self.merge_possible = None
2069 self.merge_possible = None
2056 self.merge_msg = ''
2070 self.merge_msg = ''
2057 self.merge_response = None
2071 self.merge_response = None
2058 self.failed = None
2072 self.failed = None
2059 self.errors = []
2073 self.errors = []
2060 self.error_details = OrderedDict()
2074 self.error_details = OrderedDict()
2061 self.source_commit = AttributeDict()
2075 self.source_commit = AttributeDict()
2062 self.target_commit = AttributeDict()
2076 self.target_commit = AttributeDict()
2063 self.reviewers_count = 0
2077 self.reviewers_count = 0
2064 self.observers_count = 0
2078 self.observers_count = 0
2065
2079
2066 def __repr__(self):
2080 def __repr__(self):
2067 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2081 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2068 self.merge_possible, self.failed, self.errors)
2082 self.merge_possible, self.failed, self.errors)
2069
2083
2070 def push_error(self, error_type, message, error_key, details):
2084 def push_error(self, error_type, message, error_key, details):
2071 self.failed = True
2085 self.failed = True
2072 self.errors.append([error_type, message])
2086 self.errors.append([error_type, message])
2073 self.error_details[error_key] = dict(
2087 self.error_details[error_key] = dict(
2074 details=details,
2088 details=details,
2075 error_type=error_type,
2089 error_type=error_type,
2076 message=message
2090 message=message
2077 )
2091 )
2078
2092
2079 @classmethod
2093 @classmethod
2080 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2094 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2081 force_shadow_repo_refresh=False):
2095 force_shadow_repo_refresh=False):
2082 _ = translator
2096 _ = translator
2083 merge_check = cls()
2097 merge_check = cls()
2084
2098
2085 # title has WIP:
2099 # title has WIP:
2086 if pull_request.work_in_progress:
2100 if pull_request.work_in_progress:
2087 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2101 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2088
2102
2089 msg = _('WIP marker in title prevents from accidental merge.')
2103 msg = _('WIP marker in title prevents from accidental merge.')
2090 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2104 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2091 if fail_early:
2105 if fail_early:
2092 return merge_check
2106 return merge_check
2093
2107
2094 # permissions to merge
2108 # permissions to merge
2095 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2109 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2096 if not user_allowed_to_merge:
2110 if not user_allowed_to_merge:
2097 log.debug("MergeCheck: cannot merge, approval is pending.")
2111 log.debug("MergeCheck: cannot merge, approval is pending.")
2098
2112
2099 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2113 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2100 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2114 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2101 if fail_early:
2115 if fail_early:
2102 return merge_check
2116 return merge_check
2103
2117
2104 # permission to merge into the target branch
2118 # permission to merge into the target branch
2105 target_commit_id = pull_request.target_ref_parts.commit_id
2119 target_commit_id = pull_request.target_ref_parts.commit_id
2106 if pull_request.target_ref_parts.type == 'branch':
2120 if pull_request.target_ref_parts.type == 'branch':
2107 branch_name = pull_request.target_ref_parts.name
2121 branch_name = pull_request.target_ref_parts.name
2108 else:
2122 else:
2109 # for mercurial we can always figure out the branch from the commit
2123 # for mercurial we can always figure out the branch from the commit
2110 # in case of bookmark
2124 # in case of bookmark
2111 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2125 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2112 branch_name = target_commit.branch
2126 branch_name = target_commit.branch
2113
2127
2114 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2128 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2115 pull_request.target_repo.repo_name, branch_name)
2129 pull_request.target_repo.repo_name, branch_name)
2116 if branch_perm and branch_perm == 'branch.none':
2130 if branch_perm and branch_perm == 'branch.none':
2117 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2131 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2118 branch_name, rule)
2132 branch_name, rule)
2119 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2133 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2120 if fail_early:
2134 if fail_early:
2121 return merge_check
2135 return merge_check
2122
2136
2123 # review status, must be always present
2137 # review status, must be always present
2124 review_status = pull_request.calculated_review_status()
2138 review_status = pull_request.calculated_review_status()
2125 merge_check.review_status = review_status
2139 merge_check.review_status = review_status
2126 merge_check.reviewers_count = pull_request.reviewers_count
2140 merge_check.reviewers_count = pull_request.reviewers_count
2127 merge_check.observers_count = pull_request.observers_count
2141 merge_check.observers_count = pull_request.observers_count
2128
2142
2129 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2143 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2130 if not status_approved and merge_check.reviewers_count:
2144 if not status_approved and merge_check.reviewers_count:
2131 log.debug("MergeCheck: cannot merge, approval is pending.")
2145 log.debug("MergeCheck: cannot merge, approval is pending.")
2132 msg = _('Pull request reviewer approval is pending.')
2146 msg = _('Pull request reviewer approval is pending.')
2133
2147
2134 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2148 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2135
2149
2136 if fail_early:
2150 if fail_early:
2137 return merge_check
2151 return merge_check
2138
2152
2139 # left over TODOs
2153 # left over TODOs
2140 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2154 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2141 if todos:
2155 if todos:
2142 log.debug("MergeCheck: cannot merge, {} "
2156 log.debug("MergeCheck: cannot merge, {} "
2143 "unresolved TODOs left.".format(len(todos)))
2157 "unresolved TODOs left.".format(len(todos)))
2144
2158
2145 if len(todos) == 1:
2159 if len(todos) == 1:
2146 msg = _('Cannot merge, {} TODO still not resolved.').format(
2160 msg = _('Cannot merge, {} TODO still not resolved.').format(
2147 len(todos))
2161 len(todos))
2148 else:
2162 else:
2149 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2163 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2150 len(todos))
2164 len(todos))
2151
2165
2152 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2166 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2153
2167
2154 if fail_early:
2168 if fail_early:
2155 return merge_check
2169 return merge_check
2156
2170
2157 # merge possible, here is the filesystem simulation + shadow repo
2171 # merge possible, here is the filesystem simulation + shadow repo
2158 merge_response, merge_status, msg = PullRequestModel().merge_status(
2172 merge_response, merge_status, msg = PullRequestModel().merge_status(
2159 pull_request, translator=translator,
2173 pull_request, translator=translator,
2160 force_shadow_repo_refresh=force_shadow_repo_refresh)
2174 force_shadow_repo_refresh=force_shadow_repo_refresh)
2161
2175
2162 merge_check.merge_possible = merge_status
2176 merge_check.merge_possible = merge_status
2163 merge_check.merge_msg = msg
2177 merge_check.merge_msg = msg
2164 merge_check.merge_response = merge_response
2178 merge_check.merge_response = merge_response
2165
2179
2166 source_ref_id = pull_request.source_ref_parts.commit_id
2180 source_ref_id = pull_request.source_ref_parts.commit_id
2167 target_ref_id = pull_request.target_ref_parts.commit_id
2181 target_ref_id = pull_request.target_ref_parts.commit_id
2168
2182
2169 try:
2183 try:
2170 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2184 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2171 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2185 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2172 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2186 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2173 merge_check.source_commit.current_raw_id = source_commit.raw_id
2187 merge_check.source_commit.current_raw_id = source_commit.raw_id
2174 merge_check.source_commit.previous_raw_id = source_ref_id
2188 merge_check.source_commit.previous_raw_id = source_ref_id
2175
2189
2176 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2190 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2177 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2191 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2178 merge_check.target_commit.current_raw_id = target_commit.raw_id
2192 merge_check.target_commit.current_raw_id = target_commit.raw_id
2179 merge_check.target_commit.previous_raw_id = target_ref_id
2193 merge_check.target_commit.previous_raw_id = target_ref_id
2180 except (SourceRefMissing, TargetRefMissing):
2194 except (SourceRefMissing, TargetRefMissing):
2181 pass
2195 pass
2182
2196
2183 if not merge_status:
2197 if not merge_status:
2184 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2198 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2185 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2199 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2186
2200
2187 if fail_early:
2201 if fail_early:
2188 return merge_check
2202 return merge_check
2189
2203
2190 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2204 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2191 return merge_check
2205 return merge_check
2192
2206
2193 @classmethod
2207 @classmethod
2194 def get_merge_conditions(cls, pull_request, translator):
2208 def get_merge_conditions(cls, pull_request, translator):
2195 _ = translator
2209 _ = translator
2196 merge_details = {}
2210 merge_details = {}
2197
2211
2198 model = PullRequestModel()
2212 model = PullRequestModel()
2199 use_rebase = model._use_rebase_for_merging(pull_request)
2213 use_rebase = model._use_rebase_for_merging(pull_request)
2200
2214
2201 if use_rebase:
2215 if use_rebase:
2202 merge_details['merge_strategy'] = dict(
2216 merge_details['merge_strategy'] = dict(
2203 details={},
2217 details={},
2204 message=_('Merge strategy: rebase')
2218 message=_('Merge strategy: rebase')
2205 )
2219 )
2206 else:
2220 else:
2207 merge_details['merge_strategy'] = dict(
2221 merge_details['merge_strategy'] = dict(
2208 details={},
2222 details={},
2209 message=_('Merge strategy: explicit merge commit')
2223 message=_('Merge strategy: explicit merge commit')
2210 )
2224 )
2211
2225
2212 close_branch = model._close_branch_before_merging(pull_request)
2226 close_branch = model._close_branch_before_merging(pull_request)
2213 if close_branch:
2227 if close_branch:
2214 repo_type = pull_request.target_repo.repo_type
2228 repo_type = pull_request.target_repo.repo_type
2215 close_msg = ''
2229 close_msg = ''
2216 if repo_type == 'hg':
2230 if repo_type == 'hg':
2217 close_msg = _('Source branch will be closed before the merge.')
2231 close_msg = _('Source branch will be closed before the merge.')
2218 elif repo_type == 'git':
2232 elif repo_type == 'git':
2219 close_msg = _('Source branch will be deleted after the merge.')
2233 close_msg = _('Source branch will be deleted after the merge.')
2220
2234
2221 merge_details['close_branch'] = dict(
2235 merge_details['close_branch'] = dict(
2222 details={},
2236 details={},
2223 message=close_msg
2237 message=close_msg
2224 )
2238 )
2225
2239
2226 return merge_details
2240 return merge_details
2227
2241
2228
2242
2229 ChangeTuple = collections.namedtuple(
2243 ChangeTuple = collections.namedtuple(
2230 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2244 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2231
2245
2232 FileChangeTuple = collections.namedtuple(
2246 FileChangeTuple = collections.namedtuple(
2233 'FileChangeTuple', ['added', 'modified', 'removed'])
2247 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,80 +1,88 b''
1 ## Changesets table !
1 ## Changesets table !
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 %if c.ancestor:
4 %if c.ancestor:
5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
8 </div>
8 </div>
9 %endif
9 %endif
10
10
11 <div class="container">
11 <div class="container">
12 <input type="hidden" name="__start__" value="revisions:sequence">
12 <input type="hidden" name="__start__" value="revisions:sequence">
13 <table class="rctable compare_view_commits">
13 <table class="rctable compare_view_commits">
14 <tr>
14 <tr>
15 % if hasattr(c, 'commit_versions'):
16 <th>ver</th>
17 % endif
15 <th>${_('Time')}</th>
18 <th>${_('Time')}</th>
16 <th>${_('Author')}</th>
19 <th>${_('Author')}</th>
17 <th>${_('Commit')}</th>
20 <th>${_('Commit')}</th>
18 <th></th>
21 <th></th>
19 <th>${_('Description')}</th>
22 <th>${_('Description')}</th>
20 </tr>
23 </tr>
21 ## to speed up lookups cache some functions before the loop
24 ## to speed up lookups cache some functions before the loop
22 <%
25 <%
23 active_patterns = h.get_active_pattern_entries(c.repo_name)
26 active_patterns = h.get_active_pattern_entries(c.repo_name)
24 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
27 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
25 %>
28 %>
26
29
27 %for commit in c.commit_ranges:
30 %for commit in c.commit_ranges:
28 <tr id="row-${commit.raw_id}"
31 <tr id="row-${commit.raw_id}"
29 commit_id="${commit.raw_id}"
32 commit_id="${commit.raw_id}"
30 class="compare_select"
33 class="compare_select"
31 style="${'display: none' if c.collapse_all_commits else ''}"
34 style="${'display: none' if c.collapse_all_commits else ''}"
32 >
35 >
36 % if hasattr(c, 'commit_versions'):
37 <td class="tooltip" title="${_('Pull request version this commit was introduced')}">
38 <code>${('v{}'.format(c.commit_versions[commit.raw_id][0]) if c.commit_versions[commit.raw_id] else 'latest')}</code>
39 </td>
40 % endif
33 <td class="td-time">
41 <td class="td-time">
34 ${h.age_component(commit.date)}
42 ${h.age_component(commit.date)}
35 </td>
43 </td>
36 <td class="td-user">
44 <td class="td-user">
37 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
45 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
38 </td>
46 </td>
39 <td class="td-hash">
47 <td class="td-hash">
40 <code>
48 <code>
41 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
49 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
42 r${commit.idx}:${h.short_id(commit.raw_id)}
50 r${commit.idx}:${h.short_id(commit.raw_id)}
43 </a>
51 </a>
44 ${h.hidden('revisions',commit.raw_id)}
52 ${h.hidden('revisions',commit.raw_id)}
45 </code>
53 </code>
46 </td>
54 </td>
47 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
55 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
48 <i class="icon-expand-linked"></i>
56 <i class="icon-expand-linked"></i>
49 </td>
57 </td>
50 <td class="mid td-description">
58 <td class="mid td-description">
51 <div class="log-container truncate-wrap">
59 <div class="log-container truncate-wrap">
52 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
60 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
53 </div>
61 </div>
54 </td>
62 </td>
55 </tr>
63 </tr>
56 %endfor
64 %endfor
57 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
65 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
58 <td colspan="5">
66 <td colspan="5">
59 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
67 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
60 </td>
68 </td>
61 </tr>
69 </tr>
62 % if not c.commit_ranges:
70 % if not c.commit_ranges:
63 <tr class="compare_select">
71 <tr class="compare_select">
64 <td colspan="5">
72 <td colspan="5">
65 ${_('No commits in this compare')}
73 ${_('No commits in this compare')}
66 </td>
74 </td>
67 </tr>
75 </tr>
68 % endif
76 % endif
69 </table>
77 </table>
70 <input type="hidden" name="__end__" value="revisions:sequence">
78 <input type="hidden" name="__end__" value="revisions:sequence">
71
79
72 </div>
80 </div>
73
81
74 <script>
82 <script>
75 commitsController = new CommitsController();
83 commitsController = new CommitsController();
76 $('.compare_select').on('click',function(e){
84 $('.compare_select').on('click',function(e){
77 var cid = $(this).attr('commit_id');
85 var cid = $(this).attr('commit_id');
78 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
86 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
79 });
87 });
80 </script>
88 </script>
General Comments 0
You need to be logged in to leave comments. Login now