##// END OF EJS Templates
inline-comments: added helper to properly count inline comments.
marcink -
r1206:5653a4e4 stable
parent child Browse files
Show More
@@ -1,464 +1,464 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 c.commit_statuses = ChangesetStatus.STATUSES
201 c.commit_statuses = ChangesetStatus.STATUSES
202 c.inline_comments = []
202 c.inline_comments = []
203 c.inline_cnt = 0
204 c.files = []
203 c.files = []
205
204
206 c.statuses = []
205 c.statuses = []
207 c.comments = []
206 c.comments = []
208 if len(c.commit_ranges) == 1:
207 if len(c.commit_ranges) == 1:
209 commit = c.commit_ranges[0]
208 commit = c.commit_ranges[0]
210 c.comments = ChangesetCommentsModel().get_comments(
209 c.comments = ChangesetCommentsModel().get_comments(
211 c.rhodecode_db_repo.repo_id,
210 c.rhodecode_db_repo.repo_id,
212 revision=commit.raw_id)
211 revision=commit.raw_id)
213 c.statuses.append(ChangesetStatusModel().get_status(
212 c.statuses.append(ChangesetStatusModel().get_status(
214 c.rhodecode_db_repo.repo_id, commit.raw_id))
213 c.rhodecode_db_repo.repo_id, commit.raw_id))
215 # comments from PR
214 # comments from PR
216 statuses = ChangesetStatusModel().get_statuses(
215 statuses = ChangesetStatusModel().get_statuses(
217 c.rhodecode_db_repo.repo_id, commit.raw_id,
216 c.rhodecode_db_repo.repo_id, commit.raw_id,
218 with_revisions=True)
217 with_revisions=True)
219 prs = set(st.pull_request for st in statuses
218 prs = set(st.pull_request for st in statuses
220 if st.pull_request is not None)
219 if st.pull_request is not None)
221 # from associated statuses, check the pull requests, and
220 # from associated statuses, check the pull requests, and
222 # show comments from them
221 # show comments from them
223 for pr in prs:
222 for pr in prs:
224 c.comments.extend(pr.comments)
223 c.comments.extend(pr.comments)
225
224
226 # Iterate over ranges (default commit view is always one commit)
225 # Iterate over ranges (default commit view is always one commit)
227 for commit in c.commit_ranges:
226 for commit in c.commit_ranges:
228 c.changes[commit.raw_id] = []
227 c.changes[commit.raw_id] = []
229
228
230 commit2 = commit
229 commit2 = commit
231 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
230 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
232
231
233 _diff = c.rhodecode_repo.get_diff(
232 _diff = c.rhodecode_repo.get_diff(
234 commit1, commit2,
233 commit1, commit2,
235 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
234 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
236 diff_processor = diffs.DiffProcessor(
235 diff_processor = diffs.DiffProcessor(
237 _diff, format='newdiff', diff_limit=diff_limit,
236 _diff, format='newdiff', diff_limit=diff_limit,
238 file_limit=file_limit, show_full_diff=fulldiff)
237 file_limit=file_limit, show_full_diff=fulldiff)
239
238
240 commit_changes = OrderedDict()
239 commit_changes = OrderedDict()
241 if method == 'show':
240 if method == 'show':
242 _parsed = diff_processor.prepare()
241 _parsed = diff_processor.prepare()
243 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
242 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
244
243
245 _parsed = diff_processor.prepare()
244 _parsed = diff_processor.prepare()
246
245
247 def _node_getter(commit):
246 def _node_getter(commit):
248 def get_node(fname):
247 def get_node(fname):
249 try:
248 try:
250 return commit.get_node(fname)
249 return commit.get_node(fname)
251 except NodeDoesNotExistError:
250 except NodeDoesNotExistError:
252 return None
251 return None
253 return get_node
252 return get_node
254
253
255 inline_comments = ChangesetCommentsModel().get_inline_comments(
254 inline_comments = ChangesetCommentsModel().get_inline_comments(
256 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
255 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
257 c.inline_cnt += len(inline_comments)
256 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
257 inline_comments)
258
258
259 diffset = codeblocks.DiffSet(
259 diffset = codeblocks.DiffSet(
260 repo_name=c.repo_name,
260 repo_name=c.repo_name,
261 source_node_getter=_node_getter(commit1),
261 source_node_getter=_node_getter(commit1),
262 target_node_getter=_node_getter(commit2),
262 target_node_getter=_node_getter(commit2),
263 comments=inline_comments
263 comments=inline_comments
264 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
264 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
265 c.changes[commit.raw_id] = diffset
265 c.changes[commit.raw_id] = diffset
266 else:
266 else:
267 # downloads/raw we only need RAW diff nothing else
267 # downloads/raw we only need RAW diff nothing else
268 diff = diff_processor.as_raw()
268 diff = diff_processor.as_raw()
269 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
269 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
270
270
271 # sort comments by how they were generated
271 # sort comments by how they were generated
272 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
272 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
273
273
274
274
275 if len(c.commit_ranges) == 1:
275 if len(c.commit_ranges) == 1:
276 c.commit = c.commit_ranges[0]
276 c.commit = c.commit_ranges[0]
277 c.parent_tmpl = ''.join(
277 c.parent_tmpl = ''.join(
278 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
278 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
279 if method == 'download':
279 if method == 'download':
280 response.content_type = 'text/plain'
280 response.content_type = 'text/plain'
281 response.content_disposition = (
281 response.content_disposition = (
282 'attachment; filename=%s.diff' % commit_id_range[:12])
282 'attachment; filename=%s.diff' % commit_id_range[:12])
283 return diff
283 return diff
284 elif method == 'patch':
284 elif method == 'patch':
285 response.content_type = 'text/plain'
285 response.content_type = 'text/plain'
286 c.diff = safe_unicode(diff)
286 c.diff = safe_unicode(diff)
287 return render('changeset/patch_changeset.html')
287 return render('changeset/patch_changeset.html')
288 elif method == 'raw':
288 elif method == 'raw':
289 response.content_type = 'text/plain'
289 response.content_type = 'text/plain'
290 return diff
290 return diff
291 elif method == 'show':
291 elif method == 'show':
292 if len(c.commit_ranges) == 1:
292 if len(c.commit_ranges) == 1:
293 return render('changeset/changeset.html')
293 return render('changeset/changeset.html')
294 else:
294 else:
295 c.ancestor = None
295 c.ancestor = None
296 c.target_repo = c.rhodecode_db_repo
296 c.target_repo = c.rhodecode_db_repo
297 return render('changeset/changeset_range.html')
297 return render('changeset/changeset_range.html')
298
298
299 @LoginRequired()
299 @LoginRequired()
300 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
300 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
301 'repository.admin')
301 'repository.admin')
302 def index(self, revision, method='show'):
302 def index(self, revision, method='show'):
303 return self._index(revision, method=method)
303 return self._index(revision, method=method)
304
304
305 @LoginRequired()
305 @LoginRequired()
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 'repository.admin')
307 'repository.admin')
308 def changeset_raw(self, revision):
308 def changeset_raw(self, revision):
309 return self._index(revision, method='raw')
309 return self._index(revision, method='raw')
310
310
311 @LoginRequired()
311 @LoginRequired()
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 'repository.admin')
313 'repository.admin')
314 def changeset_patch(self, revision):
314 def changeset_patch(self, revision):
315 return self._index(revision, method='patch')
315 return self._index(revision, method='patch')
316
316
317 @LoginRequired()
317 @LoginRequired()
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 'repository.admin')
319 'repository.admin')
320 def changeset_download(self, revision):
320 def changeset_download(self, revision):
321 return self._index(revision, method='download')
321 return self._index(revision, method='download')
322
322
323 @LoginRequired()
323 @LoginRequired()
324 @NotAnonymous()
324 @NotAnonymous()
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 'repository.admin')
326 'repository.admin')
327 @auth.CSRFRequired()
327 @auth.CSRFRequired()
328 @jsonify
328 @jsonify
329 def comment(self, repo_name, revision):
329 def comment(self, repo_name, revision):
330 commit_id = revision
330 commit_id = revision
331 status = request.POST.get('changeset_status', None)
331 status = request.POST.get('changeset_status', None)
332 text = request.POST.get('text')
332 text = request.POST.get('text')
333 if status:
333 if status:
334 text = text or (_('Status change %(transition_icon)s %(status)s')
334 text = text or (_('Status change %(transition_icon)s %(status)s')
335 % {'transition_icon': '>',
335 % {'transition_icon': '>',
336 'status': ChangesetStatus.get_status_lbl(status)})
336 'status': ChangesetStatus.get_status_lbl(status)})
337
337
338 multi_commit_ids = filter(
338 multi_commit_ids = filter(
339 lambda s: s not in ['', None],
339 lambda s: s not in ['', None],
340 request.POST.get('commit_ids', '').split(','),)
340 request.POST.get('commit_ids', '').split(','),)
341
341
342 commit_ids = multi_commit_ids or [commit_id]
342 commit_ids = multi_commit_ids or [commit_id]
343 comment = None
343 comment = None
344 for current_id in filter(None, commit_ids):
344 for current_id in filter(None, commit_ids):
345 c.co = comment = ChangesetCommentsModel().create(
345 c.co = comment = ChangesetCommentsModel().create(
346 text=text,
346 text=text,
347 repo=c.rhodecode_db_repo.repo_id,
347 repo=c.rhodecode_db_repo.repo_id,
348 user=c.rhodecode_user.user_id,
348 user=c.rhodecode_user.user_id,
349 revision=current_id,
349 revision=current_id,
350 f_path=request.POST.get('f_path'),
350 f_path=request.POST.get('f_path'),
351 line_no=request.POST.get('line'),
351 line_no=request.POST.get('line'),
352 status_change=(ChangesetStatus.get_status_lbl(status)
352 status_change=(ChangesetStatus.get_status_lbl(status)
353 if status else None),
353 if status else None),
354 status_change_type=status
354 status_change_type=status
355 )
355 )
356 # get status if set !
356 # get status if set !
357 if status:
357 if status:
358 # if latest status was from pull request and it's closed
358 # if latest status was from pull request and it's closed
359 # disallow changing status !
359 # disallow changing status !
360 # dont_allow_on_closed_pull_request = True !
360 # dont_allow_on_closed_pull_request = True !
361
361
362 try:
362 try:
363 ChangesetStatusModel().set_status(
363 ChangesetStatusModel().set_status(
364 c.rhodecode_db_repo.repo_id,
364 c.rhodecode_db_repo.repo_id,
365 status,
365 status,
366 c.rhodecode_user.user_id,
366 c.rhodecode_user.user_id,
367 comment,
367 comment,
368 revision=current_id,
368 revision=current_id,
369 dont_allow_on_closed_pull_request=True
369 dont_allow_on_closed_pull_request=True
370 )
370 )
371 except StatusChangeOnClosedPullRequestError:
371 except StatusChangeOnClosedPullRequestError:
372 msg = _('Changing the status of a commit associated with '
372 msg = _('Changing the status of a commit associated with '
373 'a closed pull request is not allowed')
373 'a closed pull request is not allowed')
374 log.exception(msg)
374 log.exception(msg)
375 h.flash(msg, category='warning')
375 h.flash(msg, category='warning')
376 return redirect(h.url(
376 return redirect(h.url(
377 'changeset_home', repo_name=repo_name,
377 'changeset_home', repo_name=repo_name,
378 revision=current_id))
378 revision=current_id))
379
379
380 # finalize, commit and redirect
380 # finalize, commit and redirect
381 Session().commit()
381 Session().commit()
382
382
383 data = {
383 data = {
384 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
384 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
385 }
385 }
386 if comment:
386 if comment:
387 data.update(comment.get_dict())
387 data.update(comment.get_dict())
388 data.update({'rendered_text':
388 data.update({'rendered_text':
389 render('changeset/changeset_comment_block.html')})
389 render('changeset/changeset_comment_block.html')})
390
390
391 return data
391 return data
392
392
393 @LoginRequired()
393 @LoginRequired()
394 @NotAnonymous()
394 @NotAnonymous()
395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 'repository.admin')
396 'repository.admin')
397 @auth.CSRFRequired()
397 @auth.CSRFRequired()
398 def preview_comment(self):
398 def preview_comment(self):
399 # Technically a CSRF token is not needed as no state changes with this
399 # Technically a CSRF token is not needed as no state changes with this
400 # call. However, as this is a POST is better to have it, so automated
400 # call. However, as this is a POST is better to have it, so automated
401 # tools don't flag it as potential CSRF.
401 # tools don't flag it as potential CSRF.
402 # Post is required because the payload could be bigger than the maximum
402 # Post is required because the payload could be bigger than the maximum
403 # allowed by GET.
403 # allowed by GET.
404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 raise HTTPBadRequest()
405 raise HTTPBadRequest()
406 text = request.POST.get('text')
406 text = request.POST.get('text')
407 renderer = request.POST.get('renderer') or 'rst'
407 renderer = request.POST.get('renderer') or 'rst'
408 if text:
408 if text:
409 return h.render(text, renderer=renderer, mentions=True)
409 return h.render(text, renderer=renderer, mentions=True)
410 return ''
410 return ''
411
411
412 @LoginRequired()
412 @LoginRequired()
413 @NotAnonymous()
413 @NotAnonymous()
414 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
414 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
415 'repository.admin')
415 'repository.admin')
416 @auth.CSRFRequired()
416 @auth.CSRFRequired()
417 @jsonify
417 @jsonify
418 def delete_comment(self, repo_name, comment_id):
418 def delete_comment(self, repo_name, comment_id):
419 comment = ChangesetComment.get(comment_id)
419 comment = ChangesetComment.get(comment_id)
420 owner = (comment.author.user_id == c.rhodecode_user.user_id)
420 owner = (comment.author.user_id == c.rhodecode_user.user_id)
421 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
421 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
422 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
422 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
423 ChangesetCommentsModel().delete(comment=comment)
423 ChangesetCommentsModel().delete(comment=comment)
424 Session().commit()
424 Session().commit()
425 return True
425 return True
426 else:
426 else:
427 raise HTTPForbidden()
427 raise HTTPForbidden()
428
428
429 @LoginRequired()
429 @LoginRequired()
430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 'repository.admin')
431 'repository.admin')
432 @jsonify
432 @jsonify
433 def changeset_info(self, repo_name, revision):
433 def changeset_info(self, repo_name, revision):
434 if request.is_xhr:
434 if request.is_xhr:
435 try:
435 try:
436 return c.rhodecode_repo.get_commit(commit_id=revision)
436 return c.rhodecode_repo.get_commit(commit_id=revision)
437 except CommitDoesNotExistError as e:
437 except CommitDoesNotExistError as e:
438 return EmptyCommit(message=str(e))
438 return EmptyCommit(message=str(e))
439 else:
439 else:
440 raise HTTPBadRequest()
440 raise HTTPBadRequest()
441
441
442 @LoginRequired()
442 @LoginRequired()
443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 'repository.admin')
444 'repository.admin')
445 @jsonify
445 @jsonify
446 def changeset_children(self, repo_name, revision):
446 def changeset_children(self, repo_name, revision):
447 if request.is_xhr:
447 if request.is_xhr:
448 commit = c.rhodecode_repo.get_commit(commit_id=revision)
448 commit = c.rhodecode_repo.get_commit(commit_id=revision)
449 result = {"results": commit.children}
449 result = {"results": commit.children}
450 return result
450 return result
451 else:
451 else:
452 raise HTTPBadRequest()
452 raise HTTPBadRequest()
453
453
454 @LoginRequired()
454 @LoginRequired()
455 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
455 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 'repository.admin')
456 'repository.admin')
457 @jsonify
457 @jsonify
458 def changeset_parents(self, repo_name, revision):
458 def changeset_parents(self, repo_name, revision):
459 if request.is_xhr:
459 if request.is_xhr:
460 commit = c.rhodecode_repo.get_commit(commit_id=revision)
460 commit = c.rhodecode_repo.get_commit(commit_id=revision)
461 result = {"results": commit.parents}
461 result = {"results": commit.parents}
462 return result
462 return result
463 else:
463 else:
464 raise HTTPBadRequest()
464 raise HTTPBadRequest()
@@ -1,899 +1,900 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24
24
25 import peppercorn
25 import peppercorn
26 import formencode
26 import formencode
27 import logging
27 import logging
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
35 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
38 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
41 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.compat import OrderedDict
46 from rhodecode.lib.compat import OrderedDict
47 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
49 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
50 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
51 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
51 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
52 NodeDoesNotExistError)
52 NodeDoesNotExistError)
53 from rhodecode.lib.diffs import LimitedDiffContainer
53 from rhodecode.lib.diffs import LimitedDiffContainer
54 from rhodecode.model.changeset_status import ChangesetStatusModel
54 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.comment import ChangesetCommentsModel
55 from rhodecode.model.comment import ChangesetCommentsModel
56 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
56 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
57 Repository
57 Repository
58 from rhodecode.model.forms import PullRequestForm
58 from rhodecode.model.forms import PullRequestForm
59 from rhodecode.model.meta import Session
59 from rhodecode.model.meta import Session
60 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.pull_request import PullRequestModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 class PullrequestsController(BaseRepoController):
65 class PullrequestsController(BaseRepoController):
66 def __before__(self):
66 def __before__(self):
67 super(PullrequestsController, self).__before__()
67 super(PullrequestsController, self).__before__()
68
68
69 def _load_compare_data(self, pull_request, inline_comments, enable_comments=True):
69 def _load_compare_data(self, pull_request, inline_comments, enable_comments=True):
70 """
70 """
71 Load context data needed for generating compare diff
71 Load context data needed for generating compare diff
72
72
73 :param pull_request: object related to the request
73 :param pull_request: object related to the request
74 :param enable_comments: flag to determine if comments are included
74 :param enable_comments: flag to determine if comments are included
75 """
75 """
76 source_repo = pull_request.source_repo
76 source_repo = pull_request.source_repo
77 source_ref_id = pull_request.source_ref_parts.commit_id
77 source_ref_id = pull_request.source_ref_parts.commit_id
78
78
79 target_repo = pull_request.target_repo
79 target_repo = pull_request.target_repo
80 target_ref_id = pull_request.target_ref_parts.commit_id
80 target_ref_id = pull_request.target_ref_parts.commit_id
81
81
82 # despite opening commits for bookmarks/branches/tags, we always
82 # despite opening commits for bookmarks/branches/tags, we always
83 # convert this to rev to prevent changes after bookmark or branch change
83 # convert this to rev to prevent changes after bookmark or branch change
84 c.source_ref_type = 'rev'
84 c.source_ref_type = 'rev'
85 c.source_ref = source_ref_id
85 c.source_ref = source_ref_id
86
86
87 c.target_ref_type = 'rev'
87 c.target_ref_type = 'rev'
88 c.target_ref = target_ref_id
88 c.target_ref = target_ref_id
89
89
90 c.source_repo = source_repo
90 c.source_repo = source_repo
91 c.target_repo = target_repo
91 c.target_repo = target_repo
92
92
93 c.fulldiff = bool(request.GET.get('fulldiff'))
93 c.fulldiff = bool(request.GET.get('fulldiff'))
94
94
95 # diff_limit is the old behavior, will cut off the whole diff
95 # diff_limit is the old behavior, will cut off the whole diff
96 # if the limit is applied otherwise will just hide the
96 # if the limit is applied otherwise will just hide the
97 # big files from the front-end
97 # big files from the front-end
98 diff_limit = self.cut_off_limit_diff
98 diff_limit = self.cut_off_limit_diff
99 file_limit = self.cut_off_limit_file
99 file_limit = self.cut_off_limit_file
100
100
101 pre_load = ["author", "branch", "date", "message"]
101 pre_load = ["author", "branch", "date", "message"]
102
102
103 c.commit_ranges = []
103 c.commit_ranges = []
104 source_commit = EmptyCommit()
104 source_commit = EmptyCommit()
105 target_commit = EmptyCommit()
105 target_commit = EmptyCommit()
106 c.missing_requirements = False
106 c.missing_requirements = False
107 try:
107 try:
108 c.commit_ranges = [
108 c.commit_ranges = [
109 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
109 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
110 for rev in pull_request.revisions]
110 for rev in pull_request.revisions]
111
111
112 c.statuses = source_repo.statuses(
112 c.statuses = source_repo.statuses(
113 [x.raw_id for x in c.commit_ranges])
113 [x.raw_id for x in c.commit_ranges])
114
114
115 target_commit = source_repo.get_commit(
115 target_commit = source_repo.get_commit(
116 commit_id=safe_str(target_ref_id))
116 commit_id=safe_str(target_ref_id))
117 source_commit = source_repo.get_commit(
117 source_commit = source_repo.get_commit(
118 commit_id=safe_str(source_ref_id))
118 commit_id=safe_str(source_ref_id))
119 except RepositoryRequirementError:
119 except RepositoryRequirementError:
120 c.missing_requirements = True
120 c.missing_requirements = True
121
121
122 c.changes = {}
122 c.changes = {}
123 c.missing_commits = False
123 c.missing_commits = False
124 if (c.missing_requirements or
124 if (c.missing_requirements or
125 isinstance(source_commit, EmptyCommit) or
125 isinstance(source_commit, EmptyCommit) or
126 source_commit == target_commit):
126 source_commit == target_commit):
127 _parsed = []
127 _parsed = []
128 c.missing_commits = True
128 c.missing_commits = True
129 else:
129 else:
130 vcs_diff = PullRequestModel().get_diff(pull_request)
130 vcs_diff = PullRequestModel().get_diff(pull_request)
131 diff_processor = diffs.DiffProcessor(
131 diff_processor = diffs.DiffProcessor(
132 vcs_diff, format='newdiff', diff_limit=diff_limit,
132 vcs_diff, format='newdiff', diff_limit=diff_limit,
133 file_limit=file_limit, show_full_diff=c.fulldiff)
133 file_limit=file_limit, show_full_diff=c.fulldiff)
134 _parsed = diff_processor.prepare()
134 _parsed = diff_processor.prepare()
135
135
136 commit_changes = OrderedDict()
136 commit_changes = OrderedDict()
137 _parsed = diff_processor.prepare()
137 _parsed = diff_processor.prepare()
138 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
138 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
139
139
140 _parsed = diff_processor.prepare()
140 _parsed = diff_processor.prepare()
141
141
142 def _node_getter(commit):
142 def _node_getter(commit):
143 def get_node(fname):
143 def get_node(fname):
144 try:
144 try:
145 return commit.get_node(fname)
145 return commit.get_node(fname)
146 except NodeDoesNotExistError:
146 except NodeDoesNotExistError:
147 return None
147 return None
148 return get_node
148 return get_node
149
149
150 c.diffset = codeblocks.DiffSet(
150 c.diffset = codeblocks.DiffSet(
151 repo_name=c.repo_name,
151 repo_name=c.repo_name,
152 source_node_getter=_node_getter(target_commit),
152 source_node_getter=_node_getter(target_commit),
153 target_node_getter=_node_getter(source_commit),
153 target_node_getter=_node_getter(source_commit),
154 comments=inline_comments
154 comments=inline_comments
155 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
155 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
156
156
157 c.included_files = []
157 c.included_files = []
158 c.deleted_files = []
158 c.deleted_files = []
159
159
160 for f in _parsed:
160 for f in _parsed:
161 st = f['stats']
161 st = f['stats']
162 fid = h.FID('', f['filename'])
162 fid = h.FID('', f['filename'])
163 c.included_files.append(f['filename'])
163 c.included_files.append(f['filename'])
164
164
165 def _extract_ordering(self, request):
165 def _extract_ordering(self, request):
166 column_index = safe_int(request.GET.get('order[0][column]'))
166 column_index = safe_int(request.GET.get('order[0][column]'))
167 order_dir = request.GET.get('order[0][dir]', 'desc')
167 order_dir = request.GET.get('order[0][dir]', 'desc')
168 order_by = request.GET.get(
168 order_by = request.GET.get(
169 'columns[%s][data][sort]' % column_index, 'name_raw')
169 'columns[%s][data][sort]' % column_index, 'name_raw')
170 return order_by, order_dir
170 return order_by, order_dir
171
171
172 @LoginRequired()
172 @LoginRequired()
173 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
173 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
174 'repository.admin')
174 'repository.admin')
175 @HasAcceptedRepoType('git', 'hg')
175 @HasAcceptedRepoType('git', 'hg')
176 def show_all(self, repo_name):
176 def show_all(self, repo_name):
177 # filter types
177 # filter types
178 c.active = 'open'
178 c.active = 'open'
179 c.source = str2bool(request.GET.get('source'))
179 c.source = str2bool(request.GET.get('source'))
180 c.closed = str2bool(request.GET.get('closed'))
180 c.closed = str2bool(request.GET.get('closed'))
181 c.my = str2bool(request.GET.get('my'))
181 c.my = str2bool(request.GET.get('my'))
182 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
182 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
183 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
183 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
184 c.repo_name = repo_name
184 c.repo_name = repo_name
185
185
186 opened_by = None
186 opened_by = None
187 if c.my:
187 if c.my:
188 c.active = 'my'
188 c.active = 'my'
189 opened_by = [c.rhodecode_user.user_id]
189 opened_by = [c.rhodecode_user.user_id]
190
190
191 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
191 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
192 if c.closed:
192 if c.closed:
193 c.active = 'closed'
193 c.active = 'closed'
194 statuses = [PullRequest.STATUS_CLOSED]
194 statuses = [PullRequest.STATUS_CLOSED]
195
195
196 if c.awaiting_review and not c.source:
196 if c.awaiting_review and not c.source:
197 c.active = 'awaiting'
197 c.active = 'awaiting'
198 if c.source and not c.awaiting_review:
198 if c.source and not c.awaiting_review:
199 c.active = 'source'
199 c.active = 'source'
200 if c.awaiting_my_review:
200 if c.awaiting_my_review:
201 c.active = 'awaiting_my'
201 c.active = 'awaiting_my'
202
202
203 data = self._get_pull_requests_list(
203 data = self._get_pull_requests_list(
204 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
204 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
205 if not request.is_xhr:
205 if not request.is_xhr:
206 c.data = json.dumps(data['data'])
206 c.data = json.dumps(data['data'])
207 c.records_total = data['recordsTotal']
207 c.records_total = data['recordsTotal']
208 return render('/pullrequests/pullrequests.html')
208 return render('/pullrequests/pullrequests.html')
209 else:
209 else:
210 return json.dumps(data)
210 return json.dumps(data)
211
211
212 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
212 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
213 # pagination
213 # pagination
214 start = safe_int(request.GET.get('start'), 0)
214 start = safe_int(request.GET.get('start'), 0)
215 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
215 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
216 order_by, order_dir = self._extract_ordering(request)
216 order_by, order_dir = self._extract_ordering(request)
217
217
218 if c.awaiting_review:
218 if c.awaiting_review:
219 pull_requests = PullRequestModel().get_awaiting_review(
219 pull_requests = PullRequestModel().get_awaiting_review(
220 repo_name, source=c.source, opened_by=opened_by,
220 repo_name, source=c.source, opened_by=opened_by,
221 statuses=statuses, offset=start, length=length,
221 statuses=statuses, offset=start, length=length,
222 order_by=order_by, order_dir=order_dir)
222 order_by=order_by, order_dir=order_dir)
223 pull_requests_total_count = PullRequestModel(
223 pull_requests_total_count = PullRequestModel(
224 ).count_awaiting_review(
224 ).count_awaiting_review(
225 repo_name, source=c.source, statuses=statuses,
225 repo_name, source=c.source, statuses=statuses,
226 opened_by=opened_by)
226 opened_by=opened_by)
227 elif c.awaiting_my_review:
227 elif c.awaiting_my_review:
228 pull_requests = PullRequestModel().get_awaiting_my_review(
228 pull_requests = PullRequestModel().get_awaiting_my_review(
229 repo_name, source=c.source, opened_by=opened_by,
229 repo_name, source=c.source, opened_by=opened_by,
230 user_id=c.rhodecode_user.user_id, statuses=statuses,
230 user_id=c.rhodecode_user.user_id, statuses=statuses,
231 offset=start, length=length, order_by=order_by,
231 offset=start, length=length, order_by=order_by,
232 order_dir=order_dir)
232 order_dir=order_dir)
233 pull_requests_total_count = PullRequestModel(
233 pull_requests_total_count = PullRequestModel(
234 ).count_awaiting_my_review(
234 ).count_awaiting_my_review(
235 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
235 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
236 statuses=statuses, opened_by=opened_by)
236 statuses=statuses, opened_by=opened_by)
237 else:
237 else:
238 pull_requests = PullRequestModel().get_all(
238 pull_requests = PullRequestModel().get_all(
239 repo_name, source=c.source, opened_by=opened_by,
239 repo_name, source=c.source, opened_by=opened_by,
240 statuses=statuses, offset=start, length=length,
240 statuses=statuses, offset=start, length=length,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242 pull_requests_total_count = PullRequestModel().count_all(
242 pull_requests_total_count = PullRequestModel().count_all(
243 repo_name, source=c.source, statuses=statuses,
243 repo_name, source=c.source, statuses=statuses,
244 opened_by=opened_by)
244 opened_by=opened_by)
245
245
246 from rhodecode.lib.utils import PartialRenderer
246 from rhodecode.lib.utils import PartialRenderer
247 _render = PartialRenderer('data_table/_dt_elements.html')
247 _render = PartialRenderer('data_table/_dt_elements.html')
248 data = []
248 data = []
249 for pr in pull_requests:
249 for pr in pull_requests:
250 comments = ChangesetCommentsModel().get_all_comments(
250 comments = ChangesetCommentsModel().get_all_comments(
251 c.rhodecode_db_repo.repo_id, pull_request=pr)
251 c.rhodecode_db_repo.repo_id, pull_request=pr)
252
252
253 data.append({
253 data.append({
254 'name': _render('pullrequest_name',
254 'name': _render('pullrequest_name',
255 pr.pull_request_id, pr.target_repo.repo_name),
255 pr.pull_request_id, pr.target_repo.repo_name),
256 'name_raw': pr.pull_request_id,
256 'name_raw': pr.pull_request_id,
257 'status': _render('pullrequest_status',
257 'status': _render('pullrequest_status',
258 pr.calculated_review_status()),
258 pr.calculated_review_status()),
259 'title': _render(
259 'title': _render(
260 'pullrequest_title', pr.title, pr.description),
260 'pullrequest_title', pr.title, pr.description),
261 'description': h.escape(pr.description),
261 'description': h.escape(pr.description),
262 'updated_on': _render('pullrequest_updated_on',
262 'updated_on': _render('pullrequest_updated_on',
263 h.datetime_to_time(pr.updated_on)),
263 h.datetime_to_time(pr.updated_on)),
264 'updated_on_raw': h.datetime_to_time(pr.updated_on),
264 'updated_on_raw': h.datetime_to_time(pr.updated_on),
265 'created_on': _render('pullrequest_updated_on',
265 'created_on': _render('pullrequest_updated_on',
266 h.datetime_to_time(pr.created_on)),
266 h.datetime_to_time(pr.created_on)),
267 'created_on_raw': h.datetime_to_time(pr.created_on),
267 'created_on_raw': h.datetime_to_time(pr.created_on),
268 'author': _render('pullrequest_author',
268 'author': _render('pullrequest_author',
269 pr.author.full_contact, ),
269 pr.author.full_contact, ),
270 'author_raw': pr.author.full_name,
270 'author_raw': pr.author.full_name,
271 'comments': _render('pullrequest_comments', len(comments)),
271 'comments': _render('pullrequest_comments', len(comments)),
272 'comments_raw': len(comments),
272 'comments_raw': len(comments),
273 'closed': pr.is_closed(),
273 'closed': pr.is_closed(),
274 })
274 })
275 # json used to render the grid
275 # json used to render the grid
276 data = ({
276 data = ({
277 'data': data,
277 'data': data,
278 'recordsTotal': pull_requests_total_count,
278 'recordsTotal': pull_requests_total_count,
279 'recordsFiltered': pull_requests_total_count,
279 'recordsFiltered': pull_requests_total_count,
280 })
280 })
281 return data
281 return data
282
282
283 @LoginRequired()
283 @LoginRequired()
284 @NotAnonymous()
284 @NotAnonymous()
285 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
285 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
286 'repository.admin')
286 'repository.admin')
287 @HasAcceptedRepoType('git', 'hg')
287 @HasAcceptedRepoType('git', 'hg')
288 def index(self):
288 def index(self):
289 source_repo = c.rhodecode_db_repo
289 source_repo = c.rhodecode_db_repo
290
290
291 try:
291 try:
292 source_repo.scm_instance().get_commit()
292 source_repo.scm_instance().get_commit()
293 except EmptyRepositoryError:
293 except EmptyRepositoryError:
294 h.flash(h.literal(_('There are no commits yet')),
294 h.flash(h.literal(_('There are no commits yet')),
295 category='warning')
295 category='warning')
296 redirect(url('summary_home', repo_name=source_repo.repo_name))
296 redirect(url('summary_home', repo_name=source_repo.repo_name))
297
297
298 commit_id = request.GET.get('commit')
298 commit_id = request.GET.get('commit')
299 branch_ref = request.GET.get('branch')
299 branch_ref = request.GET.get('branch')
300 bookmark_ref = request.GET.get('bookmark')
300 bookmark_ref = request.GET.get('bookmark')
301
301
302 try:
302 try:
303 source_repo_data = PullRequestModel().generate_repo_data(
303 source_repo_data = PullRequestModel().generate_repo_data(
304 source_repo, commit_id=commit_id,
304 source_repo, commit_id=commit_id,
305 branch=branch_ref, bookmark=bookmark_ref)
305 branch=branch_ref, bookmark=bookmark_ref)
306 except CommitDoesNotExistError as e:
306 except CommitDoesNotExistError as e:
307 log.exception(e)
307 log.exception(e)
308 h.flash(_('Commit does not exist'), 'error')
308 h.flash(_('Commit does not exist'), 'error')
309 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
309 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
310
310
311 default_target_repo = source_repo
311 default_target_repo = source_repo
312
312
313 if source_repo.parent:
313 if source_repo.parent:
314 parent_vcs_obj = source_repo.parent.scm_instance()
314 parent_vcs_obj = source_repo.parent.scm_instance()
315 if parent_vcs_obj and not parent_vcs_obj.is_empty():
315 if parent_vcs_obj and not parent_vcs_obj.is_empty():
316 # change default if we have a parent repo
316 # change default if we have a parent repo
317 default_target_repo = source_repo.parent
317 default_target_repo = source_repo.parent
318
318
319 target_repo_data = PullRequestModel().generate_repo_data(
319 target_repo_data = PullRequestModel().generate_repo_data(
320 default_target_repo)
320 default_target_repo)
321
321
322 selected_source_ref = source_repo_data['refs']['selected_ref']
322 selected_source_ref = source_repo_data['refs']['selected_ref']
323
323
324 title_source_ref = selected_source_ref.split(':', 2)[1]
324 title_source_ref = selected_source_ref.split(':', 2)[1]
325 c.default_title = PullRequestModel().generate_pullrequest_title(
325 c.default_title = PullRequestModel().generate_pullrequest_title(
326 source=source_repo.repo_name,
326 source=source_repo.repo_name,
327 source_ref=title_source_ref,
327 source_ref=title_source_ref,
328 target=default_target_repo.repo_name
328 target=default_target_repo.repo_name
329 )
329 )
330
330
331 c.default_repo_data = {
331 c.default_repo_data = {
332 'source_repo_name': source_repo.repo_name,
332 'source_repo_name': source_repo.repo_name,
333 'source_refs_json': json.dumps(source_repo_data),
333 'source_refs_json': json.dumps(source_repo_data),
334 'target_repo_name': default_target_repo.repo_name,
334 'target_repo_name': default_target_repo.repo_name,
335 'target_refs_json': json.dumps(target_repo_data),
335 'target_refs_json': json.dumps(target_repo_data),
336 }
336 }
337 c.default_source_ref = selected_source_ref
337 c.default_source_ref = selected_source_ref
338
338
339 return render('/pullrequests/pullrequest.html')
339 return render('/pullrequests/pullrequest.html')
340
340
341 @LoginRequired()
341 @LoginRequired()
342 @NotAnonymous()
342 @NotAnonymous()
343 @XHRRequired()
343 @XHRRequired()
344 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
344 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
345 'repository.admin')
345 'repository.admin')
346 @jsonify
346 @jsonify
347 def get_repo_refs(self, repo_name, target_repo_name):
347 def get_repo_refs(self, repo_name, target_repo_name):
348 repo = Repository.get_by_repo_name(target_repo_name)
348 repo = Repository.get_by_repo_name(target_repo_name)
349 if not repo:
349 if not repo:
350 raise HTTPNotFound
350 raise HTTPNotFound
351 return PullRequestModel().generate_repo_data(repo)
351 return PullRequestModel().generate_repo_data(repo)
352
352
353 @LoginRequired()
353 @LoginRequired()
354 @NotAnonymous()
354 @NotAnonymous()
355 @XHRRequired()
355 @XHRRequired()
356 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
357 'repository.admin')
357 'repository.admin')
358 @jsonify
358 @jsonify
359 def get_repo_destinations(self, repo_name):
359 def get_repo_destinations(self, repo_name):
360 repo = Repository.get_by_repo_name(repo_name)
360 repo = Repository.get_by_repo_name(repo_name)
361 if not repo:
361 if not repo:
362 raise HTTPNotFound
362 raise HTTPNotFound
363 filter_query = request.GET.get('query')
363 filter_query = request.GET.get('query')
364
364
365 query = Repository.query() \
365 query = Repository.query() \
366 .order_by(func.length(Repository.repo_name)) \
366 .order_by(func.length(Repository.repo_name)) \
367 .filter(or_(
367 .filter(or_(
368 Repository.repo_name == repo.repo_name,
368 Repository.repo_name == repo.repo_name,
369 Repository.fork_id == repo.repo_id))
369 Repository.fork_id == repo.repo_id))
370
370
371 if filter_query:
371 if filter_query:
372 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
372 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
373 query = query.filter(
373 query = query.filter(
374 Repository.repo_name.ilike(ilike_expression))
374 Repository.repo_name.ilike(ilike_expression))
375
375
376 add_parent = False
376 add_parent = False
377 if repo.parent:
377 if repo.parent:
378 if filter_query in repo.parent.repo_name:
378 if filter_query in repo.parent.repo_name:
379 parent_vcs_obj = repo.parent.scm_instance()
379 parent_vcs_obj = repo.parent.scm_instance()
380 if parent_vcs_obj and not parent_vcs_obj.is_empty():
380 if parent_vcs_obj and not parent_vcs_obj.is_empty():
381 add_parent = True
381 add_parent = True
382
382
383 limit = 20 - 1 if add_parent else 20
383 limit = 20 - 1 if add_parent else 20
384 all_repos = query.limit(limit).all()
384 all_repos = query.limit(limit).all()
385 if add_parent:
385 if add_parent:
386 all_repos += [repo.parent]
386 all_repos += [repo.parent]
387
387
388 repos = []
388 repos = []
389 for obj in self.scm_model.get_repos(all_repos):
389 for obj in self.scm_model.get_repos(all_repos):
390 repos.append({
390 repos.append({
391 'id': obj['name'],
391 'id': obj['name'],
392 'text': obj['name'],
392 'text': obj['name'],
393 'type': 'repo',
393 'type': 'repo',
394 'obj': obj['dbrepo']
394 'obj': obj['dbrepo']
395 })
395 })
396
396
397 data = {
397 data = {
398 'more': False,
398 'more': False,
399 'results': [{
399 'results': [{
400 'text': _('Repositories'),
400 'text': _('Repositories'),
401 'children': repos
401 'children': repos
402 }] if repos else []
402 }] if repos else []
403 }
403 }
404 return data
404 return data
405
405
406 @LoginRequired()
406 @LoginRequired()
407 @NotAnonymous()
407 @NotAnonymous()
408 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
408 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
409 'repository.admin')
409 'repository.admin')
410 @HasAcceptedRepoType('git', 'hg')
410 @HasAcceptedRepoType('git', 'hg')
411 @auth.CSRFRequired()
411 @auth.CSRFRequired()
412 def create(self, repo_name):
412 def create(self, repo_name):
413 repo = Repository.get_by_repo_name(repo_name)
413 repo = Repository.get_by_repo_name(repo_name)
414 if not repo:
414 if not repo:
415 raise HTTPNotFound
415 raise HTTPNotFound
416
416
417 controls = peppercorn.parse(request.POST.items())
417 controls = peppercorn.parse(request.POST.items())
418
418
419 try:
419 try:
420 _form = PullRequestForm(repo.repo_id)().to_python(controls)
420 _form = PullRequestForm(repo.repo_id)().to_python(controls)
421 except formencode.Invalid as errors:
421 except formencode.Invalid as errors:
422 if errors.error_dict.get('revisions'):
422 if errors.error_dict.get('revisions'):
423 msg = 'Revisions: %s' % errors.error_dict['revisions']
423 msg = 'Revisions: %s' % errors.error_dict['revisions']
424 elif errors.error_dict.get('pullrequest_title'):
424 elif errors.error_dict.get('pullrequest_title'):
425 msg = _('Pull request requires a title with min. 3 chars')
425 msg = _('Pull request requires a title with min. 3 chars')
426 else:
426 else:
427 msg = _('Error creating pull request: {}').format(errors)
427 msg = _('Error creating pull request: {}').format(errors)
428 log.exception(msg)
428 log.exception(msg)
429 h.flash(msg, 'error')
429 h.flash(msg, 'error')
430
430
431 # would rather just go back to form ...
431 # would rather just go back to form ...
432 return redirect(url('pullrequest_home', repo_name=repo_name))
432 return redirect(url('pullrequest_home', repo_name=repo_name))
433
433
434 source_repo = _form['source_repo']
434 source_repo = _form['source_repo']
435 source_ref = _form['source_ref']
435 source_ref = _form['source_ref']
436 target_repo = _form['target_repo']
436 target_repo = _form['target_repo']
437 target_ref = _form['target_ref']
437 target_ref = _form['target_ref']
438 commit_ids = _form['revisions'][::-1]
438 commit_ids = _form['revisions'][::-1]
439 reviewers = [
439 reviewers = [
440 (r['user_id'], r['reasons']) for r in _form['review_members']]
440 (r['user_id'], r['reasons']) for r in _form['review_members']]
441
441
442 # find the ancestor for this pr
442 # find the ancestor for this pr
443 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
443 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
444 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
444 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
445
445
446 source_scm = source_db_repo.scm_instance()
446 source_scm = source_db_repo.scm_instance()
447 target_scm = target_db_repo.scm_instance()
447 target_scm = target_db_repo.scm_instance()
448
448
449 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
449 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
450 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
450 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
451
451
452 ancestor = source_scm.get_common_ancestor(
452 ancestor = source_scm.get_common_ancestor(
453 source_commit.raw_id, target_commit.raw_id, target_scm)
453 source_commit.raw_id, target_commit.raw_id, target_scm)
454
454
455 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
455 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
456 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
456 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
457
457
458 pullrequest_title = _form['pullrequest_title']
458 pullrequest_title = _form['pullrequest_title']
459 title_source_ref = source_ref.split(':', 2)[1]
459 title_source_ref = source_ref.split(':', 2)[1]
460 if not pullrequest_title:
460 if not pullrequest_title:
461 pullrequest_title = PullRequestModel().generate_pullrequest_title(
461 pullrequest_title = PullRequestModel().generate_pullrequest_title(
462 source=source_repo,
462 source=source_repo,
463 source_ref=title_source_ref,
463 source_ref=title_source_ref,
464 target=target_repo
464 target=target_repo
465 )
465 )
466
466
467 description = _form['pullrequest_desc']
467 description = _form['pullrequest_desc']
468 try:
468 try:
469 pull_request = PullRequestModel().create(
469 pull_request = PullRequestModel().create(
470 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
470 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
471 target_ref, commit_ids, reviewers, pullrequest_title,
471 target_ref, commit_ids, reviewers, pullrequest_title,
472 description
472 description
473 )
473 )
474 Session().commit()
474 Session().commit()
475 h.flash(_('Successfully opened new pull request'),
475 h.flash(_('Successfully opened new pull request'),
476 category='success')
476 category='success')
477 except Exception as e:
477 except Exception as e:
478 msg = _('Error occurred during sending pull request')
478 msg = _('Error occurred during sending pull request')
479 log.exception(msg)
479 log.exception(msg)
480 h.flash(msg, category='error')
480 h.flash(msg, category='error')
481 return redirect(url('pullrequest_home', repo_name=repo_name))
481 return redirect(url('pullrequest_home', repo_name=repo_name))
482
482
483 return redirect(url('pullrequest_show', repo_name=target_repo,
483 return redirect(url('pullrequest_show', repo_name=target_repo,
484 pull_request_id=pull_request.pull_request_id))
484 pull_request_id=pull_request.pull_request_id))
485
485
486 @LoginRequired()
486 @LoginRequired()
487 @NotAnonymous()
487 @NotAnonymous()
488 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
488 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
489 'repository.admin')
489 'repository.admin')
490 @auth.CSRFRequired()
490 @auth.CSRFRequired()
491 @jsonify
491 @jsonify
492 def update(self, repo_name, pull_request_id):
492 def update(self, repo_name, pull_request_id):
493 pull_request_id = safe_int(pull_request_id)
493 pull_request_id = safe_int(pull_request_id)
494 pull_request = PullRequest.get_or_404(pull_request_id)
494 pull_request = PullRequest.get_or_404(pull_request_id)
495 # only owner or admin can update it
495 # only owner or admin can update it
496 allowed_to_update = PullRequestModel().check_user_update(
496 allowed_to_update = PullRequestModel().check_user_update(
497 pull_request, c.rhodecode_user)
497 pull_request, c.rhodecode_user)
498 if allowed_to_update:
498 if allowed_to_update:
499 controls = peppercorn.parse(request.POST.items())
499 controls = peppercorn.parse(request.POST.items())
500
500
501 if 'review_members' in controls:
501 if 'review_members' in controls:
502 self._update_reviewers(
502 self._update_reviewers(
503 pull_request_id, controls['review_members'])
503 pull_request_id, controls['review_members'])
504 elif str2bool(request.POST.get('update_commits', 'false')):
504 elif str2bool(request.POST.get('update_commits', 'false')):
505 self._update_commits(pull_request)
505 self._update_commits(pull_request)
506 elif str2bool(request.POST.get('close_pull_request', 'false')):
506 elif str2bool(request.POST.get('close_pull_request', 'false')):
507 self._reject_close(pull_request)
507 self._reject_close(pull_request)
508 elif str2bool(request.POST.get('edit_pull_request', 'false')):
508 elif str2bool(request.POST.get('edit_pull_request', 'false')):
509 self._edit_pull_request(pull_request)
509 self._edit_pull_request(pull_request)
510 else:
510 else:
511 raise HTTPBadRequest()
511 raise HTTPBadRequest()
512 return True
512 return True
513 raise HTTPForbidden()
513 raise HTTPForbidden()
514
514
515 def _edit_pull_request(self, pull_request):
515 def _edit_pull_request(self, pull_request):
516 try:
516 try:
517 PullRequestModel().edit(
517 PullRequestModel().edit(
518 pull_request, request.POST.get('title'),
518 pull_request, request.POST.get('title'),
519 request.POST.get('description'))
519 request.POST.get('description'))
520 except ValueError:
520 except ValueError:
521 msg = _(u'Cannot update closed pull requests.')
521 msg = _(u'Cannot update closed pull requests.')
522 h.flash(msg, category='error')
522 h.flash(msg, category='error')
523 return
523 return
524 else:
524 else:
525 Session().commit()
525 Session().commit()
526
526
527 msg = _(u'Pull request title & description updated.')
527 msg = _(u'Pull request title & description updated.')
528 h.flash(msg, category='success')
528 h.flash(msg, category='success')
529 return
529 return
530
530
531 def _update_commits(self, pull_request):
531 def _update_commits(self, pull_request):
532 resp = PullRequestModel().update_commits(pull_request)
532 resp = PullRequestModel().update_commits(pull_request)
533
533
534 if resp.executed:
534 if resp.executed:
535 msg = _(
535 msg = _(
536 u'Pull request updated to "{source_commit_id}" with '
536 u'Pull request updated to "{source_commit_id}" with '
537 u'{count_added} added, {count_removed} removed commits.')
537 u'{count_added} added, {count_removed} removed commits.')
538 msg = msg.format(
538 msg = msg.format(
539 source_commit_id=pull_request.source_ref_parts.commit_id,
539 source_commit_id=pull_request.source_ref_parts.commit_id,
540 count_added=len(resp.changes.added),
540 count_added=len(resp.changes.added),
541 count_removed=len(resp.changes.removed))
541 count_removed=len(resp.changes.removed))
542 h.flash(msg, category='success')
542 h.flash(msg, category='success')
543
543
544 registry = get_current_registry()
544 registry = get_current_registry()
545 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
545 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
546 channelstream_config = rhodecode_plugins.get('channelstream', {})
546 channelstream_config = rhodecode_plugins.get('channelstream', {})
547 if channelstream_config.get('enabled'):
547 if channelstream_config.get('enabled'):
548 message = msg + (
548 message = msg + (
549 ' - <a onclick="window.location.reload()">'
549 ' - <a onclick="window.location.reload()">'
550 '<strong>{}</strong></a>'.format(_('Reload page')))
550 '<strong>{}</strong></a>'.format(_('Reload page')))
551 channel = '/repo${}$/pr/{}'.format(
551 channel = '/repo${}$/pr/{}'.format(
552 pull_request.target_repo.repo_name,
552 pull_request.target_repo.repo_name,
553 pull_request.pull_request_id
553 pull_request.pull_request_id
554 )
554 )
555 payload = {
555 payload = {
556 'type': 'message',
556 'type': 'message',
557 'user': 'system',
557 'user': 'system',
558 'exclude_users': [request.user.username],
558 'exclude_users': [request.user.username],
559 'channel': channel,
559 'channel': channel,
560 'message': {
560 'message': {
561 'message': message,
561 'message': message,
562 'level': 'success',
562 'level': 'success',
563 'topic': '/notifications'
563 'topic': '/notifications'
564 }
564 }
565 }
565 }
566 channelstream_request(
566 channelstream_request(
567 channelstream_config, [payload], '/message',
567 channelstream_config, [payload], '/message',
568 raise_exc=False)
568 raise_exc=False)
569 else:
569 else:
570 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
570 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
571 warning_reasons = [
571 warning_reasons = [
572 UpdateFailureReason.NO_CHANGE,
572 UpdateFailureReason.NO_CHANGE,
573 UpdateFailureReason.WRONG_REF_TPYE,
573 UpdateFailureReason.WRONG_REF_TPYE,
574 ]
574 ]
575 category = 'warning' if resp.reason in warning_reasons else 'error'
575 category = 'warning' if resp.reason in warning_reasons else 'error'
576 h.flash(msg, category=category)
576 h.flash(msg, category=category)
577
577
578 @auth.CSRFRequired()
578 @auth.CSRFRequired()
579 @LoginRequired()
579 @LoginRequired()
580 @NotAnonymous()
580 @NotAnonymous()
581 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
581 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
582 'repository.admin')
582 'repository.admin')
583 def merge(self, repo_name, pull_request_id):
583 def merge(self, repo_name, pull_request_id):
584 """
584 """
585 POST /{repo_name}/pull-request/{pull_request_id}
585 POST /{repo_name}/pull-request/{pull_request_id}
586
586
587 Merge will perform a server-side merge of the specified
587 Merge will perform a server-side merge of the specified
588 pull request, if the pull request is approved and mergeable.
588 pull request, if the pull request is approved and mergeable.
589 After succesfull merging, the pull request is automatically
589 After succesfull merging, the pull request is automatically
590 closed, with a relevant comment.
590 closed, with a relevant comment.
591 """
591 """
592 pull_request_id = safe_int(pull_request_id)
592 pull_request_id = safe_int(pull_request_id)
593 pull_request = PullRequest.get_or_404(pull_request_id)
593 pull_request = PullRequest.get_or_404(pull_request_id)
594 user = c.rhodecode_user
594 user = c.rhodecode_user
595
595
596 if self._meets_merge_pre_conditions(pull_request, user):
596 if self._meets_merge_pre_conditions(pull_request, user):
597 log.debug("Pre-conditions checked, trying to merge.")
597 log.debug("Pre-conditions checked, trying to merge.")
598 extras = vcs_operation_context(
598 extras = vcs_operation_context(
599 request.environ, repo_name=pull_request.target_repo.repo_name,
599 request.environ, repo_name=pull_request.target_repo.repo_name,
600 username=user.username, action='push',
600 username=user.username, action='push',
601 scm=pull_request.target_repo.repo_type)
601 scm=pull_request.target_repo.repo_type)
602 self._merge_pull_request(pull_request, user, extras)
602 self._merge_pull_request(pull_request, user, extras)
603
603
604 return redirect(url(
604 return redirect(url(
605 'pullrequest_show',
605 'pullrequest_show',
606 repo_name=pull_request.target_repo.repo_name,
606 repo_name=pull_request.target_repo.repo_name,
607 pull_request_id=pull_request.pull_request_id))
607 pull_request_id=pull_request.pull_request_id))
608
608
609 def _meets_merge_pre_conditions(self, pull_request, user):
609 def _meets_merge_pre_conditions(self, pull_request, user):
610 if not PullRequestModel().check_user_merge(pull_request, user):
610 if not PullRequestModel().check_user_merge(pull_request, user):
611 raise HTTPForbidden()
611 raise HTTPForbidden()
612
612
613 merge_status, msg = PullRequestModel().merge_status(pull_request)
613 merge_status, msg = PullRequestModel().merge_status(pull_request)
614 if not merge_status:
614 if not merge_status:
615 log.debug("Cannot merge, not mergeable.")
615 log.debug("Cannot merge, not mergeable.")
616 h.flash(msg, category='error')
616 h.flash(msg, category='error')
617 return False
617 return False
618
618
619 if (pull_request.calculated_review_status()
619 if (pull_request.calculated_review_status()
620 is not ChangesetStatus.STATUS_APPROVED):
620 is not ChangesetStatus.STATUS_APPROVED):
621 log.debug("Cannot merge, approval is pending.")
621 log.debug("Cannot merge, approval is pending.")
622 msg = _('Pull request reviewer approval is pending.')
622 msg = _('Pull request reviewer approval is pending.')
623 h.flash(msg, category='error')
623 h.flash(msg, category='error')
624 return False
624 return False
625 return True
625 return True
626
626
627 def _merge_pull_request(self, pull_request, user, extras):
627 def _merge_pull_request(self, pull_request, user, extras):
628 merge_resp = PullRequestModel().merge(
628 merge_resp = PullRequestModel().merge(
629 pull_request, user, extras=extras)
629 pull_request, user, extras=extras)
630
630
631 if merge_resp.executed:
631 if merge_resp.executed:
632 log.debug("The merge was successful, closing the pull request.")
632 log.debug("The merge was successful, closing the pull request.")
633 PullRequestModel().close_pull_request(
633 PullRequestModel().close_pull_request(
634 pull_request.pull_request_id, user)
634 pull_request.pull_request_id, user)
635 Session().commit()
635 Session().commit()
636 msg = _('Pull request was successfully merged and closed.')
636 msg = _('Pull request was successfully merged and closed.')
637 h.flash(msg, category='success')
637 h.flash(msg, category='success')
638 else:
638 else:
639 log.debug(
639 log.debug(
640 "The merge was not successful. Merge response: %s",
640 "The merge was not successful. Merge response: %s",
641 merge_resp)
641 merge_resp)
642 msg = PullRequestModel().merge_status_message(
642 msg = PullRequestModel().merge_status_message(
643 merge_resp.failure_reason)
643 merge_resp.failure_reason)
644 h.flash(msg, category='error')
644 h.flash(msg, category='error')
645
645
646 def _update_reviewers(self, pull_request_id, review_members):
646 def _update_reviewers(self, pull_request_id, review_members):
647 reviewers = [
647 reviewers = [
648 (int(r['user_id']), r['reasons']) for r in review_members]
648 (int(r['user_id']), r['reasons']) for r in review_members]
649 PullRequestModel().update_reviewers(pull_request_id, reviewers)
649 PullRequestModel().update_reviewers(pull_request_id, reviewers)
650 Session().commit()
650 Session().commit()
651
651
652 def _reject_close(self, pull_request):
652 def _reject_close(self, pull_request):
653 if pull_request.is_closed():
653 if pull_request.is_closed():
654 raise HTTPForbidden()
654 raise HTTPForbidden()
655
655
656 PullRequestModel().close_pull_request_with_comment(
656 PullRequestModel().close_pull_request_with_comment(
657 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
657 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
658 Session().commit()
658 Session().commit()
659
659
660 @LoginRequired()
660 @LoginRequired()
661 @NotAnonymous()
661 @NotAnonymous()
662 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
662 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
663 'repository.admin')
663 'repository.admin')
664 @auth.CSRFRequired()
664 @auth.CSRFRequired()
665 @jsonify
665 @jsonify
666 def delete(self, repo_name, pull_request_id):
666 def delete(self, repo_name, pull_request_id):
667 pull_request_id = safe_int(pull_request_id)
667 pull_request_id = safe_int(pull_request_id)
668 pull_request = PullRequest.get_or_404(pull_request_id)
668 pull_request = PullRequest.get_or_404(pull_request_id)
669 # only owner can delete it !
669 # only owner can delete it !
670 if pull_request.author.user_id == c.rhodecode_user.user_id:
670 if pull_request.author.user_id == c.rhodecode_user.user_id:
671 PullRequestModel().delete(pull_request)
671 PullRequestModel().delete(pull_request)
672 Session().commit()
672 Session().commit()
673 h.flash(_('Successfully deleted pull request'),
673 h.flash(_('Successfully deleted pull request'),
674 category='success')
674 category='success')
675 return redirect(url('my_account_pullrequests'))
675 return redirect(url('my_account_pullrequests'))
676 raise HTTPForbidden()
676 raise HTTPForbidden()
677
677
678 @LoginRequired()
678 @LoginRequired()
679 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
679 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
680 'repository.admin')
680 'repository.admin')
681 def show(self, repo_name, pull_request_id):
681 def show(self, repo_name, pull_request_id):
682 pull_request_id = safe_int(pull_request_id)
682 pull_request_id = safe_int(pull_request_id)
683 c.pull_request = PullRequest.get_or_404(pull_request_id)
683 c.pull_request = PullRequest.get_or_404(pull_request_id)
684
684
685 c.template_context['pull_request_data']['pull_request_id'] = \
685 c.template_context['pull_request_data']['pull_request_id'] = \
686 pull_request_id
686 pull_request_id
687
687
688 # pull_requests repo_name we opened it against
688 # pull_requests repo_name we opened it against
689 # ie. target_repo must match
689 # ie. target_repo must match
690 if repo_name != c.pull_request.target_repo.repo_name:
690 if repo_name != c.pull_request.target_repo.repo_name:
691 raise HTTPNotFound
691 raise HTTPNotFound
692
692
693 c.allowed_to_change_status = PullRequestModel(). \
693 c.allowed_to_change_status = PullRequestModel(). \
694 check_user_change_status(c.pull_request, c.rhodecode_user)
694 check_user_change_status(c.pull_request, c.rhodecode_user)
695 c.allowed_to_update = PullRequestModel().check_user_update(
695 c.allowed_to_update = PullRequestModel().check_user_update(
696 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
696 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
697 c.allowed_to_merge = PullRequestModel().check_user_merge(
697 c.allowed_to_merge = PullRequestModel().check_user_merge(
698 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
698 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
699 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
699 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
700 c.pull_request)
700 c.pull_request)
701 c.allowed_to_delete = PullRequestModel().check_user_delete(
701 c.allowed_to_delete = PullRequestModel().check_user_delete(
702 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
702 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
703
703
704 cc_model = ChangesetCommentsModel()
704 cc_model = ChangesetCommentsModel()
705
705
706 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
706 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
707
707
708 c.pull_request_review_status = c.pull_request.calculated_review_status()
708 c.pull_request_review_status = c.pull_request.calculated_review_status()
709 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
709 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
710 c.pull_request)
710 c.pull_request)
711 c.approval_msg = None
711 c.approval_msg = None
712 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
712 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
713 c.approval_msg = _('Reviewer approval is pending.')
713 c.approval_msg = _('Reviewer approval is pending.')
714 c.pr_merge_status = False
714 c.pr_merge_status = False
715 # load compare data into template context
715 # load compare data into template context
716 enable_comments = not c.pull_request.is_closed()
716 enable_comments = not c.pull_request.is_closed()
717
717
718
718
719 # inline comments
719 # inline comments
720 c.inline_comments = cc_model.get_inline_comments(
720 c.inline_comments = cc_model.get_inline_comments(
721 c.rhodecode_db_repo.repo_id,
721 c.rhodecode_db_repo.repo_id,
722 pull_request=pull_request_id)
722 pull_request=pull_request_id)
723 c.inline_cnt = len(c.inline_comments)
723
724 c.inline_cnt = cc_model.get_inline_comments_count(c.inline_comments)
724
725
725 self._load_compare_data(
726 self._load_compare_data(
726 c.pull_request, c.inline_comments, enable_comments=enable_comments)
727 c.pull_request, c.inline_comments, enable_comments=enable_comments)
727
728
728 # outdated comments
729 # outdated comments
729 c.outdated_comments = {}
730 c.outdated_comments = {}
730 c.outdated_cnt = 0
731 c.outdated_cnt = 0
731 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
732 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
732 c.outdated_comments = cc_model.get_outdated_comments(
733 c.outdated_comments = cc_model.get_outdated_comments(
733 c.rhodecode_db_repo.repo_id,
734 c.rhodecode_db_repo.repo_id,
734 pull_request=c.pull_request)
735 pull_request=c.pull_request)
735 # Count outdated comments and check for deleted files
736 # Count outdated comments and check for deleted files
736 for file_name, lines in c.outdated_comments.iteritems():
737 for file_name, lines in c.outdated_comments.iteritems():
737 for comments in lines.values():
738 for comments in lines.values():
738 c.outdated_cnt += len(comments)
739 c.outdated_cnt += len(comments)
739 if file_name not in c.included_files:
740 if file_name not in c.included_files:
740 c.deleted_files.append(file_name)
741 c.deleted_files.append(file_name)
741
742
742
743
743 # this is a hack to properly display links, when creating PR, the
744 # this is a hack to properly display links, when creating PR, the
744 # compare view and others uses different notation, and
745 # compare view and others uses different notation, and
745 # compare_commits.html renders links based on the target_repo.
746 # compare_commits.html renders links based on the target_repo.
746 # We need to swap that here to generate it properly on the html side
747 # We need to swap that here to generate it properly on the html side
747 c.target_repo = c.source_repo
748 c.target_repo = c.source_repo
748
749
749 # comments
750 # comments
750 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
751 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
751 pull_request=pull_request_id)
752 pull_request=pull_request_id)
752
753
753 if c.allowed_to_update:
754 if c.allowed_to_update:
754 force_close = ('forced_closed', _('Close Pull Request'))
755 force_close = ('forced_closed', _('Close Pull Request'))
755 statuses = ChangesetStatus.STATUSES + [force_close]
756 statuses = ChangesetStatus.STATUSES + [force_close]
756 else:
757 else:
757 statuses = ChangesetStatus.STATUSES
758 statuses = ChangesetStatus.STATUSES
758 c.commit_statuses = statuses
759 c.commit_statuses = statuses
759
760
760 c.ancestor = None # TODO: add ancestor here
761 c.ancestor = None # TODO: add ancestor here
761
762
762 return render('/pullrequests/pullrequest_show.html')
763 return render('/pullrequests/pullrequest_show.html')
763
764
764 @LoginRequired()
765 @LoginRequired()
765 @NotAnonymous()
766 @NotAnonymous()
766 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
767 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
767 'repository.admin')
768 'repository.admin')
768 @auth.CSRFRequired()
769 @auth.CSRFRequired()
769 @jsonify
770 @jsonify
770 def comment(self, repo_name, pull_request_id):
771 def comment(self, repo_name, pull_request_id):
771 pull_request_id = safe_int(pull_request_id)
772 pull_request_id = safe_int(pull_request_id)
772 pull_request = PullRequest.get_or_404(pull_request_id)
773 pull_request = PullRequest.get_or_404(pull_request_id)
773 if pull_request.is_closed():
774 if pull_request.is_closed():
774 raise HTTPForbidden()
775 raise HTTPForbidden()
775
776
776 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
777 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
777 # as a changeset status, still we want to send it in one value.
778 # as a changeset status, still we want to send it in one value.
778 status = request.POST.get('changeset_status', None)
779 status = request.POST.get('changeset_status', None)
779 text = request.POST.get('text')
780 text = request.POST.get('text')
780 if status and '_closed' in status:
781 if status and '_closed' in status:
781 close_pr = True
782 close_pr = True
782 status = status.replace('_closed', '')
783 status = status.replace('_closed', '')
783 else:
784 else:
784 close_pr = False
785 close_pr = False
785
786
786 forced = (status == 'forced')
787 forced = (status == 'forced')
787 if forced:
788 if forced:
788 status = 'rejected'
789 status = 'rejected'
789
790
790 allowed_to_change_status = PullRequestModel().check_user_change_status(
791 allowed_to_change_status = PullRequestModel().check_user_change_status(
791 pull_request, c.rhodecode_user)
792 pull_request, c.rhodecode_user)
792
793
793 if status and allowed_to_change_status:
794 if status and allowed_to_change_status:
794 message = (_('Status change %(transition_icon)s %(status)s')
795 message = (_('Status change %(transition_icon)s %(status)s')
795 % {'transition_icon': '>',
796 % {'transition_icon': '>',
796 'status': ChangesetStatus.get_status_lbl(status)})
797 'status': ChangesetStatus.get_status_lbl(status)})
797 if close_pr:
798 if close_pr:
798 message = _('Closing with') + ' ' + message
799 message = _('Closing with') + ' ' + message
799 text = text or message
800 text = text or message
800 comm = ChangesetCommentsModel().create(
801 comm = ChangesetCommentsModel().create(
801 text=text,
802 text=text,
802 repo=c.rhodecode_db_repo.repo_id,
803 repo=c.rhodecode_db_repo.repo_id,
803 user=c.rhodecode_user.user_id,
804 user=c.rhodecode_user.user_id,
804 pull_request=pull_request_id,
805 pull_request=pull_request_id,
805 f_path=request.POST.get('f_path'),
806 f_path=request.POST.get('f_path'),
806 line_no=request.POST.get('line'),
807 line_no=request.POST.get('line'),
807 status_change=(ChangesetStatus.get_status_lbl(status)
808 status_change=(ChangesetStatus.get_status_lbl(status)
808 if status and allowed_to_change_status else None),
809 if status and allowed_to_change_status else None),
809 status_change_type=(status
810 status_change_type=(status
810 if status and allowed_to_change_status else None),
811 if status and allowed_to_change_status else None),
811 closing_pr=close_pr
812 closing_pr=close_pr
812 )
813 )
813
814
814
815
815
816
816 if allowed_to_change_status:
817 if allowed_to_change_status:
817 old_calculated_status = pull_request.calculated_review_status()
818 old_calculated_status = pull_request.calculated_review_status()
818 # get status if set !
819 # get status if set !
819 if status:
820 if status:
820 ChangesetStatusModel().set_status(
821 ChangesetStatusModel().set_status(
821 c.rhodecode_db_repo.repo_id,
822 c.rhodecode_db_repo.repo_id,
822 status,
823 status,
823 c.rhodecode_user.user_id,
824 c.rhodecode_user.user_id,
824 comm,
825 comm,
825 pull_request=pull_request_id
826 pull_request=pull_request_id
826 )
827 )
827
828
828 Session().flush()
829 Session().flush()
829 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
830 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
830 # we now calculate the status of pull request, and based on that
831 # we now calculate the status of pull request, and based on that
831 # calculation we set the commits status
832 # calculation we set the commits status
832 calculated_status = pull_request.calculated_review_status()
833 calculated_status = pull_request.calculated_review_status()
833 if old_calculated_status != calculated_status:
834 if old_calculated_status != calculated_status:
834 PullRequestModel()._trigger_pull_request_hook(
835 PullRequestModel()._trigger_pull_request_hook(
835 pull_request, c.rhodecode_user, 'review_status_change')
836 pull_request, c.rhodecode_user, 'review_status_change')
836
837
837 calculated_status_lbl = ChangesetStatus.get_status_lbl(
838 calculated_status_lbl = ChangesetStatus.get_status_lbl(
838 calculated_status)
839 calculated_status)
839
840
840 if close_pr:
841 if close_pr:
841 status_completed = (
842 status_completed = (
842 calculated_status in [ChangesetStatus.STATUS_APPROVED,
843 calculated_status in [ChangesetStatus.STATUS_APPROVED,
843 ChangesetStatus.STATUS_REJECTED])
844 ChangesetStatus.STATUS_REJECTED])
844 if forced or status_completed:
845 if forced or status_completed:
845 PullRequestModel().close_pull_request(
846 PullRequestModel().close_pull_request(
846 pull_request_id, c.rhodecode_user)
847 pull_request_id, c.rhodecode_user)
847 else:
848 else:
848 h.flash(_('Closing pull request on other statuses than '
849 h.flash(_('Closing pull request on other statuses than '
849 'rejected or approved is forbidden. '
850 'rejected or approved is forbidden. '
850 'Calculated status from all reviewers '
851 'Calculated status from all reviewers '
851 'is currently: %s') % calculated_status_lbl,
852 'is currently: %s') % calculated_status_lbl,
852 category='warning')
853 category='warning')
853
854
854 Session().commit()
855 Session().commit()
855
856
856 if not request.is_xhr:
857 if not request.is_xhr:
857 return redirect(h.url('pullrequest_show', repo_name=repo_name,
858 return redirect(h.url('pullrequest_show', repo_name=repo_name,
858 pull_request_id=pull_request_id))
859 pull_request_id=pull_request_id))
859
860
860 data = {
861 data = {
861 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
862 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
862 }
863 }
863 if comm:
864 if comm:
864 c.co = comm
865 c.co = comm
865 data.update(comm.get_dict())
866 data.update(comm.get_dict())
866 data.update({'rendered_text':
867 data.update({'rendered_text':
867 render('changeset/changeset_comment_block.html')})
868 render('changeset/changeset_comment_block.html')})
868
869
869 return data
870 return data
870
871
871 @LoginRequired()
872 @LoginRequired()
872 @NotAnonymous()
873 @NotAnonymous()
873 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
874 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
874 'repository.admin')
875 'repository.admin')
875 @auth.CSRFRequired()
876 @auth.CSRFRequired()
876 @jsonify
877 @jsonify
877 def delete_comment(self, repo_name, comment_id):
878 def delete_comment(self, repo_name, comment_id):
878 return self._delete_comment(comment_id)
879 return self._delete_comment(comment_id)
879
880
880 def _delete_comment(self, comment_id):
881 def _delete_comment(self, comment_id):
881 comment_id = safe_int(comment_id)
882 comment_id = safe_int(comment_id)
882 co = ChangesetComment.get_or_404(comment_id)
883 co = ChangesetComment.get_or_404(comment_id)
883 if co.pull_request.is_closed():
884 if co.pull_request.is_closed():
884 # don't allow deleting comments on closed pull request
885 # don't allow deleting comments on closed pull request
885 raise HTTPForbidden()
886 raise HTTPForbidden()
886
887
887 is_owner = co.author.user_id == c.rhodecode_user.user_id
888 is_owner = co.author.user_id == c.rhodecode_user.user_id
888 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
889 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
889 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
890 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
890 old_calculated_status = co.pull_request.calculated_review_status()
891 old_calculated_status = co.pull_request.calculated_review_status()
891 ChangesetCommentsModel().delete(comment=co)
892 ChangesetCommentsModel().delete(comment=co)
892 Session().commit()
893 Session().commit()
893 calculated_status = co.pull_request.calculated_review_status()
894 calculated_status = co.pull_request.calculated_review_status()
894 if old_calculated_status != calculated_status:
895 if old_calculated_status != calculated_status:
895 PullRequestModel()._trigger_pull_request_hook(
896 PullRequestModel()._trigger_pull_request_hook(
896 co.pull_request, c.rhodecode_user, 'review_status_change')
897 co.pull_request, c.rhodecode_user, 'review_status_change')
897 return True
898 return True
898 else:
899 else:
899 raise HTTPForbidden()
900 raise HTTPForbidden()
@@ -1,515 +1,525 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 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 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest)
42 ChangesetComment, User, Notification, PullRequest)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class ChangesetCommentsModel(BaseModel):
51 class ChangesetCommentsModel(BaseModel):
52
52
53 cls = ChangesetComment
53 cls = ChangesetComment
54
54
55 DIFF_CONTEXT_BEFORE = 3
55 DIFF_CONTEXT_BEFORE = 3
56 DIFF_CONTEXT_AFTER = 3
56 DIFF_CONTEXT_AFTER = 3
57
57
58 def __get_commit_comment(self, changeset_comment):
58 def __get_commit_comment(self, changeset_comment):
59 return self._get_instance(ChangesetComment, changeset_comment)
59 return self._get_instance(ChangesetComment, changeset_comment)
60
60
61 def __get_pull_request(self, pull_request):
61 def __get_pull_request(self, pull_request):
62 return self._get_instance(PullRequest, pull_request)
62 return self._get_instance(PullRequest, pull_request)
63
63
64 def _extract_mentions(self, s):
64 def _extract_mentions(self, s):
65 user_objects = []
65 user_objects = []
66 for username in extract_mentioned_users(s):
66 for username in extract_mentioned_users(s):
67 user_obj = User.get_by_username(username, case_insensitive=True)
67 user_obj = User.get_by_username(username, case_insensitive=True)
68 if user_obj:
68 if user_obj:
69 user_objects.append(user_obj)
69 user_objects.append(user_obj)
70 return user_objects
70 return user_objects
71
71
72 def _get_renderer(self, global_renderer='rst'):
72 def _get_renderer(self, global_renderer='rst'):
73 try:
73 try:
74 # try reading from visual context
74 # try reading from visual context
75 from pylons import tmpl_context
75 from pylons import tmpl_context
76 global_renderer = tmpl_context.visual.default_renderer
76 global_renderer = tmpl_context.visual.default_renderer
77 except AttributeError:
77 except AttributeError:
78 log.debug("Renderer not set, falling back "
78 log.debug("Renderer not set, falling back "
79 "to default renderer '%s'", global_renderer)
79 "to default renderer '%s'", global_renderer)
80 except Exception:
80 except Exception:
81 log.error(traceback.format_exc())
81 log.error(traceback.format_exc())
82 return global_renderer
82 return global_renderer
83
83
84 def create(self, text, repo, user, revision=None, pull_request=None,
84 def create(self, text, repo, user, revision=None, pull_request=None,
85 f_path=None, line_no=None, status_change=None,
85 f_path=None, line_no=None, status_change=None,
86 status_change_type=None, closing_pr=False,
86 status_change_type=None, closing_pr=False,
87 send_email=True, renderer=None):
87 send_email=True, renderer=None):
88 """
88 """
89 Creates new comment for commit or pull request.
89 Creates new comment for commit or pull request.
90 IF status_change is not none this comment is associated with a
90 IF status_change is not none this comment is associated with a
91 status change of commit or commit associated with pull request
91 status change of commit or commit associated with pull request
92
92
93 :param text:
93 :param text:
94 :param repo:
94 :param repo:
95 :param user:
95 :param user:
96 :param revision:
96 :param revision:
97 :param pull_request:
97 :param pull_request:
98 :param f_path:
98 :param f_path:
99 :param line_no:
99 :param line_no:
100 :param status_change: Label for status change
100 :param status_change: Label for status change
101 :param status_change_type: type of status change
101 :param status_change_type: type of status change
102 :param closing_pr:
102 :param closing_pr:
103 :param send_email:
103 :param send_email:
104 """
104 """
105 if not text:
105 if not text:
106 log.warning('Missing text for comment, skipping...')
106 log.warning('Missing text for comment, skipping...')
107 return
107 return
108
108
109 if not renderer:
109 if not renderer:
110 renderer = self._get_renderer()
110 renderer = self._get_renderer()
111
111
112 repo = self._get_repo(repo)
112 repo = self._get_repo(repo)
113 user = self._get_user(user)
113 user = self._get_user(user)
114 comment = ChangesetComment()
114 comment = ChangesetComment()
115 comment.renderer = renderer
115 comment.renderer = renderer
116 comment.repo = repo
116 comment.repo = repo
117 comment.author = user
117 comment.author = user
118 comment.text = text
118 comment.text = text
119 comment.f_path = f_path
119 comment.f_path = f_path
120 comment.line_no = line_no
120 comment.line_no = line_no
121
121
122 #TODO (marcink): fix this and remove revision as param
122 #TODO (marcink): fix this and remove revision as param
123 commit_id = revision
123 commit_id = revision
124 pull_request_id = pull_request
124 pull_request_id = pull_request
125
125
126 commit_obj = None
126 commit_obj = None
127 pull_request_obj = None
127 pull_request_obj = None
128
128
129 if commit_id:
129 if commit_id:
130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
131 # do a lookup, so we don't pass something bad here
131 # do a lookup, so we don't pass something bad here
132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
133 comment.revision = commit_obj.raw_id
133 comment.revision = commit_obj.raw_id
134
134
135 elif pull_request_id:
135 elif pull_request_id:
136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
137 pull_request_obj = self.__get_pull_request(pull_request_id)
137 pull_request_obj = self.__get_pull_request(pull_request_id)
138 comment.pull_request = pull_request_obj
138 comment.pull_request = pull_request_obj
139 else:
139 else:
140 raise Exception('Please specify commit or pull_request_id')
140 raise Exception('Please specify commit or pull_request_id')
141
141
142 Session().add(comment)
142 Session().add(comment)
143 Session().flush()
143 Session().flush()
144 kwargs = {
144 kwargs = {
145 'user': user,
145 'user': user,
146 'renderer_type': renderer,
146 'renderer_type': renderer,
147 'repo_name': repo.repo_name,
147 'repo_name': repo.repo_name,
148 'status_change': status_change,
148 'status_change': status_change,
149 'status_change_type': status_change_type,
149 'status_change_type': status_change_type,
150 'comment_body': text,
150 'comment_body': text,
151 'comment_file': f_path,
151 'comment_file': f_path,
152 'comment_line': line_no,
152 'comment_line': line_no,
153 }
153 }
154
154
155 if commit_obj:
155 if commit_obj:
156 recipients = ChangesetComment.get_users(
156 recipients = ChangesetComment.get_users(
157 revision=commit_obj.raw_id)
157 revision=commit_obj.raw_id)
158 # add commit author if it's in RhodeCode system
158 # add commit author if it's in RhodeCode system
159 cs_author = User.get_from_cs_author(commit_obj.author)
159 cs_author = User.get_from_cs_author(commit_obj.author)
160 if not cs_author:
160 if not cs_author:
161 # use repo owner if we cannot extract the author correctly
161 # use repo owner if we cannot extract the author correctly
162 cs_author = repo.user
162 cs_author = repo.user
163 recipients += [cs_author]
163 recipients += [cs_author]
164
164
165 commit_comment_url = self.get_url(comment)
165 commit_comment_url = self.get_url(comment)
166
166
167 target_repo_url = h.link_to(
167 target_repo_url = h.link_to(
168 repo.repo_name,
168 repo.repo_name,
169 h.url('summary_home',
169 h.url('summary_home',
170 repo_name=repo.repo_name, qualified=True))
170 repo_name=repo.repo_name, qualified=True))
171
171
172 # commit specifics
172 # commit specifics
173 kwargs.update({
173 kwargs.update({
174 'commit': commit_obj,
174 'commit': commit_obj,
175 'commit_message': commit_obj.message,
175 'commit_message': commit_obj.message,
176 'commit_target_repo': target_repo_url,
176 'commit_target_repo': target_repo_url,
177 'commit_comment_url': commit_comment_url,
177 'commit_comment_url': commit_comment_url,
178 })
178 })
179
179
180 elif pull_request_obj:
180 elif pull_request_obj:
181 # get the current participants of this pull request
181 # get the current participants of this pull request
182 recipients = ChangesetComment.get_users(
182 recipients = ChangesetComment.get_users(
183 pull_request_id=pull_request_obj.pull_request_id)
183 pull_request_id=pull_request_obj.pull_request_id)
184 # add pull request author
184 # add pull request author
185 recipients += [pull_request_obj.author]
185 recipients += [pull_request_obj.author]
186
186
187 # add the reviewers to notification
187 # add the reviewers to notification
188 recipients += [x.user for x in pull_request_obj.reviewers]
188 recipients += [x.user for x in pull_request_obj.reviewers]
189
189
190 pr_target_repo = pull_request_obj.target_repo
190 pr_target_repo = pull_request_obj.target_repo
191 pr_source_repo = pull_request_obj.source_repo
191 pr_source_repo = pull_request_obj.source_repo
192
192
193 pr_comment_url = h.url(
193 pr_comment_url = h.url(
194 'pullrequest_show',
194 'pullrequest_show',
195 repo_name=pr_target_repo.repo_name,
195 repo_name=pr_target_repo.repo_name,
196 pull_request_id=pull_request_obj.pull_request_id,
196 pull_request_id=pull_request_obj.pull_request_id,
197 anchor='comment-%s' % comment.comment_id,
197 anchor='comment-%s' % comment.comment_id,
198 qualified=True,)
198 qualified=True,)
199
199
200 # set some variables for email notification
200 # set some variables for email notification
201 pr_target_repo_url = h.url(
201 pr_target_repo_url = h.url(
202 'summary_home', repo_name=pr_target_repo.repo_name,
202 'summary_home', repo_name=pr_target_repo.repo_name,
203 qualified=True)
203 qualified=True)
204
204
205 pr_source_repo_url = h.url(
205 pr_source_repo_url = h.url(
206 'summary_home', repo_name=pr_source_repo.repo_name,
206 'summary_home', repo_name=pr_source_repo.repo_name,
207 qualified=True)
207 qualified=True)
208
208
209 # pull request specifics
209 # pull request specifics
210 kwargs.update({
210 kwargs.update({
211 'pull_request': pull_request_obj,
211 'pull_request': pull_request_obj,
212 'pr_id': pull_request_obj.pull_request_id,
212 'pr_id': pull_request_obj.pull_request_id,
213 'pr_target_repo': pr_target_repo,
213 'pr_target_repo': pr_target_repo,
214 'pr_target_repo_url': pr_target_repo_url,
214 'pr_target_repo_url': pr_target_repo_url,
215 'pr_source_repo': pr_source_repo,
215 'pr_source_repo': pr_source_repo,
216 'pr_source_repo_url': pr_source_repo_url,
216 'pr_source_repo_url': pr_source_repo_url,
217 'pr_comment_url': pr_comment_url,
217 'pr_comment_url': pr_comment_url,
218 'pr_closing': closing_pr,
218 'pr_closing': closing_pr,
219 })
219 })
220 if send_email:
220 if send_email:
221 # pre-generate the subject for notification itself
221 # pre-generate the subject for notification itself
222 (subject,
222 (subject,
223 _h, _e, # we don't care about those
223 _h, _e, # we don't care about those
224 body_plaintext) = EmailNotificationModel().render_email(
224 body_plaintext) = EmailNotificationModel().render_email(
225 notification_type, **kwargs)
225 notification_type, **kwargs)
226
226
227 mention_recipients = set(
227 mention_recipients = set(
228 self._extract_mentions(text)).difference(recipients)
228 self._extract_mentions(text)).difference(recipients)
229
229
230 # create notification objects, and emails
230 # create notification objects, and emails
231 NotificationModel().create(
231 NotificationModel().create(
232 created_by=user,
232 created_by=user,
233 notification_subject=subject,
233 notification_subject=subject,
234 notification_body=body_plaintext,
234 notification_body=body_plaintext,
235 notification_type=notification_type,
235 notification_type=notification_type,
236 recipients=recipients,
236 recipients=recipients,
237 mention_recipients=mention_recipients,
237 mention_recipients=mention_recipients,
238 email_kwargs=kwargs,
238 email_kwargs=kwargs,
239 )
239 )
240
240
241 action = (
241 action = (
242 'user_commented_pull_request:{}'.format(
242 'user_commented_pull_request:{}'.format(
243 comment.pull_request.pull_request_id)
243 comment.pull_request.pull_request_id)
244 if comment.pull_request
244 if comment.pull_request
245 else 'user_commented_revision:{}'.format(comment.revision)
245 else 'user_commented_revision:{}'.format(comment.revision)
246 )
246 )
247 action_logger(user, action, comment.repo)
247 action_logger(user, action, comment.repo)
248
248
249 registry = get_current_registry()
249 registry = get_current_registry()
250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
251 channelstream_config = rhodecode_plugins.get('channelstream', {})
251 channelstream_config = rhodecode_plugins.get('channelstream', {})
252 msg_url = ''
252 msg_url = ''
253 if commit_obj:
253 if commit_obj:
254 msg_url = commit_comment_url
254 msg_url = commit_comment_url
255 repo_name = repo.repo_name
255 repo_name = repo.repo_name
256 elif pull_request_obj:
256 elif pull_request_obj:
257 msg_url = pr_comment_url
257 msg_url = pr_comment_url
258 repo_name = pr_target_repo.repo_name
258 repo_name = pr_target_repo.repo_name
259
259
260 if channelstream_config.get('enabled'):
260 if channelstream_config.get('enabled'):
261 message = '<strong>{}</strong> {} - ' \
261 message = '<strong>{}</strong> {} - ' \
262 '<a onclick="window.location=\'{}\';' \
262 '<a onclick="window.location=\'{}\';' \
263 'window.location.reload()">' \
263 'window.location.reload()">' \
264 '<strong>{}</strong></a>'
264 '<strong>{}</strong></a>'
265 message = message.format(
265 message = message.format(
266 user.username, _('made a comment'), msg_url,
266 user.username, _('made a comment'), msg_url,
267 _('Show it now'))
267 _('Show it now'))
268 channel = '/repo${}$/pr/{}'.format(
268 channel = '/repo${}$/pr/{}'.format(
269 repo_name,
269 repo_name,
270 pull_request_id
270 pull_request_id
271 )
271 )
272 payload = {
272 payload = {
273 'type': 'message',
273 'type': 'message',
274 'timestamp': datetime.utcnow(),
274 'timestamp': datetime.utcnow(),
275 'user': 'system',
275 'user': 'system',
276 'exclude_users': [user.username],
276 'exclude_users': [user.username],
277 'channel': channel,
277 'channel': channel,
278 'message': {
278 'message': {
279 'message': message,
279 'message': message,
280 'level': 'info',
280 'level': 'info',
281 'topic': '/notifications'
281 'topic': '/notifications'
282 }
282 }
283 }
283 }
284 channelstream_request(channelstream_config, [payload],
284 channelstream_request(channelstream_config, [payload],
285 '/message', raise_exc=False)
285 '/message', raise_exc=False)
286
286
287 return comment
287 return comment
288
288
289 def delete(self, comment):
289 def delete(self, comment):
290 """
290 """
291 Deletes given comment
291 Deletes given comment
292
292
293 :param comment_id:
293 :param comment_id:
294 """
294 """
295 comment = self.__get_commit_comment(comment)
295 comment = self.__get_commit_comment(comment)
296 Session().delete(comment)
296 Session().delete(comment)
297
297
298 return comment
298 return comment
299
299
300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
301 q = ChangesetComment.query()\
301 q = ChangesetComment.query()\
302 .filter(ChangesetComment.repo_id == repo_id)
302 .filter(ChangesetComment.repo_id == repo_id)
303 if revision:
303 if revision:
304 q = q.filter(ChangesetComment.revision == revision)
304 q = q.filter(ChangesetComment.revision == revision)
305 elif pull_request:
305 elif pull_request:
306 pull_request = self.__get_pull_request(pull_request)
306 pull_request = self.__get_pull_request(pull_request)
307 q = q.filter(ChangesetComment.pull_request == pull_request)
307 q = q.filter(ChangesetComment.pull_request == pull_request)
308 else:
308 else:
309 raise Exception('Please specify commit or pull_request')
309 raise Exception('Please specify commit or pull_request')
310 q = q.order_by(ChangesetComment.created_on)
310 q = q.order_by(ChangesetComment.created_on)
311 return q.all()
311 return q.all()
312
312
313 def get_url(self, comment):
313 def get_url(self, comment):
314 comment = self.__get_commit_comment(comment)
314 comment = self.__get_commit_comment(comment)
315 if comment.pull_request:
315 if comment.pull_request:
316 return h.url(
316 return h.url(
317 'pullrequest_show',
317 'pullrequest_show',
318 repo_name=comment.pull_request.target_repo.repo_name,
318 repo_name=comment.pull_request.target_repo.repo_name,
319 pull_request_id=comment.pull_request.pull_request_id,
319 pull_request_id=comment.pull_request.pull_request_id,
320 anchor='comment-%s' % comment.comment_id,
320 anchor='comment-%s' % comment.comment_id,
321 qualified=True,)
321 qualified=True,)
322 else:
322 else:
323 return h.url(
323 return h.url(
324 'changeset_home',
324 'changeset_home',
325 repo_name=comment.repo.repo_name,
325 repo_name=comment.repo.repo_name,
326 revision=comment.revision,
326 revision=comment.revision,
327 anchor='comment-%s' % comment.comment_id,
327 anchor='comment-%s' % comment.comment_id,
328 qualified=True,)
328 qualified=True,)
329
329
330 def get_comments(self, repo_id, revision=None, pull_request=None):
330 def get_comments(self, repo_id, revision=None, pull_request=None):
331 """
331 """
332 Gets main comments based on revision or pull_request_id
332 Gets main comments based on revision or pull_request_id
333
333
334 :param repo_id:
334 :param repo_id:
335 :param revision:
335 :param revision:
336 :param pull_request:
336 :param pull_request:
337 """
337 """
338
338
339 q = ChangesetComment.query()\
339 q = ChangesetComment.query()\
340 .filter(ChangesetComment.repo_id == repo_id)\
340 .filter(ChangesetComment.repo_id == repo_id)\
341 .filter(ChangesetComment.line_no == None)\
341 .filter(ChangesetComment.line_no == None)\
342 .filter(ChangesetComment.f_path == None)
342 .filter(ChangesetComment.f_path == None)
343 if revision:
343 if revision:
344 q = q.filter(ChangesetComment.revision == revision)
344 q = q.filter(ChangesetComment.revision == revision)
345 elif pull_request:
345 elif pull_request:
346 pull_request = self.__get_pull_request(pull_request)
346 pull_request = self.__get_pull_request(pull_request)
347 q = q.filter(ChangesetComment.pull_request == pull_request)
347 q = q.filter(ChangesetComment.pull_request == pull_request)
348 else:
348 else:
349 raise Exception('Please specify commit or pull_request')
349 raise Exception('Please specify commit or pull_request')
350 q = q.order_by(ChangesetComment.created_on)
350 q = q.order_by(ChangesetComment.created_on)
351 return q.all()
351 return q.all()
352
352
353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
355 return self._group_comments_by_path_and_line_number(q)
355 return self._group_comments_by_path_and_line_number(q)
356
356
357 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
358 version=None):
359 inline_cnt = 0
360 for fname, per_line_comments in inline_comments.iteritems():
361 for lno, comments in per_line_comments.iteritems():
362 inline_cnt += len(
363 [comm for comm in comments
364 if (not comm.outdated and skip_outdated)])
365 return inline_cnt
366
357 def get_outdated_comments(self, repo_id, pull_request):
367 def get_outdated_comments(self, repo_id, pull_request):
358 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
368 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
359 # of a pull request.
369 # of a pull request.
360 q = self._all_inline_comments_of_pull_request(pull_request)
370 q = self._all_inline_comments_of_pull_request(pull_request)
361 q = q.filter(
371 q = q.filter(
362 ChangesetComment.display_state ==
372 ChangesetComment.display_state ==
363 ChangesetComment.COMMENT_OUTDATED
373 ChangesetComment.COMMENT_OUTDATED
364 ).order_by(ChangesetComment.comment_id.asc())
374 ).order_by(ChangesetComment.comment_id.asc())
365
375
366 return self._group_comments_by_path_and_line_number(q)
376 return self._group_comments_by_path_and_line_number(q)
367
377
368 def _get_inline_comments_query(self, repo_id, revision, pull_request):
378 def _get_inline_comments_query(self, repo_id, revision, pull_request):
369 # TODO: johbo: Split this into two methods: One for PR and one for
379 # TODO: johbo: Split this into two methods: One for PR and one for
370 # commit.
380 # commit.
371 if revision:
381 if revision:
372 q = Session().query(ChangesetComment).filter(
382 q = Session().query(ChangesetComment).filter(
373 ChangesetComment.repo_id == repo_id,
383 ChangesetComment.repo_id == repo_id,
374 ChangesetComment.line_no != null(),
384 ChangesetComment.line_no != null(),
375 ChangesetComment.f_path != null(),
385 ChangesetComment.f_path != null(),
376 ChangesetComment.revision == revision)
386 ChangesetComment.revision == revision)
377
387
378 elif pull_request:
388 elif pull_request:
379 pull_request = self.__get_pull_request(pull_request)
389 pull_request = self.__get_pull_request(pull_request)
380 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
390 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
381 q = self._visible_inline_comments_of_pull_request(pull_request)
391 q = self._visible_inline_comments_of_pull_request(pull_request)
382 else:
392 else:
383 q = self._all_inline_comments_of_pull_request(pull_request)
393 q = self._all_inline_comments_of_pull_request(pull_request)
384
394
385 else:
395 else:
386 raise Exception('Please specify commit or pull_request_id')
396 raise Exception('Please specify commit or pull_request_id')
387 q = q.order_by(ChangesetComment.comment_id.asc())
397 q = q.order_by(ChangesetComment.comment_id.asc())
388 return q
398 return q
389
399
390 def _group_comments_by_path_and_line_number(self, q):
400 def _group_comments_by_path_and_line_number(self, q):
391 comments = q.all()
401 comments = q.all()
392 paths = collections.defaultdict(lambda: collections.defaultdict(list))
402 paths = collections.defaultdict(lambda: collections.defaultdict(list))
393 for co in comments:
403 for co in comments:
394 paths[co.f_path][co.line_no].append(co)
404 paths[co.f_path][co.line_no].append(co)
395 return paths
405 return paths
396
406
397 @classmethod
407 @classmethod
398 def needed_extra_diff_context(cls):
408 def needed_extra_diff_context(cls):
399 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
409 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
400
410
401 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
411 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
402 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
412 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
403 return
413 return
404
414
405 comments = self._visible_inline_comments_of_pull_request(pull_request)
415 comments = self._visible_inline_comments_of_pull_request(pull_request)
406 comments_to_outdate = comments.all()
416 comments_to_outdate = comments.all()
407
417
408 for comment in comments_to_outdate:
418 for comment in comments_to_outdate:
409 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
419 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
410
420
411 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
421 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
412 diff_line = _parse_comment_line_number(comment.line_no)
422 diff_line = _parse_comment_line_number(comment.line_no)
413
423
414 try:
424 try:
415 old_context = old_diff_proc.get_context_of_line(
425 old_context = old_diff_proc.get_context_of_line(
416 path=comment.f_path, diff_line=diff_line)
426 path=comment.f_path, diff_line=diff_line)
417 new_context = new_diff_proc.get_context_of_line(
427 new_context = new_diff_proc.get_context_of_line(
418 path=comment.f_path, diff_line=diff_line)
428 path=comment.f_path, diff_line=diff_line)
419 except (diffs.LineNotInDiffException,
429 except (diffs.LineNotInDiffException,
420 diffs.FileNotInDiffException):
430 diffs.FileNotInDiffException):
421 comment.display_state = ChangesetComment.COMMENT_OUTDATED
431 comment.display_state = ChangesetComment.COMMENT_OUTDATED
422 return
432 return
423
433
424 if old_context == new_context:
434 if old_context == new_context:
425 return
435 return
426
436
427 if self._should_relocate_diff_line(diff_line):
437 if self._should_relocate_diff_line(diff_line):
428 new_diff_lines = new_diff_proc.find_context(
438 new_diff_lines = new_diff_proc.find_context(
429 path=comment.f_path, context=old_context,
439 path=comment.f_path, context=old_context,
430 offset=self.DIFF_CONTEXT_BEFORE)
440 offset=self.DIFF_CONTEXT_BEFORE)
431 if not new_diff_lines:
441 if not new_diff_lines:
432 comment.display_state = ChangesetComment.COMMENT_OUTDATED
442 comment.display_state = ChangesetComment.COMMENT_OUTDATED
433 else:
443 else:
434 new_diff_line = self._choose_closest_diff_line(
444 new_diff_line = self._choose_closest_diff_line(
435 diff_line, new_diff_lines)
445 diff_line, new_diff_lines)
436 comment.line_no = _diff_to_comment_line_number(new_diff_line)
446 comment.line_no = _diff_to_comment_line_number(new_diff_line)
437 else:
447 else:
438 comment.display_state = ChangesetComment.COMMENT_OUTDATED
448 comment.display_state = ChangesetComment.COMMENT_OUTDATED
439
449
440 def _should_relocate_diff_line(self, diff_line):
450 def _should_relocate_diff_line(self, diff_line):
441 """
451 """
442 Checks if relocation shall be tried for the given `diff_line`.
452 Checks if relocation shall be tried for the given `diff_line`.
443
453
444 If a comment points into the first lines, then we can have a situation
454 If a comment points into the first lines, then we can have a situation
445 that after an update another line has been added on top. In this case
455 that after an update another line has been added on top. In this case
446 we would find the context still and move the comment around. This
456 we would find the context still and move the comment around. This
447 would be wrong.
457 would be wrong.
448 """
458 """
449 should_relocate = (
459 should_relocate = (
450 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
460 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
451 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
461 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
452 return should_relocate
462 return should_relocate
453
463
454 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
464 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
455 candidate = new_diff_lines[0]
465 candidate = new_diff_lines[0]
456 best_delta = _diff_line_delta(diff_line, candidate)
466 best_delta = _diff_line_delta(diff_line, candidate)
457 for new_diff_line in new_diff_lines[1:]:
467 for new_diff_line in new_diff_lines[1:]:
458 delta = _diff_line_delta(diff_line, new_diff_line)
468 delta = _diff_line_delta(diff_line, new_diff_line)
459 if delta < best_delta:
469 if delta < best_delta:
460 candidate = new_diff_line
470 candidate = new_diff_line
461 best_delta = delta
471 best_delta = delta
462 return candidate
472 return candidate
463
473
464 def _visible_inline_comments_of_pull_request(self, pull_request):
474 def _visible_inline_comments_of_pull_request(self, pull_request):
465 comments = self._all_inline_comments_of_pull_request(pull_request)
475 comments = self._all_inline_comments_of_pull_request(pull_request)
466 comments = comments.filter(
476 comments = comments.filter(
467 coalesce(ChangesetComment.display_state, '') !=
477 coalesce(ChangesetComment.display_state, '') !=
468 ChangesetComment.COMMENT_OUTDATED)
478 ChangesetComment.COMMENT_OUTDATED)
469 return comments
479 return comments
470
480
471 def _all_inline_comments_of_pull_request(self, pull_request):
481 def _all_inline_comments_of_pull_request(self, pull_request):
472 comments = Session().query(ChangesetComment)\
482 comments = Session().query(ChangesetComment)\
473 .filter(ChangesetComment.line_no != None)\
483 .filter(ChangesetComment.line_no != None)\
474 .filter(ChangesetComment.f_path != None)\
484 .filter(ChangesetComment.f_path != None)\
475 .filter(ChangesetComment.pull_request == pull_request)
485 .filter(ChangesetComment.pull_request == pull_request)
476 return comments
486 return comments
477
487
478 @staticmethod
488 @staticmethod
479 def use_outdated_comments(pull_request):
489 def use_outdated_comments(pull_request):
480 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
490 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
481 settings = settings_model.get_general_settings()
491 settings = settings_model.get_general_settings()
482 return settings.get('rhodecode_use_outdated_comments', False)
492 return settings.get('rhodecode_use_outdated_comments', False)
483
493
484
494
485 def _parse_comment_line_number(line_no):
495 def _parse_comment_line_number(line_no):
486 """
496 """
487 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
497 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
488 """
498 """
489 old_line = None
499 old_line = None
490 new_line = None
500 new_line = None
491 if line_no.startswith('o'):
501 if line_no.startswith('o'):
492 old_line = int(line_no[1:])
502 old_line = int(line_no[1:])
493 elif line_no.startswith('n'):
503 elif line_no.startswith('n'):
494 new_line = int(line_no[1:])
504 new_line = int(line_no[1:])
495 else:
505 else:
496 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
506 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
497 return diffs.DiffLineNumber(old_line, new_line)
507 return diffs.DiffLineNumber(old_line, new_line)
498
508
499
509
500 def _diff_to_comment_line_number(diff_line):
510 def _diff_to_comment_line_number(diff_line):
501 if diff_line.new is not None:
511 if diff_line.new is not None:
502 return u'n{}'.format(diff_line.new)
512 return u'n{}'.format(diff_line.new)
503 elif diff_line.old is not None:
513 elif diff_line.old is not None:
504 return u'o{}'.format(diff_line.old)
514 return u'o{}'.format(diff_line.old)
505 return u''
515 return u''
506
516
507
517
508 def _diff_line_delta(a, b):
518 def _diff_line_delta(a, b):
509 if None not in (a.new, b.new):
519 if None not in (a.new, b.new):
510 return abs(a.new - b.new)
520 return abs(a.new - b.new)
511 elif None not in (a.old, b.old):
521 elif None not in (a.old, b.old):
512 return abs(a.old - b.old)
522 return abs(a.old - b.old)
513 else:
523 else:
514 raise ValueError(
524 raise ValueError(
515 "Cannot compute delta between {} and {}".format(a, b))
525 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,841 +1,843 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import ChangesetCommentsModel
32 from rhodecode.model.comment import ChangesetCommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 class TestPullRequestModel:
44 class TestPullRequestModel:
45
45
46 @pytest.fixture
46 @pytest.fixture
47 def pull_request(self, request, backend, pr_util):
47 def pull_request(self, request, backend, pr_util):
48 """
48 """
49 A pull request combined with multiples patches.
49 A pull request combined with multiples patches.
50 """
50 """
51 BackendClass = get_backend(backend.alias)
51 BackendClass = get_backend(backend.alias)
52 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
52 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
53 self.workspace_remove_patcher = mock.patch.object(
53 self.workspace_remove_patcher = mock.patch.object(
54 BackendClass, 'cleanup_merge_workspace')
54 BackendClass, 'cleanup_merge_workspace')
55
55
56 self.workspace_remove_mock = self.workspace_remove_patcher.start()
56 self.workspace_remove_mock = self.workspace_remove_patcher.start()
57 self.merge_mock = self.merge_patcher.start()
57 self.merge_mock = self.merge_patcher.start()
58 self.comment_patcher = mock.patch(
58 self.comment_patcher = mock.patch(
59 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
59 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
60 self.comment_patcher.start()
60 self.comment_patcher.start()
61 self.notification_patcher = mock.patch(
61 self.notification_patcher = mock.patch(
62 'rhodecode.model.notification.NotificationModel.create')
62 'rhodecode.model.notification.NotificationModel.create')
63 self.notification_patcher.start()
63 self.notification_patcher.start()
64 self.helper_patcher = mock.patch(
64 self.helper_patcher = mock.patch(
65 'rhodecode.lib.helpers.url')
65 'rhodecode.lib.helpers.url')
66 self.helper_patcher.start()
66 self.helper_patcher.start()
67
67
68 self.hook_patcher = mock.patch.object(PullRequestModel,
68 self.hook_patcher = mock.patch.object(PullRequestModel,
69 '_trigger_pull_request_hook')
69 '_trigger_pull_request_hook')
70 self.hook_mock = self.hook_patcher.start()
70 self.hook_mock = self.hook_patcher.start()
71
71
72 self.invalidation_patcher = mock.patch(
72 self.invalidation_patcher = mock.patch(
73 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
73 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
74 self.invalidation_mock = self.invalidation_patcher.start()
74 self.invalidation_mock = self.invalidation_patcher.start()
75
75
76 self.pull_request = pr_util.create_pull_request(
76 self.pull_request = pr_util.create_pull_request(
77 mergeable=True, name_suffix=u'Δ…Δ‡')
77 mergeable=True, name_suffix=u'Δ…Δ‡')
78 self.source_commit = self.pull_request.source_ref_parts.commit_id
78 self.source_commit = self.pull_request.source_ref_parts.commit_id
79 self.target_commit = self.pull_request.target_ref_parts.commit_id
79 self.target_commit = self.pull_request.target_ref_parts.commit_id
80 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
80 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
81
81
82 @request.addfinalizer
82 @request.addfinalizer
83 def cleanup_pull_request():
83 def cleanup_pull_request():
84 calls = [mock.call(
84 calls = [mock.call(
85 self.pull_request, self.pull_request.author, 'create')]
85 self.pull_request, self.pull_request.author, 'create')]
86 self.hook_mock.assert_has_calls(calls)
86 self.hook_mock.assert_has_calls(calls)
87
87
88 self.workspace_remove_patcher.stop()
88 self.workspace_remove_patcher.stop()
89 self.merge_patcher.stop()
89 self.merge_patcher.stop()
90 self.comment_patcher.stop()
90 self.comment_patcher.stop()
91 self.notification_patcher.stop()
91 self.notification_patcher.stop()
92 self.helper_patcher.stop()
92 self.helper_patcher.stop()
93 self.hook_patcher.stop()
93 self.hook_patcher.stop()
94 self.invalidation_patcher.stop()
94 self.invalidation_patcher.stop()
95
95
96 return self.pull_request
96 return self.pull_request
97
97
98 def test_get_all(self, pull_request):
98 def test_get_all(self, pull_request):
99 prs = PullRequestModel().get_all(pull_request.target_repo)
99 prs = PullRequestModel().get_all(pull_request.target_repo)
100 assert isinstance(prs, list)
100 assert isinstance(prs, list)
101 assert len(prs) == 1
101 assert len(prs) == 1
102
102
103 def test_count_all(self, pull_request):
103 def test_count_all(self, pull_request):
104 pr_count = PullRequestModel().count_all(pull_request.target_repo)
104 pr_count = PullRequestModel().count_all(pull_request.target_repo)
105 assert pr_count == 1
105 assert pr_count == 1
106
106
107 def test_get_awaiting_review(self, pull_request):
107 def test_get_awaiting_review(self, pull_request):
108 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
108 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
109 assert isinstance(prs, list)
109 assert isinstance(prs, list)
110 assert len(prs) == 1
110 assert len(prs) == 1
111
111
112 def test_count_awaiting_review(self, pull_request):
112 def test_count_awaiting_review(self, pull_request):
113 pr_count = PullRequestModel().count_awaiting_review(
113 pr_count = PullRequestModel().count_awaiting_review(
114 pull_request.target_repo)
114 pull_request.target_repo)
115 assert pr_count == 1
115 assert pr_count == 1
116
116
117 def test_get_awaiting_my_review(self, pull_request):
117 def test_get_awaiting_my_review(self, pull_request):
118 PullRequestModel().update_reviewers(
118 PullRequestModel().update_reviewers(
119 pull_request, [(pull_request.author, ['author'])])
119 pull_request, [(pull_request.author, ['author'])])
120 prs = PullRequestModel().get_awaiting_my_review(
120 prs = PullRequestModel().get_awaiting_my_review(
121 pull_request.target_repo, user_id=pull_request.author.user_id)
121 pull_request.target_repo, user_id=pull_request.author.user_id)
122 assert isinstance(prs, list)
122 assert isinstance(prs, list)
123 assert len(prs) == 1
123 assert len(prs) == 1
124
124
125 def test_count_awaiting_my_review(self, pull_request):
125 def test_count_awaiting_my_review(self, pull_request):
126 PullRequestModel().update_reviewers(
126 PullRequestModel().update_reviewers(
127 pull_request, [(pull_request.author, ['author'])])
127 pull_request, [(pull_request.author, ['author'])])
128 pr_count = PullRequestModel().count_awaiting_my_review(
128 pr_count = PullRequestModel().count_awaiting_my_review(
129 pull_request.target_repo, user_id=pull_request.author.user_id)
129 pull_request.target_repo, user_id=pull_request.author.user_id)
130 assert pr_count == 1
130 assert pr_count == 1
131
131
132 def test_delete_calls_cleanup_merge(self, pull_request):
132 def test_delete_calls_cleanup_merge(self, pull_request):
133 PullRequestModel().delete(pull_request)
133 PullRequestModel().delete(pull_request)
134
134
135 self.workspace_remove_mock.assert_called_once_with(
135 self.workspace_remove_mock.assert_called_once_with(
136 self.workspace_id)
136 self.workspace_id)
137
137
138 def test_close_calls_cleanup_and_hook(self, pull_request):
138 def test_close_calls_cleanup_and_hook(self, pull_request):
139 PullRequestModel().close_pull_request(
139 PullRequestModel().close_pull_request(
140 pull_request, pull_request.author)
140 pull_request, pull_request.author)
141
141
142 self.workspace_remove_mock.assert_called_once_with(
142 self.workspace_remove_mock.assert_called_once_with(
143 self.workspace_id)
143 self.workspace_id)
144 self.hook_mock.assert_called_with(
144 self.hook_mock.assert_called_with(
145 self.pull_request, self.pull_request.author, 'close')
145 self.pull_request, self.pull_request.author, 'close')
146
146
147 def test_merge_status(self, pull_request):
147 def test_merge_status(self, pull_request):
148 self.merge_mock.return_value = MergeResponse(
148 self.merge_mock.return_value = MergeResponse(
149 True, False, None, MergeFailureReason.NONE)
149 True, False, None, MergeFailureReason.NONE)
150
150
151 assert pull_request._last_merge_source_rev is None
151 assert pull_request._last_merge_source_rev is None
152 assert pull_request._last_merge_target_rev is None
152 assert pull_request._last_merge_target_rev is None
153 assert pull_request._last_merge_status is None
153 assert pull_request._last_merge_status is None
154
154
155 status, msg = PullRequestModel().merge_status(pull_request)
155 status, msg = PullRequestModel().merge_status(pull_request)
156 assert status is True
156 assert status is True
157 assert msg.eval() == 'This pull request can be automatically merged.'
157 assert msg.eval() == 'This pull request can be automatically merged.'
158 self.merge_mock.assert_called_once_with(
158 self.merge_mock.assert_called_once_with(
159 pull_request.target_ref_parts,
159 pull_request.target_ref_parts,
160 pull_request.source_repo.scm_instance(),
160 pull_request.source_repo.scm_instance(),
161 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
161 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
162 use_rebase=False)
162 use_rebase=False)
163
163
164 assert pull_request._last_merge_source_rev == self.source_commit
164 assert pull_request._last_merge_source_rev == self.source_commit
165 assert pull_request._last_merge_target_rev == self.target_commit
165 assert pull_request._last_merge_target_rev == self.target_commit
166 assert pull_request._last_merge_status is MergeFailureReason.NONE
166 assert pull_request._last_merge_status is MergeFailureReason.NONE
167
167
168 self.merge_mock.reset_mock()
168 self.merge_mock.reset_mock()
169 status, msg = PullRequestModel().merge_status(pull_request)
169 status, msg = PullRequestModel().merge_status(pull_request)
170 assert status is True
170 assert status is True
171 assert msg.eval() == 'This pull request can be automatically merged.'
171 assert msg.eval() == 'This pull request can be automatically merged.'
172 assert self.merge_mock.called is False
172 assert self.merge_mock.called is False
173
173
174 def test_merge_status_known_failure(self, pull_request):
174 def test_merge_status_known_failure(self, pull_request):
175 self.merge_mock.return_value = MergeResponse(
175 self.merge_mock.return_value = MergeResponse(
176 False, False, None, MergeFailureReason.MERGE_FAILED)
176 False, False, None, MergeFailureReason.MERGE_FAILED)
177
177
178 assert pull_request._last_merge_source_rev is None
178 assert pull_request._last_merge_source_rev is None
179 assert pull_request._last_merge_target_rev is None
179 assert pull_request._last_merge_target_rev is None
180 assert pull_request._last_merge_status is None
180 assert pull_request._last_merge_status is None
181
181
182 status, msg = PullRequestModel().merge_status(pull_request)
182 status, msg = PullRequestModel().merge_status(pull_request)
183 assert status is False
183 assert status is False
184 assert (
184 assert (
185 msg.eval() ==
185 msg.eval() ==
186 'This pull request cannot be merged because of conflicts.')
186 'This pull request cannot be merged because of conflicts.')
187 self.merge_mock.assert_called_once_with(
187 self.merge_mock.assert_called_once_with(
188 pull_request.target_ref_parts,
188 pull_request.target_ref_parts,
189 pull_request.source_repo.scm_instance(),
189 pull_request.source_repo.scm_instance(),
190 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
190 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
191 use_rebase=False)
191 use_rebase=False)
192
192
193 assert pull_request._last_merge_source_rev == self.source_commit
193 assert pull_request._last_merge_source_rev == self.source_commit
194 assert pull_request._last_merge_target_rev == self.target_commit
194 assert pull_request._last_merge_target_rev == self.target_commit
195 assert (
195 assert (
196 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
196 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
197
197
198 self.merge_mock.reset_mock()
198 self.merge_mock.reset_mock()
199 status, msg = PullRequestModel().merge_status(pull_request)
199 status, msg = PullRequestModel().merge_status(pull_request)
200 assert status is False
200 assert status is False
201 assert (
201 assert (
202 msg.eval() ==
202 msg.eval() ==
203 'This pull request cannot be merged because of conflicts.')
203 'This pull request cannot be merged because of conflicts.')
204 assert self.merge_mock.called is False
204 assert self.merge_mock.called is False
205
205
206 def test_merge_status_unknown_failure(self, pull_request):
206 def test_merge_status_unknown_failure(self, pull_request):
207 self.merge_mock.return_value = MergeResponse(
207 self.merge_mock.return_value = MergeResponse(
208 False, False, None, MergeFailureReason.UNKNOWN)
208 False, False, None, MergeFailureReason.UNKNOWN)
209
209
210 assert pull_request._last_merge_source_rev is None
210 assert pull_request._last_merge_source_rev is None
211 assert pull_request._last_merge_target_rev is None
211 assert pull_request._last_merge_target_rev is None
212 assert pull_request._last_merge_status is None
212 assert pull_request._last_merge_status is None
213
213
214 status, msg = PullRequestModel().merge_status(pull_request)
214 status, msg = PullRequestModel().merge_status(pull_request)
215 assert status is False
215 assert status is False
216 assert msg.eval() == (
216 assert msg.eval() == (
217 'This pull request cannot be merged because of an unhandled'
217 'This pull request cannot be merged because of an unhandled'
218 ' exception.')
218 ' exception.')
219 self.merge_mock.assert_called_once_with(
219 self.merge_mock.assert_called_once_with(
220 pull_request.target_ref_parts,
220 pull_request.target_ref_parts,
221 pull_request.source_repo.scm_instance(),
221 pull_request.source_repo.scm_instance(),
222 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
222 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
223 use_rebase=False)
223 use_rebase=False)
224
224
225 assert pull_request._last_merge_source_rev is None
225 assert pull_request._last_merge_source_rev is None
226 assert pull_request._last_merge_target_rev is None
226 assert pull_request._last_merge_target_rev is None
227 assert pull_request._last_merge_status is None
227 assert pull_request._last_merge_status is None
228
228
229 self.merge_mock.reset_mock()
229 self.merge_mock.reset_mock()
230 status, msg = PullRequestModel().merge_status(pull_request)
230 status, msg = PullRequestModel().merge_status(pull_request)
231 assert status is False
231 assert status is False
232 assert msg.eval() == (
232 assert msg.eval() == (
233 'This pull request cannot be merged because of an unhandled'
233 'This pull request cannot be merged because of an unhandled'
234 ' exception.')
234 ' exception.')
235 assert self.merge_mock.called is True
235 assert self.merge_mock.called is True
236
236
237 def test_merge_status_when_target_is_locked(self, pull_request):
237 def test_merge_status_when_target_is_locked(self, pull_request):
238 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
238 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
239 status, msg = PullRequestModel().merge_status(pull_request)
239 status, msg = PullRequestModel().merge_status(pull_request)
240 assert status is False
240 assert status is False
241 assert msg.eval() == (
241 assert msg.eval() == (
242 'This pull request cannot be merged because the target repository'
242 'This pull request cannot be merged because the target repository'
243 ' is locked.')
243 ' is locked.')
244
244
245 def test_merge_status_requirements_check_target(self, pull_request):
245 def test_merge_status_requirements_check_target(self, pull_request):
246
246
247 def has_largefiles(self, repo):
247 def has_largefiles(self, repo):
248 return repo == pull_request.source_repo
248 return repo == pull_request.source_repo
249
249
250 patcher = mock.patch.object(
250 patcher = mock.patch.object(
251 PullRequestModel, '_has_largefiles', has_largefiles)
251 PullRequestModel, '_has_largefiles', has_largefiles)
252 with patcher:
252 with patcher:
253 status, msg = PullRequestModel().merge_status(pull_request)
253 status, msg = PullRequestModel().merge_status(pull_request)
254
254
255 assert status is False
255 assert status is False
256 assert msg == 'Target repository large files support is disabled.'
256 assert msg == 'Target repository large files support is disabled.'
257
257
258 def test_merge_status_requirements_check_source(self, pull_request):
258 def test_merge_status_requirements_check_source(self, pull_request):
259
259
260 def has_largefiles(self, repo):
260 def has_largefiles(self, repo):
261 return repo == pull_request.target_repo
261 return repo == pull_request.target_repo
262
262
263 patcher = mock.patch.object(
263 patcher = mock.patch.object(
264 PullRequestModel, '_has_largefiles', has_largefiles)
264 PullRequestModel, '_has_largefiles', has_largefiles)
265 with patcher:
265 with patcher:
266 status, msg = PullRequestModel().merge_status(pull_request)
266 status, msg = PullRequestModel().merge_status(pull_request)
267
267
268 assert status is False
268 assert status is False
269 assert msg == 'Source repository large files support is disabled.'
269 assert msg == 'Source repository large files support is disabled.'
270
270
271 def test_merge(self, pull_request, merge_extras):
271 def test_merge(self, pull_request, merge_extras):
272 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
272 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
273 merge_ref = Reference(
273 merge_ref = Reference(
274 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
274 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
275 self.merge_mock.return_value = MergeResponse(
275 self.merge_mock.return_value = MergeResponse(
276 True, True, merge_ref, MergeFailureReason.NONE)
276 True, True, merge_ref, MergeFailureReason.NONE)
277
277
278 merge_extras['repository'] = pull_request.target_repo.repo_name
278 merge_extras['repository'] = pull_request.target_repo.repo_name
279 PullRequestModel().merge(
279 PullRequestModel().merge(
280 pull_request, pull_request.author, extras=merge_extras)
280 pull_request, pull_request.author, extras=merge_extras)
281
281
282 message = (
282 message = (
283 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
283 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
284 u'\n\n {pr_title}'.format(
284 u'\n\n {pr_title}'.format(
285 pr_id=pull_request.pull_request_id,
285 pr_id=pull_request.pull_request_id,
286 source_repo=safe_unicode(
286 source_repo=safe_unicode(
287 pull_request.source_repo.scm_instance().name),
287 pull_request.source_repo.scm_instance().name),
288 source_ref_name=pull_request.source_ref_parts.name,
288 source_ref_name=pull_request.source_ref_parts.name,
289 pr_title=safe_unicode(pull_request.title)
289 pr_title=safe_unicode(pull_request.title)
290 )
290 )
291 )
291 )
292 self.merge_mock.assert_called_once_with(
292 self.merge_mock.assert_called_once_with(
293 pull_request.target_ref_parts,
293 pull_request.target_ref_parts,
294 pull_request.source_repo.scm_instance(),
294 pull_request.source_repo.scm_instance(),
295 pull_request.source_ref_parts, self.workspace_id,
295 pull_request.source_ref_parts, self.workspace_id,
296 user_name=user.username, user_email=user.email, message=message,
296 user_name=user.username, user_email=user.email, message=message,
297 use_rebase=False
297 use_rebase=False
298 )
298 )
299 self.invalidation_mock.assert_called_once_with(
299 self.invalidation_mock.assert_called_once_with(
300 pull_request.target_repo.repo_name)
300 pull_request.target_repo.repo_name)
301
301
302 self.hook_mock.assert_called_with(
302 self.hook_mock.assert_called_with(
303 self.pull_request, self.pull_request.author, 'merge')
303 self.pull_request, self.pull_request.author, 'merge')
304
304
305 pull_request = PullRequest.get(pull_request.pull_request_id)
305 pull_request = PullRequest.get(pull_request.pull_request_id)
306 assert (
306 assert (
307 pull_request.merge_rev ==
307 pull_request.merge_rev ==
308 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
308 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
309
309
310 def test_merge_failed(self, pull_request, merge_extras):
310 def test_merge_failed(self, pull_request, merge_extras):
311 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
311 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
312 merge_ref = Reference(
312 merge_ref = Reference(
313 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314 self.merge_mock.return_value = MergeResponse(
314 self.merge_mock.return_value = MergeResponse(
315 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
315 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
316
316
317 merge_extras['repository'] = pull_request.target_repo.repo_name
317 merge_extras['repository'] = pull_request.target_repo.repo_name
318 PullRequestModel().merge(
318 PullRequestModel().merge(
319 pull_request, pull_request.author, extras=merge_extras)
319 pull_request, pull_request.author, extras=merge_extras)
320
320
321 message = (
321 message = (
322 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
322 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
323 u'\n\n {pr_title}'.format(
323 u'\n\n {pr_title}'.format(
324 pr_id=pull_request.pull_request_id,
324 pr_id=pull_request.pull_request_id,
325 source_repo=safe_unicode(
325 source_repo=safe_unicode(
326 pull_request.source_repo.scm_instance().name),
326 pull_request.source_repo.scm_instance().name),
327 source_ref_name=pull_request.source_ref_parts.name,
327 source_ref_name=pull_request.source_ref_parts.name,
328 pr_title=safe_unicode(pull_request.title)
328 pr_title=safe_unicode(pull_request.title)
329 )
329 )
330 )
330 )
331 self.merge_mock.assert_called_once_with(
331 self.merge_mock.assert_called_once_with(
332 pull_request.target_ref_parts,
332 pull_request.target_ref_parts,
333 pull_request.source_repo.scm_instance(),
333 pull_request.source_repo.scm_instance(),
334 pull_request.source_ref_parts, self.workspace_id,
334 pull_request.source_ref_parts, self.workspace_id,
335 user_name=user.username, user_email=user.email, message=message,
335 user_name=user.username, user_email=user.email, message=message,
336 use_rebase=False
336 use_rebase=False
337 )
337 )
338
338
339 pull_request = PullRequest.get(pull_request.pull_request_id)
339 pull_request = PullRequest.get(pull_request.pull_request_id)
340 assert self.invalidation_mock.called is False
340 assert self.invalidation_mock.called is False
341 assert pull_request.merge_rev is None
341 assert pull_request.merge_rev is None
342
342
343 def test_get_commit_ids(self, pull_request):
343 def test_get_commit_ids(self, pull_request):
344 # The PR has been not merget yet, so expect an exception
344 # The PR has been not merget yet, so expect an exception
345 with pytest.raises(ValueError):
345 with pytest.raises(ValueError):
346 PullRequestModel()._get_commit_ids(pull_request)
346 PullRequestModel()._get_commit_ids(pull_request)
347
347
348 # Merge revision is in the revisions list
348 # Merge revision is in the revisions list
349 pull_request.merge_rev = pull_request.revisions[0]
349 pull_request.merge_rev = pull_request.revisions[0]
350 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
350 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
351 assert commit_ids == pull_request.revisions
351 assert commit_ids == pull_request.revisions
352
352
353 # Merge revision is not in the revisions list
353 # Merge revision is not in the revisions list
354 pull_request.merge_rev = 'f000' * 10
354 pull_request.merge_rev = 'f000' * 10
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
356 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
357
357
358 def test_get_diff_from_pr_version(self, pull_request):
358 def test_get_diff_from_pr_version(self, pull_request):
359 diff = PullRequestModel()._get_diff_from_pr_or_version(
359 diff = PullRequestModel()._get_diff_from_pr_or_version(
360 pull_request, context=6)
360 pull_request, context=6)
361 assert 'file_1' in diff.raw
361 assert 'file_1' in diff.raw
362
362
363 def test_generate_title_returns_unicode(self):
363 def test_generate_title_returns_unicode(self):
364 title = PullRequestModel().generate_pullrequest_title(
364 title = PullRequestModel().generate_pullrequest_title(
365 source='source-dummy',
365 source='source-dummy',
366 source_ref='source-ref-dummy',
366 source_ref='source-ref-dummy',
367 target='target-dummy',
367 target='target-dummy',
368 )
368 )
369 assert type(title) == unicode
369 assert type(title) == unicode
370
370
371
371
372 class TestIntegrationMerge(object):
372 class TestIntegrationMerge(object):
373 @pytest.mark.parametrize('extra_config', (
373 @pytest.mark.parametrize('extra_config', (
374 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
374 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
375 {'vcs.hooks.protocol': 'Pyro4', 'vcs.hooks.direct_calls': False},
375 {'vcs.hooks.protocol': 'Pyro4', 'vcs.hooks.direct_calls': False},
376 ))
376 ))
377 def test_merge_triggers_push_hooks(
377 def test_merge_triggers_push_hooks(
378 self, pr_util, user_admin, capture_rcextensions, merge_extras,
378 self, pr_util, user_admin, capture_rcextensions, merge_extras,
379 extra_config):
379 extra_config):
380 pull_request = pr_util.create_pull_request(
380 pull_request = pr_util.create_pull_request(
381 approved=True, mergeable=True)
381 approved=True, mergeable=True)
382 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
382 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
383 merge_extras['repository'] = pull_request.target_repo.repo_name
383 merge_extras['repository'] = pull_request.target_repo.repo_name
384 Session().commit()
384 Session().commit()
385
385
386 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
386 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
387 merge_state = PullRequestModel().merge(
387 merge_state = PullRequestModel().merge(
388 pull_request, user_admin, extras=merge_extras)
388 pull_request, user_admin, extras=merge_extras)
389
389
390 assert merge_state.executed
390 assert merge_state.executed
391 assert 'pre_push' in capture_rcextensions
391 assert 'pre_push' in capture_rcextensions
392 assert 'post_push' in capture_rcextensions
392 assert 'post_push' in capture_rcextensions
393
393
394 def test_merge_can_be_rejected_by_pre_push_hook(
394 def test_merge_can_be_rejected_by_pre_push_hook(
395 self, pr_util, user_admin, capture_rcextensions, merge_extras):
395 self, pr_util, user_admin, capture_rcextensions, merge_extras):
396 pull_request = pr_util.create_pull_request(
396 pull_request = pr_util.create_pull_request(
397 approved=True, mergeable=True)
397 approved=True, mergeable=True)
398 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
398 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
399 merge_extras['repository'] = pull_request.target_repo.repo_name
399 merge_extras['repository'] = pull_request.target_repo.repo_name
400 Session().commit()
400 Session().commit()
401
401
402 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
402 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
403 pre_pull.side_effect = RepositoryError("Disallow push!")
403 pre_pull.side_effect = RepositoryError("Disallow push!")
404 merge_status = PullRequestModel().merge(
404 merge_status = PullRequestModel().merge(
405 pull_request, user_admin, extras=merge_extras)
405 pull_request, user_admin, extras=merge_extras)
406
406
407 assert not merge_status.executed
407 assert not merge_status.executed
408 assert 'pre_push' not in capture_rcextensions
408 assert 'pre_push' not in capture_rcextensions
409 assert 'post_push' not in capture_rcextensions
409 assert 'post_push' not in capture_rcextensions
410
410
411 def test_merge_fails_if_target_is_locked(
411 def test_merge_fails_if_target_is_locked(
412 self, pr_util, user_regular, merge_extras):
412 self, pr_util, user_regular, merge_extras):
413 pull_request = pr_util.create_pull_request(
413 pull_request = pr_util.create_pull_request(
414 approved=True, mergeable=True)
414 approved=True, mergeable=True)
415 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
415 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
416 pull_request.target_repo.locked = locked_by
416 pull_request.target_repo.locked = locked_by
417 # TODO: johbo: Check if this can work based on the database, currently
417 # TODO: johbo: Check if this can work based on the database, currently
418 # all data is pre-computed, that's why just updating the DB is not
418 # all data is pre-computed, that's why just updating the DB is not
419 # enough.
419 # enough.
420 merge_extras['locked_by'] = locked_by
420 merge_extras['locked_by'] = locked_by
421 merge_extras['repository'] = pull_request.target_repo.repo_name
421 merge_extras['repository'] = pull_request.target_repo.repo_name
422 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
422 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
423 Session().commit()
423 Session().commit()
424 merge_status = PullRequestModel().merge(
424 merge_status = PullRequestModel().merge(
425 pull_request, user_regular, extras=merge_extras)
425 pull_request, user_regular, extras=merge_extras)
426 assert not merge_status.executed
426 assert not merge_status.executed
427
427
428
428
429 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
429 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
430 (False, 1, 0),
430 (False, 1, 0),
431 (True, 0, 1),
431 (True, 0, 1),
432 ])
432 ])
433 def test_outdated_comments(
433 def test_outdated_comments(
434 pr_util, use_outdated, inlines_count, outdated_count):
434 pr_util, use_outdated, inlines_count, outdated_count):
435 pull_request = pr_util.create_pull_request()
435 pull_request = pr_util.create_pull_request()
436 pr_util.create_inline_comment(file_path='not_in_updated_diff')
436 pr_util.create_inline_comment(file_path='not_in_updated_diff')
437
437
438 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
438 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
439 pr_util.add_one_commit()
439 pr_util.add_one_commit()
440 assert_inline_comments(
440 assert_inline_comments(
441 pull_request, visible=inlines_count, outdated=outdated_count)
441 pull_request, visible=inlines_count, outdated=outdated_count)
442 outdated_comment_mock.assert_called_with(pull_request)
442 outdated_comment_mock.assert_called_with(pull_request)
443
443
444
444
445 @pytest.fixture
445 @pytest.fixture
446 def merge_extras(user_regular):
446 def merge_extras(user_regular):
447 """
447 """
448 Context for the vcs operation when running a merge.
448 Context for the vcs operation when running a merge.
449 """
449 """
450 extras = {
450 extras = {
451 'ip': '127.0.0.1',
451 'ip': '127.0.0.1',
452 'username': user_regular.username,
452 'username': user_regular.username,
453 'action': 'push',
453 'action': 'push',
454 'repository': 'fake_target_repo_name',
454 'repository': 'fake_target_repo_name',
455 'scm': 'git',
455 'scm': 'git',
456 'config': 'fake_config_ini_path',
456 'config': 'fake_config_ini_path',
457 'make_lock': None,
457 'make_lock': None,
458 'locked_by': [None, None, None],
458 'locked_by': [None, None, None],
459 'server_url': 'http://test.example.com:5000',
459 'server_url': 'http://test.example.com:5000',
460 'hooks': ['push', 'pull'],
460 'hooks': ['push', 'pull'],
461 'is_shadow_repo': False,
461 'is_shadow_repo': False,
462 }
462 }
463 return extras
463 return extras
464
464
465
465
466 class TestUpdateCommentHandling(object):
466 class TestUpdateCommentHandling(object):
467
467
468 @pytest.fixture(autouse=True, scope='class')
468 @pytest.fixture(autouse=True, scope='class')
469 def enable_outdated_comments(self, request, pylonsapp):
469 def enable_outdated_comments(self, request, pylonsapp):
470 config_patch = mock.patch.dict(
470 config_patch = mock.patch.dict(
471 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
471 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
472 config_patch.start()
472 config_patch.start()
473
473
474 @request.addfinalizer
474 @request.addfinalizer
475 def cleanup():
475 def cleanup():
476 config_patch.stop()
476 config_patch.stop()
477
477
478 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
478 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
479 commits = [
479 commits = [
480 {'message': 'a'},
480 {'message': 'a'},
481 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
481 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
482 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
482 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
483 ]
483 ]
484 pull_request = pr_util.create_pull_request(
484 pull_request = pr_util.create_pull_request(
485 commits=commits, target_head='a', source_head='b', revisions=['b'])
485 commits=commits, target_head='a', source_head='b', revisions=['b'])
486 pr_util.create_inline_comment(file_path='file_b')
486 pr_util.create_inline_comment(file_path='file_b')
487 pr_util.add_one_commit(head='c')
487 pr_util.add_one_commit(head='c')
488
488
489 assert_inline_comments(pull_request, visible=1, outdated=0)
489 assert_inline_comments(pull_request, visible=1, outdated=0)
490
490
491 def test_comment_stays_unflagged_on_change_above(self, pr_util):
491 def test_comment_stays_unflagged_on_change_above(self, pr_util):
492 original_content = ''.join(
492 original_content = ''.join(
493 ['line {}\n'.format(x) for x in range(1, 11)])
493 ['line {}\n'.format(x) for x in range(1, 11)])
494 updated_content = 'new_line_at_top\n' + original_content
494 updated_content = 'new_line_at_top\n' + original_content
495 commits = [
495 commits = [
496 {'message': 'a'},
496 {'message': 'a'},
497 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
497 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
498 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
498 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
499 ]
499 ]
500 pull_request = pr_util.create_pull_request(
500 pull_request = pr_util.create_pull_request(
501 commits=commits, target_head='a', source_head='b', revisions=['b'])
501 commits=commits, target_head='a', source_head='b', revisions=['b'])
502
502
503 with outdated_comments_patcher():
503 with outdated_comments_patcher():
504 comment = pr_util.create_inline_comment(
504 comment = pr_util.create_inline_comment(
505 line_no=u'n8', file_path='file_b')
505 line_no=u'n8', file_path='file_b')
506 pr_util.add_one_commit(head='c')
506 pr_util.add_one_commit(head='c')
507
507
508 assert_inline_comments(pull_request, visible=1, outdated=0)
508 assert_inline_comments(pull_request, visible=1, outdated=0)
509 assert comment.line_no == u'n9'
509 assert comment.line_no == u'n9'
510
510
511 def test_comment_stays_unflagged_on_change_below(self, pr_util):
511 def test_comment_stays_unflagged_on_change_below(self, pr_util):
512 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
512 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
513 updated_content = original_content + 'new_line_at_end\n'
513 updated_content = original_content + 'new_line_at_end\n'
514 commits = [
514 commits = [
515 {'message': 'a'},
515 {'message': 'a'},
516 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
516 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
517 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
517 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
518 ]
518 ]
519 pull_request = pr_util.create_pull_request(
519 pull_request = pr_util.create_pull_request(
520 commits=commits, target_head='a', source_head='b', revisions=['b'])
520 commits=commits, target_head='a', source_head='b', revisions=['b'])
521 pr_util.create_inline_comment(file_path='file_b')
521 pr_util.create_inline_comment(file_path='file_b')
522 pr_util.add_one_commit(head='c')
522 pr_util.add_one_commit(head='c')
523
523
524 assert_inline_comments(pull_request, visible=1, outdated=0)
524 assert_inline_comments(pull_request, visible=1, outdated=0)
525
525
526 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
526 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
527 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
527 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
528 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
528 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
529 change_lines = list(base_lines)
529 change_lines = list(base_lines)
530 change_lines.insert(6, 'line 6a added\n')
530 change_lines.insert(6, 'line 6a added\n')
531
531
532 # Changes on the last line of sight
532 # Changes on the last line of sight
533 update_lines = list(change_lines)
533 update_lines = list(change_lines)
534 update_lines[0] = 'line 1 changed\n'
534 update_lines[0] = 'line 1 changed\n'
535 update_lines[-1] = 'line 12 changed\n'
535 update_lines[-1] = 'line 12 changed\n'
536
536
537 def file_b(lines):
537 def file_b(lines):
538 return FileNode('file_b', ''.join(lines))
538 return FileNode('file_b', ''.join(lines))
539
539
540 commits = [
540 commits = [
541 {'message': 'a', 'added': [file_b(base_lines)]},
541 {'message': 'a', 'added': [file_b(base_lines)]},
542 {'message': 'b', 'changed': [file_b(change_lines)]},
542 {'message': 'b', 'changed': [file_b(change_lines)]},
543 {'message': 'c', 'changed': [file_b(update_lines)]},
543 {'message': 'c', 'changed': [file_b(update_lines)]},
544 ]
544 ]
545
545
546 pull_request = pr_util.create_pull_request(
546 pull_request = pr_util.create_pull_request(
547 commits=commits, target_head='a', source_head='b', revisions=['b'])
547 commits=commits, target_head='a', source_head='b', revisions=['b'])
548 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
548 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
549
549
550 with outdated_comments_patcher():
550 with outdated_comments_patcher():
551 pr_util.add_one_commit(head='c')
551 pr_util.add_one_commit(head='c')
552 assert_inline_comments(pull_request, visible=0, outdated=1)
552 assert_inline_comments(pull_request, visible=0, outdated=1)
553
553
554 @pytest.mark.parametrize("change, content", [
554 @pytest.mark.parametrize("change, content", [
555 ('changed', 'changed\n'),
555 ('changed', 'changed\n'),
556 ('removed', ''),
556 ('removed', ''),
557 ], ids=['changed', 'removed'])
557 ], ids=['changed', 'removed'])
558 def test_comment_flagged_on_change(self, pr_util, change, content):
558 def test_comment_flagged_on_change(self, pr_util, change, content):
559 commits = [
559 commits = [
560 {'message': 'a'},
560 {'message': 'a'},
561 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
561 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
562 {'message': 'c', change: [FileNode('file_b', content)]},
562 {'message': 'c', change: [FileNode('file_b', content)]},
563 ]
563 ]
564 pull_request = pr_util.create_pull_request(
564 pull_request = pr_util.create_pull_request(
565 commits=commits, target_head='a', source_head='b', revisions=['b'])
565 commits=commits, target_head='a', source_head='b', revisions=['b'])
566 pr_util.create_inline_comment(file_path='file_b')
566 pr_util.create_inline_comment(file_path='file_b')
567
567
568 with outdated_comments_patcher():
568 with outdated_comments_patcher():
569 pr_util.add_one_commit(head='c')
569 pr_util.add_one_commit(head='c')
570 assert_inline_comments(pull_request, visible=0, outdated=1)
570 assert_inline_comments(pull_request, visible=0, outdated=1)
571
571
572
572
573 class TestUpdateChangedFiles(object):
573 class TestUpdateChangedFiles(object):
574
574
575 def test_no_changes_on_unchanged_diff(self, pr_util):
575 def test_no_changes_on_unchanged_diff(self, pr_util):
576 commits = [
576 commits = [
577 {'message': 'a'},
577 {'message': 'a'},
578 {'message': 'b',
578 {'message': 'b',
579 'added': [FileNode('file_b', 'test_content b\n')]},
579 'added': [FileNode('file_b', 'test_content b\n')]},
580 {'message': 'c',
580 {'message': 'c',
581 'added': [FileNode('file_c', 'test_content c\n')]},
581 'added': [FileNode('file_c', 'test_content c\n')]},
582 ]
582 ]
583 # open a PR from a to b, adding file_b
583 # open a PR from a to b, adding file_b
584 pull_request = pr_util.create_pull_request(
584 pull_request = pr_util.create_pull_request(
585 commits=commits, target_head='a', source_head='b', revisions=['b'],
585 commits=commits, target_head='a', source_head='b', revisions=['b'],
586 name_suffix='per-file-review')
586 name_suffix='per-file-review')
587
587
588 # modify PR adding new file file_c
588 # modify PR adding new file file_c
589 pr_util.add_one_commit(head='c')
589 pr_util.add_one_commit(head='c')
590
590
591 assert_pr_file_changes(
591 assert_pr_file_changes(
592 pull_request,
592 pull_request,
593 added=['file_c'],
593 added=['file_c'],
594 modified=[],
594 modified=[],
595 removed=[])
595 removed=[])
596
596
597 def test_modify_and_undo_modification_diff(self, pr_util):
597 def test_modify_and_undo_modification_diff(self, pr_util):
598 commits = [
598 commits = [
599 {'message': 'a'},
599 {'message': 'a'},
600 {'message': 'b',
600 {'message': 'b',
601 'added': [FileNode('file_b', 'test_content b\n')]},
601 'added': [FileNode('file_b', 'test_content b\n')]},
602 {'message': 'c',
602 {'message': 'c',
603 'changed': [FileNode('file_b', 'test_content b modified\n')]},
603 'changed': [FileNode('file_b', 'test_content b modified\n')]},
604 {'message': 'd',
604 {'message': 'd',
605 'changed': [FileNode('file_b', 'test_content b\n')]},
605 'changed': [FileNode('file_b', 'test_content b\n')]},
606 ]
606 ]
607 # open a PR from a to b, adding file_b
607 # open a PR from a to b, adding file_b
608 pull_request = pr_util.create_pull_request(
608 pull_request = pr_util.create_pull_request(
609 commits=commits, target_head='a', source_head='b', revisions=['b'],
609 commits=commits, target_head='a', source_head='b', revisions=['b'],
610 name_suffix='per-file-review')
610 name_suffix='per-file-review')
611
611
612 # modify PR modifying file file_b
612 # modify PR modifying file file_b
613 pr_util.add_one_commit(head='c')
613 pr_util.add_one_commit(head='c')
614
614
615 assert_pr_file_changes(
615 assert_pr_file_changes(
616 pull_request,
616 pull_request,
617 added=[],
617 added=[],
618 modified=['file_b'],
618 modified=['file_b'],
619 removed=[])
619 removed=[])
620
620
621 # move the head again to d, which rollbacks change,
621 # move the head again to d, which rollbacks change,
622 # meaning we should indicate no changes
622 # meaning we should indicate no changes
623 pr_util.add_one_commit(head='d')
623 pr_util.add_one_commit(head='d')
624
624
625 assert_pr_file_changes(
625 assert_pr_file_changes(
626 pull_request,
626 pull_request,
627 added=[],
627 added=[],
628 modified=[],
628 modified=[],
629 removed=[])
629 removed=[])
630
630
631 def test_updated_all_files_in_pr(self, pr_util):
631 def test_updated_all_files_in_pr(self, pr_util):
632 commits = [
632 commits = [
633 {'message': 'a'},
633 {'message': 'a'},
634 {'message': 'b', 'added': [
634 {'message': 'b', 'added': [
635 FileNode('file_a', 'test_content a\n'),
635 FileNode('file_a', 'test_content a\n'),
636 FileNode('file_b', 'test_content b\n'),
636 FileNode('file_b', 'test_content b\n'),
637 FileNode('file_c', 'test_content c\n')]},
637 FileNode('file_c', 'test_content c\n')]},
638 {'message': 'c', 'changed': [
638 {'message': 'c', 'changed': [
639 FileNode('file_a', 'test_content a changed\n'),
639 FileNode('file_a', 'test_content a changed\n'),
640 FileNode('file_b', 'test_content b changed\n'),
640 FileNode('file_b', 'test_content b changed\n'),
641 FileNode('file_c', 'test_content c changed\n')]},
641 FileNode('file_c', 'test_content c changed\n')]},
642 ]
642 ]
643 # open a PR from a to b, changing 3 files
643 # open a PR from a to b, changing 3 files
644 pull_request = pr_util.create_pull_request(
644 pull_request = pr_util.create_pull_request(
645 commits=commits, target_head='a', source_head='b', revisions=['b'],
645 commits=commits, target_head='a', source_head='b', revisions=['b'],
646 name_suffix='per-file-review')
646 name_suffix='per-file-review')
647
647
648 pr_util.add_one_commit(head='c')
648 pr_util.add_one_commit(head='c')
649
649
650 assert_pr_file_changes(
650 assert_pr_file_changes(
651 pull_request,
651 pull_request,
652 added=[],
652 added=[],
653 modified=['file_a', 'file_b', 'file_c'],
653 modified=['file_a', 'file_b', 'file_c'],
654 removed=[])
654 removed=[])
655
655
656 def test_updated_and_removed_all_files_in_pr(self, pr_util):
656 def test_updated_and_removed_all_files_in_pr(self, pr_util):
657 commits = [
657 commits = [
658 {'message': 'a'},
658 {'message': 'a'},
659 {'message': 'b', 'added': [
659 {'message': 'b', 'added': [
660 FileNode('file_a', 'test_content a\n'),
660 FileNode('file_a', 'test_content a\n'),
661 FileNode('file_b', 'test_content b\n'),
661 FileNode('file_b', 'test_content b\n'),
662 FileNode('file_c', 'test_content c\n')]},
662 FileNode('file_c', 'test_content c\n')]},
663 {'message': 'c', 'removed': [
663 {'message': 'c', 'removed': [
664 FileNode('file_a', 'test_content a changed\n'),
664 FileNode('file_a', 'test_content a changed\n'),
665 FileNode('file_b', 'test_content b changed\n'),
665 FileNode('file_b', 'test_content b changed\n'),
666 FileNode('file_c', 'test_content c changed\n')]},
666 FileNode('file_c', 'test_content c changed\n')]},
667 ]
667 ]
668 # open a PR from a to b, removing 3 files
668 # open a PR from a to b, removing 3 files
669 pull_request = pr_util.create_pull_request(
669 pull_request = pr_util.create_pull_request(
670 commits=commits, target_head='a', source_head='b', revisions=['b'],
670 commits=commits, target_head='a', source_head='b', revisions=['b'],
671 name_suffix='per-file-review')
671 name_suffix='per-file-review')
672
672
673 pr_util.add_one_commit(head='c')
673 pr_util.add_one_commit(head='c')
674
674
675 assert_pr_file_changes(
675 assert_pr_file_changes(
676 pull_request,
676 pull_request,
677 added=[],
677 added=[],
678 modified=[],
678 modified=[],
679 removed=['file_a', 'file_b', 'file_c'])
679 removed=['file_a', 'file_b', 'file_c'])
680
680
681
681
682 def test_update_writes_snapshot_into_pull_request_version(pr_util):
682 def test_update_writes_snapshot_into_pull_request_version(pr_util):
683 model = PullRequestModel()
683 model = PullRequestModel()
684 pull_request = pr_util.create_pull_request()
684 pull_request = pr_util.create_pull_request()
685 pr_util.update_source_repository()
685 pr_util.update_source_repository()
686
686
687 model.update_commits(pull_request)
687 model.update_commits(pull_request)
688
688
689 # Expect that it has a version entry now
689 # Expect that it has a version entry now
690 assert len(model.get_versions(pull_request)) == 1
690 assert len(model.get_versions(pull_request)) == 1
691
691
692
692
693 def test_update_skips_new_version_if_unchanged(pr_util):
693 def test_update_skips_new_version_if_unchanged(pr_util):
694 pull_request = pr_util.create_pull_request()
694 pull_request = pr_util.create_pull_request()
695 model = PullRequestModel()
695 model = PullRequestModel()
696 model.update_commits(pull_request)
696 model.update_commits(pull_request)
697
697
698 # Expect that it still has no versions
698 # Expect that it still has no versions
699 assert len(model.get_versions(pull_request)) == 0
699 assert len(model.get_versions(pull_request)) == 0
700
700
701
701
702 def test_update_assigns_comments_to_the_new_version(pr_util):
702 def test_update_assigns_comments_to_the_new_version(pr_util):
703 model = PullRequestModel()
703 model = PullRequestModel()
704 pull_request = pr_util.create_pull_request()
704 pull_request = pr_util.create_pull_request()
705 comment = pr_util.create_comment()
705 comment = pr_util.create_comment()
706 pr_util.update_source_repository()
706 pr_util.update_source_repository()
707
707
708 model.update_commits(pull_request)
708 model.update_commits(pull_request)
709
709
710 # Expect that the comment is linked to the pr version now
710 # Expect that the comment is linked to the pr version now
711 assert comment.pull_request_version == model.get_versions(pull_request)[0]
711 assert comment.pull_request_version == model.get_versions(pull_request)[0]
712
712
713
713
714 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util):
714 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util):
715 model = PullRequestModel()
715 model = PullRequestModel()
716 pull_request = pr_util.create_pull_request()
716 pull_request = pr_util.create_pull_request()
717 pr_util.update_source_repository()
717 pr_util.update_source_repository()
718 pr_util.update_source_repository()
718 pr_util.update_source_repository()
719
719
720 model.update_commits(pull_request)
720 model.update_commits(pull_request)
721
721
722 # Expect to find a new comment about the change
722 # Expect to find a new comment about the change
723 expected_message = textwrap.dedent(
723 expected_message = textwrap.dedent(
724 """\
724 """\
725 Auto status change to |under_review|
725 Auto status change to |under_review|
726
726
727 .. role:: added
727 .. role:: added
728 .. role:: removed
728 .. role:: removed
729 .. parsed-literal::
729 .. parsed-literal::
730
730
731 Changed commits:
731 Changed commits:
732 * :added:`1 added`
732 * :added:`1 added`
733 * :removed:`0 removed`
733 * :removed:`0 removed`
734
734
735 Changed files:
735 Changed files:
736 * `A file_2 <#a_c--92ed3b5f07b4>`_
736 * `A file_2 <#a_c--92ed3b5f07b4>`_
737
737
738 .. |under_review| replace:: *"Under Review"*"""
738 .. |under_review| replace:: *"Under Review"*"""
739 )
739 )
740 pull_request_comments = sorted(
740 pull_request_comments = sorted(
741 pull_request.comments, key=lambda c: c.modified_at)
741 pull_request.comments, key=lambda c: c.modified_at)
742 update_comment = pull_request_comments[-1]
742 update_comment = pull_request_comments[-1]
743 assert update_comment.text == expected_message
743 assert update_comment.text == expected_message
744
744
745
745
746 def test_create_version_from_snapshot_updates_attributes(pr_util):
746 def test_create_version_from_snapshot_updates_attributes(pr_util):
747 pull_request = pr_util.create_pull_request()
747 pull_request = pr_util.create_pull_request()
748
748
749 # Avoiding default values
749 # Avoiding default values
750 pull_request.status = PullRequest.STATUS_CLOSED
750 pull_request.status = PullRequest.STATUS_CLOSED
751 pull_request._last_merge_source_rev = "0" * 40
751 pull_request._last_merge_source_rev = "0" * 40
752 pull_request._last_merge_target_rev = "1" * 40
752 pull_request._last_merge_target_rev = "1" * 40
753 pull_request._last_merge_status = 1
753 pull_request._last_merge_status = 1
754 pull_request.merge_rev = "2" * 40
754 pull_request.merge_rev = "2" * 40
755
755
756 # Remember automatic values
756 # Remember automatic values
757 created_on = pull_request.created_on
757 created_on = pull_request.created_on
758 updated_on = pull_request.updated_on
758 updated_on = pull_request.updated_on
759
759
760 # Create a new version of the pull request
760 # Create a new version of the pull request
761 version = PullRequestModel()._create_version_from_snapshot(pull_request)
761 version = PullRequestModel()._create_version_from_snapshot(pull_request)
762
762
763 # Check attributes
763 # Check attributes
764 assert version.title == pr_util.create_parameters['title']
764 assert version.title == pr_util.create_parameters['title']
765 assert version.description == pr_util.create_parameters['description']
765 assert version.description == pr_util.create_parameters['description']
766 assert version.status == PullRequest.STATUS_CLOSED
766 assert version.status == PullRequest.STATUS_CLOSED
767 assert version.created_on == created_on
767 assert version.created_on == created_on
768 assert version.updated_on == updated_on
768 assert version.updated_on == updated_on
769 assert version.user_id == pull_request.user_id
769 assert version.user_id == pull_request.user_id
770 assert version.revisions == pr_util.create_parameters['revisions']
770 assert version.revisions == pr_util.create_parameters['revisions']
771 assert version.source_repo == pr_util.source_repository
771 assert version.source_repo == pr_util.source_repository
772 assert version.source_ref == pr_util.create_parameters['source_ref']
772 assert version.source_ref == pr_util.create_parameters['source_ref']
773 assert version.target_repo == pr_util.target_repository
773 assert version.target_repo == pr_util.target_repository
774 assert version.target_ref == pr_util.create_parameters['target_ref']
774 assert version.target_ref == pr_util.create_parameters['target_ref']
775 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
775 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
776 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
776 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
777 assert version._last_merge_status == pull_request._last_merge_status
777 assert version._last_merge_status == pull_request._last_merge_status
778 assert version.merge_rev == pull_request.merge_rev
778 assert version.merge_rev == pull_request.merge_rev
779 assert version.pull_request == pull_request
779 assert version.pull_request == pull_request
780
780
781
781
782 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util):
782 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util):
783 version1 = pr_util.create_version_of_pull_request()
783 version1 = pr_util.create_version_of_pull_request()
784 comment_linked = pr_util.create_comment(linked_to=version1)
784 comment_linked = pr_util.create_comment(linked_to=version1)
785 comment_unlinked = pr_util.create_comment()
785 comment_unlinked = pr_util.create_comment()
786 version2 = pr_util.create_version_of_pull_request()
786 version2 = pr_util.create_version_of_pull_request()
787
787
788 PullRequestModel()._link_comments_to_version(version2)
788 PullRequestModel()._link_comments_to_version(version2)
789
789
790 # Expect that only the new comment is linked to version2
790 # Expect that only the new comment is linked to version2
791 assert (
791 assert (
792 comment_unlinked.pull_request_version_id ==
792 comment_unlinked.pull_request_version_id ==
793 version2.pull_request_version_id)
793 version2.pull_request_version_id)
794 assert (
794 assert (
795 comment_linked.pull_request_version_id ==
795 comment_linked.pull_request_version_id ==
796 version1.pull_request_version_id)
796 version1.pull_request_version_id)
797 assert (
797 assert (
798 comment_unlinked.pull_request_version_id !=
798 comment_unlinked.pull_request_version_id !=
799 comment_linked.pull_request_version_id)
799 comment_linked.pull_request_version_id)
800
800
801
801
802 def test_calculate_commits():
802 def test_calculate_commits():
803 change = PullRequestModel()._calculate_commit_id_changes(
803 change = PullRequestModel()._calculate_commit_id_changes(
804 set([1, 2, 3]), set([1, 3, 4, 5]))
804 set([1, 2, 3]), set([1, 3, 4, 5]))
805 assert (set([4, 5]), set([1, 3]), set([2])) == (
805 assert (set([4, 5]), set([1, 3]), set([2])) == (
806 change.added, change.common, change.removed)
806 change.added, change.common, change.removed)
807
807
808
808
809 def assert_inline_comments(pull_request, visible=None, outdated=None):
809 def assert_inline_comments(pull_request, visible=None, outdated=None):
810 if visible is not None:
810 if visible is not None:
811 inline_comments = ChangesetCommentsModel().get_inline_comments(
811 inline_comments = ChangesetCommentsModel().get_inline_comments(
812 pull_request.target_repo.repo_id, pull_request=pull_request)
812 pull_request.target_repo.repo_id, pull_request=pull_request)
813 assert len(inline_comments) == visible
813 inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
814 inline_comments)
815 assert inline_cnt == visible
814 if outdated is not None:
816 if outdated is not None:
815 outdated_comments = ChangesetCommentsModel().get_outdated_comments(
817 outdated_comments = ChangesetCommentsModel().get_outdated_comments(
816 pull_request.target_repo.repo_id, pull_request)
818 pull_request.target_repo.repo_id, pull_request)
817 assert len(outdated_comments) == outdated
819 assert len(outdated_comments) == outdated
818
820
819
821
820 def assert_pr_file_changes(
822 def assert_pr_file_changes(
821 pull_request, added=None, modified=None, removed=None):
823 pull_request, added=None, modified=None, removed=None):
822 pr_versions = PullRequestModel().get_versions(pull_request)
824 pr_versions = PullRequestModel().get_versions(pull_request)
823 # always use first version, ie original PR to calculate changes
825 # always use first version, ie original PR to calculate changes
824 pull_request_version = pr_versions[0]
826 pull_request_version = pr_versions[0]
825 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
827 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
826 pull_request, pull_request_version)
828 pull_request, pull_request_version)
827 file_changes = PullRequestModel()._calculate_file_changes(
829 file_changes = PullRequestModel()._calculate_file_changes(
828 old_diff_data, new_diff_data)
830 old_diff_data, new_diff_data)
829
831
830 assert added == file_changes.added, \
832 assert added == file_changes.added, \
831 'expected added:%s vs value:%s' % (added, file_changes.added)
833 'expected added:%s vs value:%s' % (added, file_changes.added)
832 assert modified == file_changes.modified, \
834 assert modified == file_changes.modified, \
833 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
835 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
834 assert removed == file_changes.removed, \
836 assert removed == file_changes.removed, \
835 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
837 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
836
838
837
839
838 def outdated_comments_patcher(use_outdated=True):
840 def outdated_comments_patcher(use_outdated=True):
839 return mock.patch.object(
841 return mock.patch.object(
840 ChangesetCommentsModel, 'use_outdated_comments',
842 ChangesetCommentsModel, 'use_outdated_comments',
841 return_value=use_outdated)
843 return_value=use_outdated)
General Comments 0
You need to be logged in to leave comments. Login now