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