##// END OF EJS Templates
diffs: introducing diff menu for whitespace toggle and context changes
dan -
r3134:0c8f7d31 default
parent child Browse files
Show More
@@ -1,590 +1,502 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31
31
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35
35
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.diffs import cache_diff, load_cached_diff, diff_cache_exist
37 from rhodecode.lib.diffs import (
38 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 get_diff_whitespace_flag)
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
40 from rhodecode.lib.utils2 import safe_unicode, str2bool
42 from rhodecode.lib.utils2 import safe_unicode, str2bool
41 from rhodecode.lib.vcs.backends.base import EmptyCommit
43 from rhodecode.lib.vcs.backends.base import EmptyCommit
42 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
43 RepositoryError, CommitDoesNotExistError)
45 RepositoryError, CommitDoesNotExistError)
44 from rhodecode.model.db import ChangesetComment, ChangesetStatus
46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
45 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
48 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
49
51
50 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
51
53
52
54
53 def _update_with_GET(params, request):
55 def _update_with_GET(params, request):
54 for k in ['diff1', 'diff2', 'diff']:
56 for k in ['diff1', 'diff2', 'diff']:
55 params[k] += request.GET.getall(k)
57 params[k] += request.GET.getall(k)
56
58
57
59
58 def get_ignore_ws(fid, request):
59 ig_ws_global = request.GET.get('ignorews')
60 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
61 if ig_ws:
62 try:
63 return int(ig_ws[0].split(':')[-1])
64 except Exception:
65 pass
66 return ig_ws_global
67
60
68
61
69 def _ignorews_url(request, fileid=None):
70 _ = request.translate
71 fileid = str(fileid) if fileid else None
72 params = collections.defaultdict(list)
73 _update_with_GET(params, request)
74 label = _('Show whitespace')
75 tooltiplbl = _('Show whitespace for all diffs')
76 ig_ws = get_ignore_ws(fileid, request)
77 ln_ctx = get_line_ctx(fileid, request)
78
79 if ig_ws is None:
80 params['ignorews'] += [1]
81 label = _('Ignore whitespace')
82 tooltiplbl = _('Ignore whitespace for all diffs')
83 ctx_key = 'context'
84 ctx_val = ln_ctx
85
86 # if we have passed in ln_ctx pass it along to our params
87 if ln_ctx:
88 params[ctx_key] += [ctx_val]
89
90 if fileid:
91 params['anchor'] = 'a_' + fileid
92 return h.link_to(label, request.current_route_path(_query=params),
93 title=tooltiplbl, class_='tooltip')
94
95
96 def get_line_ctx(fid, request):
97 ln_ctx_global = request.GET.get('context')
98 if fid:
99 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
100 else:
101 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
102 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
103 if ln_ctx:
104 ln_ctx = [ln_ctx]
105
106 if ln_ctx:
107 retval = ln_ctx[0].split(':')[-1]
108 else:
109 retval = ln_ctx_global
110
111 try:
112 return min(diffs.MAX_CONTEXT, int(retval))
113 except Exception:
114 return 3
115
116
117 def _context_url(request, fileid=None):
118 """
119 Generates a url for context lines.
120
121 :param fileid:
122 """
123
124 _ = request.translate
125 fileid = str(fileid) if fileid else None
126 ig_ws = get_ignore_ws(fileid, request)
127 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
128
129 params = collections.defaultdict(list)
130 _update_with_GET(params, request)
131
132 if ln_ctx > 0:
133 params['context'] += [ln_ctx]
134
135 if ig_ws:
136 ig_ws_key = 'ignorews'
137 ig_ws_val = 1
138 params[ig_ws_key] += [ig_ws_val]
139
140 lbl = _('Increase context')
141 tooltiplbl = _('Increase context for all diffs')
142
143 if fileid:
144 params['anchor'] = 'a_' + fileid
145 return h.link_to(lbl, request.current_route_path(_query=params),
146 title=tooltiplbl, class_='tooltip')
147
148
62
149 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
150 def load_default_context(self):
64 def load_default_context(self):
151 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
152 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
153
67
154 return c
68 return c
155
69
156 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
157 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
158 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
159 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
160 return caching_enabled
74 return caching_enabled
161
75
162 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
163 _ = self.request.translate
77 _ = self.request.translate
164 c = self.load_default_context()
78 c = self.load_default_context()
165 c.ignorews_url = _ignorews_url
166 c.context_url = _context_url
167 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
168
80
169 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
170 context_lcl = get_line_ctx('', self.request)
82 diff_context = get_diff_context(self.request)
171 ign_whitespace_lcl = get_ignore_ws('', self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
172
84
173 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
174 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
175 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
176 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
177
89
178 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
179 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
180
92
181 try:
93 try:
182 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
183 'message', 'parents']
95 'message', 'parents']
184
96
185 if len(commit_range) == 2:
97 if len(commit_range) == 2:
186 commits = self.rhodecode_vcs_repo.get_commits(
98 commits = self.rhodecode_vcs_repo.get_commits(
187 start_id=commit_range[0], end_id=commit_range[1],
99 start_id=commit_range[0], end_id=commit_range[1],
188 pre_load=pre_load)
100 pre_load=pre_load)
189 commits = list(commits)
101 commits = list(commits)
190 else:
102 else:
191 commits = [self.rhodecode_vcs_repo.get_commit(
103 commits = [self.rhodecode_vcs_repo.get_commit(
192 commit_id=commit_id_range, pre_load=pre_load)]
104 commit_id=commit_id_range, pre_load=pre_load)]
193
105
194 c.commit_ranges = commits
106 c.commit_ranges = commits
195 if not c.commit_ranges:
107 if not c.commit_ranges:
196 raise RepositoryError(
108 raise RepositoryError(
197 'The commit range returned an empty result')
109 'The commit range returned an empty result')
198 except CommitDoesNotExistError:
110 except CommitDoesNotExistError:
199 msg = _('No such commit exists for this repository')
111 msg = _('No such commit exists for this repository')
200 h.flash(msg, category='error')
112 h.flash(msg, category='error')
201 raise HTTPNotFound()
113 raise HTTPNotFound()
202 except Exception:
114 except Exception:
203 log.exception("General failure")
115 log.exception("General failure")
204 raise HTTPNotFound()
116 raise HTTPNotFound()
205
117
206 c.changes = OrderedDict()
118 c.changes = OrderedDict()
207 c.lines_added = 0
119 c.lines_added = 0
208 c.lines_deleted = 0
120 c.lines_deleted = 0
209
121
210 # auto collapse if we have more than limit
122 # auto collapse if we have more than limit
211 collapse_limit = diffs.DiffProcessor._collapse_commits_over
123 collapse_limit = diffs.DiffProcessor._collapse_commits_over
212 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
124 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
213
125
214 c.commit_statuses = ChangesetStatus.STATUSES
126 c.commit_statuses = ChangesetStatus.STATUSES
215 c.inline_comments = []
127 c.inline_comments = []
216 c.files = []
128 c.files = []
217
129
218 c.statuses = []
130 c.statuses = []
219 c.comments = []
131 c.comments = []
220 c.unresolved_comments = []
132 c.unresolved_comments = []
221 if len(c.commit_ranges) == 1:
133 if len(c.commit_ranges) == 1:
222 commit = c.commit_ranges[0]
134 commit = c.commit_ranges[0]
223 c.comments = CommentsModel().get_comments(
135 c.comments = CommentsModel().get_comments(
224 self.db_repo.repo_id,
136 self.db_repo.repo_id,
225 revision=commit.raw_id)
137 revision=commit.raw_id)
226 c.statuses.append(ChangesetStatusModel().get_status(
138 c.statuses.append(ChangesetStatusModel().get_status(
227 self.db_repo.repo_id, commit.raw_id))
139 self.db_repo.repo_id, commit.raw_id))
228 # comments from PR
140 # comments from PR
229 statuses = ChangesetStatusModel().get_statuses(
141 statuses = ChangesetStatusModel().get_statuses(
230 self.db_repo.repo_id, commit.raw_id,
142 self.db_repo.repo_id, commit.raw_id,
231 with_revisions=True)
143 with_revisions=True)
232 prs = set(st.pull_request for st in statuses
144 prs = set(st.pull_request for st in statuses
233 if st.pull_request is not None)
145 if st.pull_request is not None)
234 # from associated statuses, check the pull requests, and
146 # from associated statuses, check the pull requests, and
235 # show comments from them
147 # show comments from them
236 for pr in prs:
148 for pr in prs:
237 c.comments.extend(pr.comments)
149 c.comments.extend(pr.comments)
238
150
239 c.unresolved_comments = CommentsModel()\
151 c.unresolved_comments = CommentsModel()\
240 .get_commit_unresolved_todos(commit.raw_id)
152 .get_commit_unresolved_todos(commit.raw_id)
241
153
242 diff = None
154 diff = None
243 # Iterate over ranges (default commit view is always one commit)
155 # Iterate over ranges (default commit view is always one commit)
244 for commit in c.commit_ranges:
156 for commit in c.commit_ranges:
245 c.changes[commit.raw_id] = []
157 c.changes[commit.raw_id] = []
246
158
247 commit2 = commit
159 commit2 = commit
248 commit1 = commit.first_parent
160 commit1 = commit.first_parent
249
161
250 if method == 'show':
162 if method == 'show':
251 inline_comments = CommentsModel().get_inline_comments(
163 inline_comments = CommentsModel().get_inline_comments(
252 self.db_repo.repo_id, revision=commit.raw_id)
164 self.db_repo.repo_id, revision=commit.raw_id)
253 c.inline_cnt = CommentsModel().get_inline_comments_count(
165 c.inline_cnt = CommentsModel().get_inline_comments_count(
254 inline_comments)
166 inline_comments)
255 c.inline_comments = inline_comments
167 c.inline_comments = inline_comments
256
168
257 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
169 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
258 self.db_repo)
170 self.db_repo)
259 cache_file_path = diff_cache_exist(
171 cache_file_path = diff_cache_exist(
260 cache_path, 'diff', commit.raw_id,
172 cache_path, 'diff', commit.raw_id,
261 ign_whitespace_lcl, context_lcl, c.fulldiff)
173 hide_whitespace_changes, diff_context, c.fulldiff)
262
174
263 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
175 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
264 force_recache = str2bool(self.request.GET.get('force_recache'))
176 force_recache = str2bool(self.request.GET.get('force_recache'))
265
177
266 cached_diff = None
178 cached_diff = None
267 if caching_enabled:
179 if caching_enabled:
268 cached_diff = load_cached_diff(cache_file_path)
180 cached_diff = load_cached_diff(cache_file_path)
269
181
270 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
182 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
271 if not force_recache and has_proper_diff_cache:
183 if not force_recache and has_proper_diff_cache:
272 diffset = cached_diff['diff']
184 diffset = cached_diff['diff']
273 else:
185 else:
274 vcs_diff = self.rhodecode_vcs_repo.get_diff(
186 vcs_diff = self.rhodecode_vcs_repo.get_diff(
275 commit1, commit2,
187 commit1, commit2,
276 ignore_whitespace=ign_whitespace_lcl,
188 ignore_whitespace=hide_whitespace_changes,
277 context=context_lcl)
189 context=diff_context)
278
190
279 diff_processor = diffs.DiffProcessor(
191 diff_processor = diffs.DiffProcessor(
280 vcs_diff, format='newdiff', diff_limit=diff_limit,
192 vcs_diff, format='newdiff', diff_limit=diff_limit,
281 file_limit=file_limit, show_full_diff=c.fulldiff)
193 file_limit=file_limit, show_full_diff=c.fulldiff)
282
194
283 _parsed = diff_processor.prepare()
195 _parsed = diff_processor.prepare()
284
196
285 diffset = codeblocks.DiffSet(
197 diffset = codeblocks.DiffSet(
286 repo_name=self.db_repo_name,
198 repo_name=self.db_repo_name,
287 source_node_getter=codeblocks.diffset_node_getter(commit1),
199 source_node_getter=codeblocks.diffset_node_getter(commit1),
288 target_node_getter=codeblocks.diffset_node_getter(commit2))
200 target_node_getter=codeblocks.diffset_node_getter(commit2))
289
201
290 diffset = self.path_filter.render_patchset_filtered(
202 diffset = self.path_filter.render_patchset_filtered(
291 diffset, _parsed, commit1.raw_id, commit2.raw_id)
203 diffset, _parsed, commit1.raw_id, commit2.raw_id)
292
204
293 # save cached diff
205 # save cached diff
294 if caching_enabled:
206 if caching_enabled:
295 cache_diff(cache_file_path, diffset, None)
207 cache_diff(cache_file_path, diffset, None)
296
208
297 c.limited_diff = diffset.limited_diff
209 c.limited_diff = diffset.limited_diff
298 c.changes[commit.raw_id] = diffset
210 c.changes[commit.raw_id] = diffset
299 else:
211 else:
300 # TODO(marcink): no cache usage here...
212 # TODO(marcink): no cache usage here...
301 _diff = self.rhodecode_vcs_repo.get_diff(
213 _diff = self.rhodecode_vcs_repo.get_diff(
302 commit1, commit2,
214 commit1, commit2,
303 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
215 ignore_whitespace=hide_whitespace_changes, context=diff_context)
304 diff_processor = diffs.DiffProcessor(
216 diff_processor = diffs.DiffProcessor(
305 _diff, format='newdiff', diff_limit=diff_limit,
217 _diff, format='newdiff', diff_limit=diff_limit,
306 file_limit=file_limit, show_full_diff=c.fulldiff)
218 file_limit=file_limit, show_full_diff=c.fulldiff)
307 # downloads/raw we only need RAW diff nothing else
219 # downloads/raw we only need RAW diff nothing else
308 diff = self.path_filter.get_raw_patch(diff_processor)
220 diff = self.path_filter.get_raw_patch(diff_processor)
309 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
221 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
310
222
311 # sort comments by how they were generated
223 # sort comments by how they were generated
312 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
224 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
313
225
314 if len(c.commit_ranges) == 1:
226 if len(c.commit_ranges) == 1:
315 c.commit = c.commit_ranges[0]
227 c.commit = c.commit_ranges[0]
316 c.parent_tmpl = ''.join(
228 c.parent_tmpl = ''.join(
317 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
229 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
318
230
319 if method == 'download':
231 if method == 'download':
320 response = Response(diff)
232 response = Response(diff)
321 response.content_type = 'text/plain'
233 response.content_type = 'text/plain'
322 response.content_disposition = (
234 response.content_disposition = (
323 'attachment; filename=%s.diff' % commit_id_range[:12])
235 'attachment; filename=%s.diff' % commit_id_range[:12])
324 return response
236 return response
325 elif method == 'patch':
237 elif method == 'patch':
326 c.diff = safe_unicode(diff)
238 c.diff = safe_unicode(diff)
327 patch = render(
239 patch = render(
328 'rhodecode:templates/changeset/patch_changeset.mako',
240 'rhodecode:templates/changeset/patch_changeset.mako',
329 self._get_template_context(c), self.request)
241 self._get_template_context(c), self.request)
330 response = Response(patch)
242 response = Response(patch)
331 response.content_type = 'text/plain'
243 response.content_type = 'text/plain'
332 return response
244 return response
333 elif method == 'raw':
245 elif method == 'raw':
334 response = Response(diff)
246 response = Response(diff)
335 response.content_type = 'text/plain'
247 response.content_type = 'text/plain'
336 return response
248 return response
337 elif method == 'show':
249 elif method == 'show':
338 if len(c.commit_ranges) == 1:
250 if len(c.commit_ranges) == 1:
339 html = render(
251 html = render(
340 'rhodecode:templates/changeset/changeset.mako',
252 'rhodecode:templates/changeset/changeset.mako',
341 self._get_template_context(c), self.request)
253 self._get_template_context(c), self.request)
342 return Response(html)
254 return Response(html)
343 else:
255 else:
344 c.ancestor = None
256 c.ancestor = None
345 c.target_repo = self.db_repo
257 c.target_repo = self.db_repo
346 html = render(
258 html = render(
347 'rhodecode:templates/changeset/changeset_range.mako',
259 'rhodecode:templates/changeset/changeset_range.mako',
348 self._get_template_context(c), self.request)
260 self._get_template_context(c), self.request)
349 return Response(html)
261 return Response(html)
350
262
351 raise HTTPBadRequest()
263 raise HTTPBadRequest()
352
264
353 @LoginRequired()
265 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
266 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
267 'repository.read', 'repository.write', 'repository.admin')
356 @view_config(
268 @view_config(
357 route_name='repo_commit', request_method='GET',
269 route_name='repo_commit', request_method='GET',
358 renderer=None)
270 renderer=None)
359 def repo_commit_show(self):
271 def repo_commit_show(self):
360 commit_id = self.request.matchdict['commit_id']
272 commit_id = self.request.matchdict['commit_id']
361 return self._commit(commit_id, method='show')
273 return self._commit(commit_id, method='show')
362
274
363 @LoginRequired()
275 @LoginRequired()
364 @HasRepoPermissionAnyDecorator(
276 @HasRepoPermissionAnyDecorator(
365 'repository.read', 'repository.write', 'repository.admin')
277 'repository.read', 'repository.write', 'repository.admin')
366 @view_config(
278 @view_config(
367 route_name='repo_commit_raw', request_method='GET',
279 route_name='repo_commit_raw', request_method='GET',
368 renderer=None)
280 renderer=None)
369 @view_config(
281 @view_config(
370 route_name='repo_commit_raw_deprecated', request_method='GET',
282 route_name='repo_commit_raw_deprecated', request_method='GET',
371 renderer=None)
283 renderer=None)
372 def repo_commit_raw(self):
284 def repo_commit_raw(self):
373 commit_id = self.request.matchdict['commit_id']
285 commit_id = self.request.matchdict['commit_id']
374 return self._commit(commit_id, method='raw')
286 return self._commit(commit_id, method='raw')
375
287
376 @LoginRequired()
288 @LoginRequired()
377 @HasRepoPermissionAnyDecorator(
289 @HasRepoPermissionAnyDecorator(
378 'repository.read', 'repository.write', 'repository.admin')
290 'repository.read', 'repository.write', 'repository.admin')
379 @view_config(
291 @view_config(
380 route_name='repo_commit_patch', request_method='GET',
292 route_name='repo_commit_patch', request_method='GET',
381 renderer=None)
293 renderer=None)
382 def repo_commit_patch(self):
294 def repo_commit_patch(self):
383 commit_id = self.request.matchdict['commit_id']
295 commit_id = self.request.matchdict['commit_id']
384 return self._commit(commit_id, method='patch')
296 return self._commit(commit_id, method='patch')
385
297
386 @LoginRequired()
298 @LoginRequired()
387 @HasRepoPermissionAnyDecorator(
299 @HasRepoPermissionAnyDecorator(
388 'repository.read', 'repository.write', 'repository.admin')
300 'repository.read', 'repository.write', 'repository.admin')
389 @view_config(
301 @view_config(
390 route_name='repo_commit_download', request_method='GET',
302 route_name='repo_commit_download', request_method='GET',
391 renderer=None)
303 renderer=None)
392 def repo_commit_download(self):
304 def repo_commit_download(self):
393 commit_id = self.request.matchdict['commit_id']
305 commit_id = self.request.matchdict['commit_id']
394 return self._commit(commit_id, method='download')
306 return self._commit(commit_id, method='download')
395
307
396 @LoginRequired()
308 @LoginRequired()
397 @NotAnonymous()
309 @NotAnonymous()
398 @HasRepoPermissionAnyDecorator(
310 @HasRepoPermissionAnyDecorator(
399 'repository.read', 'repository.write', 'repository.admin')
311 'repository.read', 'repository.write', 'repository.admin')
400 @CSRFRequired()
312 @CSRFRequired()
401 @view_config(
313 @view_config(
402 route_name='repo_commit_comment_create', request_method='POST',
314 route_name='repo_commit_comment_create', request_method='POST',
403 renderer='json_ext')
315 renderer='json_ext')
404 def repo_commit_comment_create(self):
316 def repo_commit_comment_create(self):
405 _ = self.request.translate
317 _ = self.request.translate
406 commit_id = self.request.matchdict['commit_id']
318 commit_id = self.request.matchdict['commit_id']
407
319
408 c = self.load_default_context()
320 c = self.load_default_context()
409 status = self.request.POST.get('changeset_status', None)
321 status = self.request.POST.get('changeset_status', None)
410 text = self.request.POST.get('text')
322 text = self.request.POST.get('text')
411 comment_type = self.request.POST.get('comment_type')
323 comment_type = self.request.POST.get('comment_type')
412 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
324 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
413
325
414 if status:
326 if status:
415 text = text or (_('Status change %(transition_icon)s %(status)s')
327 text = text or (_('Status change %(transition_icon)s %(status)s')
416 % {'transition_icon': '>',
328 % {'transition_icon': '>',
417 'status': ChangesetStatus.get_status_lbl(status)})
329 'status': ChangesetStatus.get_status_lbl(status)})
418
330
419 multi_commit_ids = []
331 multi_commit_ids = []
420 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
332 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
421 if _commit_id not in ['', None, EmptyCommit.raw_id]:
333 if _commit_id not in ['', None, EmptyCommit.raw_id]:
422 if _commit_id not in multi_commit_ids:
334 if _commit_id not in multi_commit_ids:
423 multi_commit_ids.append(_commit_id)
335 multi_commit_ids.append(_commit_id)
424
336
425 commit_ids = multi_commit_ids or [commit_id]
337 commit_ids = multi_commit_ids or [commit_id]
426
338
427 comment = None
339 comment = None
428 for current_id in filter(None, commit_ids):
340 for current_id in filter(None, commit_ids):
429 comment = CommentsModel().create(
341 comment = CommentsModel().create(
430 text=text,
342 text=text,
431 repo=self.db_repo.repo_id,
343 repo=self.db_repo.repo_id,
432 user=self._rhodecode_db_user.user_id,
344 user=self._rhodecode_db_user.user_id,
433 commit_id=current_id,
345 commit_id=current_id,
434 f_path=self.request.POST.get('f_path'),
346 f_path=self.request.POST.get('f_path'),
435 line_no=self.request.POST.get('line'),
347 line_no=self.request.POST.get('line'),
436 status_change=(ChangesetStatus.get_status_lbl(status)
348 status_change=(ChangesetStatus.get_status_lbl(status)
437 if status else None),
349 if status else None),
438 status_change_type=status,
350 status_change_type=status,
439 comment_type=comment_type,
351 comment_type=comment_type,
440 resolves_comment_id=resolves_comment_id,
352 resolves_comment_id=resolves_comment_id,
441 auth_user=self._rhodecode_user
353 auth_user=self._rhodecode_user
442 )
354 )
443
355
444 # get status if set !
356 # get status if set !
445 if status:
357 if status:
446 # if latest status was from pull request and it's closed
358 # if latest status was from pull request and it's closed
447 # disallow changing status !
359 # disallow changing status !
448 # dont_allow_on_closed_pull_request = True !
360 # dont_allow_on_closed_pull_request = True !
449
361
450 try:
362 try:
451 ChangesetStatusModel().set_status(
363 ChangesetStatusModel().set_status(
452 self.db_repo.repo_id,
364 self.db_repo.repo_id,
453 status,
365 status,
454 self._rhodecode_db_user.user_id,
366 self._rhodecode_db_user.user_id,
455 comment,
367 comment,
456 revision=current_id,
368 revision=current_id,
457 dont_allow_on_closed_pull_request=True
369 dont_allow_on_closed_pull_request=True
458 )
370 )
459 except StatusChangeOnClosedPullRequestError:
371 except StatusChangeOnClosedPullRequestError:
460 msg = _('Changing the status of a commit associated with '
372 msg = _('Changing the status of a commit associated with '
461 'a closed pull request is not allowed')
373 'a closed pull request is not allowed')
462 log.exception(msg)
374 log.exception(msg)
463 h.flash(msg, category='warning')
375 h.flash(msg, category='warning')
464 raise HTTPFound(h.route_path(
376 raise HTTPFound(h.route_path(
465 'repo_commit', repo_name=self.db_repo_name,
377 'repo_commit', repo_name=self.db_repo_name,
466 commit_id=current_id))
378 commit_id=current_id))
467
379
468 # finalize, commit and redirect
380 # finalize, commit and redirect
469 Session().commit()
381 Session().commit()
470
382
471 data = {
383 data = {
472 'target_id': h.safeid(h.safe_unicode(
384 'target_id': h.safeid(h.safe_unicode(
473 self.request.POST.get('f_path'))),
385 self.request.POST.get('f_path'))),
474 }
386 }
475 if comment:
387 if comment:
476 c.co = comment
388 c.co = comment
477 rendered_comment = render(
389 rendered_comment = render(
478 'rhodecode:templates/changeset/changeset_comment_block.mako',
390 'rhodecode:templates/changeset/changeset_comment_block.mako',
479 self._get_template_context(c), self.request)
391 self._get_template_context(c), self.request)
480
392
481 data.update(comment.get_dict())
393 data.update(comment.get_dict())
482 data.update({'rendered_text': rendered_comment})
394 data.update({'rendered_text': rendered_comment})
483
395
484 return data
396 return data
485
397
486 @LoginRequired()
398 @LoginRequired()
487 @NotAnonymous()
399 @NotAnonymous()
488 @HasRepoPermissionAnyDecorator(
400 @HasRepoPermissionAnyDecorator(
489 'repository.read', 'repository.write', 'repository.admin')
401 'repository.read', 'repository.write', 'repository.admin')
490 @CSRFRequired()
402 @CSRFRequired()
491 @view_config(
403 @view_config(
492 route_name='repo_commit_comment_preview', request_method='POST',
404 route_name='repo_commit_comment_preview', request_method='POST',
493 renderer='string', xhr=True)
405 renderer='string', xhr=True)
494 def repo_commit_comment_preview(self):
406 def repo_commit_comment_preview(self):
495 # Technically a CSRF token is not needed as no state changes with this
407 # Technically a CSRF token is not needed as no state changes with this
496 # call. However, as this is a POST is better to have it, so automated
408 # call. However, as this is a POST is better to have it, so automated
497 # tools don't flag it as potential CSRF.
409 # tools don't flag it as potential CSRF.
498 # Post is required because the payload could be bigger than the maximum
410 # Post is required because the payload could be bigger than the maximum
499 # allowed by GET.
411 # allowed by GET.
500
412
501 text = self.request.POST.get('text')
413 text = self.request.POST.get('text')
502 renderer = self.request.POST.get('renderer') or 'rst'
414 renderer = self.request.POST.get('renderer') or 'rst'
503 if text:
415 if text:
504 return h.render(text, renderer=renderer, mentions=True)
416 return h.render(text, renderer=renderer, mentions=True)
505 return ''
417 return ''
506
418
507 @LoginRequired()
419 @LoginRequired()
508 @NotAnonymous()
420 @NotAnonymous()
509 @HasRepoPermissionAnyDecorator(
421 @HasRepoPermissionAnyDecorator(
510 'repository.read', 'repository.write', 'repository.admin')
422 'repository.read', 'repository.write', 'repository.admin')
511 @CSRFRequired()
423 @CSRFRequired()
512 @view_config(
424 @view_config(
513 route_name='repo_commit_comment_delete', request_method='POST',
425 route_name='repo_commit_comment_delete', request_method='POST',
514 renderer='json_ext')
426 renderer='json_ext')
515 def repo_commit_comment_delete(self):
427 def repo_commit_comment_delete(self):
516 commit_id = self.request.matchdict['commit_id']
428 commit_id = self.request.matchdict['commit_id']
517 comment_id = self.request.matchdict['comment_id']
429 comment_id = self.request.matchdict['comment_id']
518
430
519 comment = ChangesetComment.get_or_404(comment_id)
431 comment = ChangesetComment.get_or_404(comment_id)
520 if not comment:
432 if not comment:
521 log.debug('Comment with id:%s not found, skipping', comment_id)
433 log.debug('Comment with id:%s not found, skipping', comment_id)
522 # comment already deleted in another call probably
434 # comment already deleted in another call probably
523 return True
435 return True
524
436
525 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
437 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
526 super_admin = h.HasPermissionAny('hg.admin')()
438 super_admin = h.HasPermissionAny('hg.admin')()
527 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
439 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
528 is_repo_comment = comment.repo.repo_name == self.db_repo_name
440 is_repo_comment = comment.repo.repo_name == self.db_repo_name
529 comment_repo_admin = is_repo_admin and is_repo_comment
441 comment_repo_admin = is_repo_admin and is_repo_comment
530
442
531 if super_admin or comment_owner or comment_repo_admin:
443 if super_admin or comment_owner or comment_repo_admin:
532 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
444 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
533 Session().commit()
445 Session().commit()
534 return True
446 return True
535 else:
447 else:
536 log.warning('No permissions for user %s to delete comment_id: %s',
448 log.warning('No permissions for user %s to delete comment_id: %s',
537 self._rhodecode_db_user, comment_id)
449 self._rhodecode_db_user, comment_id)
538 raise HTTPNotFound()
450 raise HTTPNotFound()
539
451
540 @LoginRequired()
452 @LoginRequired()
541 @HasRepoPermissionAnyDecorator(
453 @HasRepoPermissionAnyDecorator(
542 'repository.read', 'repository.write', 'repository.admin')
454 'repository.read', 'repository.write', 'repository.admin')
543 @view_config(
455 @view_config(
544 route_name='repo_commit_data', request_method='GET',
456 route_name='repo_commit_data', request_method='GET',
545 renderer='json_ext', xhr=True)
457 renderer='json_ext', xhr=True)
546 def repo_commit_data(self):
458 def repo_commit_data(self):
547 commit_id = self.request.matchdict['commit_id']
459 commit_id = self.request.matchdict['commit_id']
548 self.load_default_context()
460 self.load_default_context()
549
461
550 try:
462 try:
551 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
463 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
552 except CommitDoesNotExistError as e:
464 except CommitDoesNotExistError as e:
553 return EmptyCommit(message=str(e))
465 return EmptyCommit(message=str(e))
554
466
555 @LoginRequired()
467 @LoginRequired()
556 @HasRepoPermissionAnyDecorator(
468 @HasRepoPermissionAnyDecorator(
557 'repository.read', 'repository.write', 'repository.admin')
469 'repository.read', 'repository.write', 'repository.admin')
558 @view_config(
470 @view_config(
559 route_name='repo_commit_children', request_method='GET',
471 route_name='repo_commit_children', request_method='GET',
560 renderer='json_ext', xhr=True)
472 renderer='json_ext', xhr=True)
561 def repo_commit_children(self):
473 def repo_commit_children(self):
562 commit_id = self.request.matchdict['commit_id']
474 commit_id = self.request.matchdict['commit_id']
563 self.load_default_context()
475 self.load_default_context()
564
476
565 try:
477 try:
566 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
478 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
567 children = commit.children
479 children = commit.children
568 except CommitDoesNotExistError:
480 except CommitDoesNotExistError:
569 children = []
481 children = []
570
482
571 result = {"results": children}
483 result = {"results": children}
572 return result
484 return result
573
485
574 @LoginRequired()
486 @LoginRequired()
575 @HasRepoPermissionAnyDecorator(
487 @HasRepoPermissionAnyDecorator(
576 'repository.read', 'repository.write', 'repository.admin')
488 'repository.read', 'repository.write', 'repository.admin')
577 @view_config(
489 @view_config(
578 route_name='repo_commit_parents', request_method='GET',
490 route_name='repo_commit_parents', request_method='GET',
579 renderer='json_ext')
491 renderer='json_ext')
580 def repo_commit_parents(self):
492 def repo_commit_parents(self):
581 commit_id = self.request.matchdict['commit_id']
493 commit_id = self.request.matchdict['commit_id']
582 self.load_default_context()
494 self.load_default_context()
583
495
584 try:
496 try:
585 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
497 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
586 parents = commit.parents
498 parents = commit.parents
587 except CommitDoesNotExistError:
499 except CommitDoesNotExistError:
588 parents = []
500 parents = []
589 result = {"results": parents}
501 result = {"results": parents}
590 return result
502 return result
@@ -1,313 +1,315 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.utils import safe_str
34 from rhodecode.lib.utils import safe_str
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 from rhodecode.lib.vcs.exceptions import (
36 from rhodecode.lib.vcs.exceptions import (
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 NodeDoesNotExistError)
38 NodeDoesNotExistError)
39 from rhodecode.model.db import Repository, ChangesetStatus
39 from rhodecode.model.db import Repository, ChangesetStatus
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class RepoCompareView(RepoAppView):
44 class RepoCompareView(RepoAppView):
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context(include_app_defaults=True)
46 c = self._get_local_tmpl_context(include_app_defaults=True)
47
48 c.rhodecode_repo = self.rhodecode_vcs_repo
47 c.rhodecode_repo = self.rhodecode_vcs_repo
49
50
51 return c
48 return c
52
49
53 def _get_commit_or_redirect(
50 def _get_commit_or_redirect(
54 self, ref, ref_type, repo, redirect_after=True, partial=False):
51 self, ref, ref_type, repo, redirect_after=True, partial=False):
55 """
52 """
56 This is a safe way to get a commit. If an error occurs it
53 This is a safe way to get a commit. If an error occurs it
57 redirects to a commit with a proper message. If partial is set
54 redirects to a commit with a proper message. If partial is set
58 then it does not do redirect raise and throws an exception instead.
55 then it does not do redirect raise and throws an exception instead.
59 """
56 """
60 _ = self.request.translate
57 _ = self.request.translate
61 try:
58 try:
62 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
59 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
63 except EmptyRepositoryError:
60 except EmptyRepositoryError:
64 if not redirect_after:
61 if not redirect_after:
65 return repo.scm_instance().EMPTY_COMMIT
62 return repo.scm_instance().EMPTY_COMMIT
66 h.flash(h.literal(_('There are no commits yet')),
63 h.flash(h.literal(_('There are no commits yet')),
67 category='warning')
64 category='warning')
68 if not partial:
65 if not partial:
69 raise HTTPFound(
66 raise HTTPFound(
70 h.route_path('repo_summary', repo_name=repo.repo_name))
67 h.route_path('repo_summary', repo_name=repo.repo_name))
71 raise HTTPBadRequest()
68 raise HTTPBadRequest()
72
69
73 except RepositoryError as e:
70 except RepositoryError as e:
74 log.exception(safe_str(e))
71 log.exception(safe_str(e))
75 h.flash(safe_str(h.escape(e)), category='warning')
72 h.flash(safe_str(h.escape(e)), category='warning')
76 if not partial:
73 if not partial:
77 raise HTTPFound(
74 raise HTTPFound(
78 h.route_path('repo_summary', repo_name=repo.repo_name))
75 h.route_path('repo_summary', repo_name=repo.repo_name))
79 raise HTTPBadRequest()
76 raise HTTPBadRequest()
80
77
81 @LoginRequired()
78 @LoginRequired()
82 @HasRepoPermissionAnyDecorator(
79 @HasRepoPermissionAnyDecorator(
83 'repository.read', 'repository.write', 'repository.admin')
80 'repository.read', 'repository.write', 'repository.admin')
84 @view_config(
81 @view_config(
85 route_name='repo_compare_select', request_method='GET',
82 route_name='repo_compare_select', request_method='GET',
86 renderer='rhodecode:templates/compare/compare_diff.mako')
83 renderer='rhodecode:templates/compare/compare_diff.mako')
87 def compare_select(self):
84 def compare_select(self):
88 _ = self.request.translate
85 _ = self.request.translate
89 c = self.load_default_context()
86 c = self.load_default_context()
90
87
91 source_repo = self.db_repo_name
88 source_repo = self.db_repo_name
92 target_repo = self.request.GET.get('target_repo', source_repo)
89 target_repo = self.request.GET.get('target_repo', source_repo)
93 c.source_repo = Repository.get_by_repo_name(source_repo)
90 c.source_repo = Repository.get_by_repo_name(source_repo)
94 c.target_repo = Repository.get_by_repo_name(target_repo)
91 c.target_repo = Repository.get_by_repo_name(target_repo)
95
92
96 if c.source_repo is None or c.target_repo is None:
93 if c.source_repo is None or c.target_repo is None:
97 raise HTTPNotFound()
94 raise HTTPNotFound()
98
95
99 c.compare_home = True
96 c.compare_home = True
100 c.commit_ranges = []
97 c.commit_ranges = []
101 c.collapse_all_commits = False
98 c.collapse_all_commits = False
102 c.diffset = None
99 c.diffset = None
103 c.limited_diff = False
100 c.limited_diff = False
104 c.source_ref = c.target_ref = _('Select commit')
101 c.source_ref = c.target_ref = _('Select commit')
105 c.source_ref_type = ""
102 c.source_ref_type = ""
106 c.target_ref_type = ""
103 c.target_ref_type = ""
107 c.commit_statuses = ChangesetStatus.STATUSES
104 c.commit_statuses = ChangesetStatus.STATUSES
108 c.preview_mode = False
105 c.preview_mode = False
109 c.file_path = None
106 c.file_path = None
110
107
111 return self._get_template_context(c)
108 return self._get_template_context(c)
112
109
113 @LoginRequired()
110 @LoginRequired()
114 @HasRepoPermissionAnyDecorator(
111 @HasRepoPermissionAnyDecorator(
115 'repository.read', 'repository.write', 'repository.admin')
112 'repository.read', 'repository.write', 'repository.admin')
116 @view_config(
113 @view_config(
117 route_name='repo_compare', request_method='GET',
114 route_name='repo_compare', request_method='GET',
118 renderer=None)
115 renderer=None)
119 def compare(self):
116 def compare(self):
120 _ = self.request.translate
117 _ = self.request.translate
121 c = self.load_default_context()
118 c = self.load_default_context()
122
119
123 source_ref_type = self.request.matchdict['source_ref_type']
120 source_ref_type = self.request.matchdict['source_ref_type']
124 source_ref = self.request.matchdict['source_ref']
121 source_ref = self.request.matchdict['source_ref']
125 target_ref_type = self.request.matchdict['target_ref_type']
122 target_ref_type = self.request.matchdict['target_ref_type']
126 target_ref = self.request.matchdict['target_ref']
123 target_ref = self.request.matchdict['target_ref']
127
124
128 # source_ref will be evaluated in source_repo
125 # source_ref will be evaluated in source_repo
129 source_repo_name = self.db_repo_name
126 source_repo_name = self.db_repo_name
130 source_path, source_id = parse_path_ref(source_ref)
127 source_path, source_id = parse_path_ref(source_ref)
131
128
132 # target_ref will be evaluated in target_repo
129 # target_ref will be evaluated in target_repo
133 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
130 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
134 target_path, target_id = parse_path_ref(
131 target_path, target_id = parse_path_ref(
135 target_ref, default_path=self.request.GET.get('f_path', ''))
132 target_ref, default_path=self.request.GET.get('f_path', ''))
136
133
137 # if merge is True
134 # if merge is True
138 # Show what changes since the shared ancestor commit of target/source
135 # Show what changes since the shared ancestor commit of target/source
139 # the source would get if it was merged with target. Only commits
136 # the source would get if it was merged with target. Only commits
140 # which are in target but not in source will be shown.
137 # which are in target but not in source will be shown.
141 merge = str2bool(self.request.GET.get('merge'))
138 merge = str2bool(self.request.GET.get('merge'))
142 # if merge is False
139 # if merge is False
143 # Show a raw diff of source/target refs even if no ancestor exists
140 # Show a raw diff of source/target refs even if no ancestor exists
144
141
145 # c.fulldiff disables cut_off_limit
142 # c.fulldiff disables cut_off_limit
146 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
143 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
147
144
145 # fetch global flags of ignore ws or context lines
146 diff_context = diffs.get_diff_context(self.request)
147 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
148
148 c.file_path = target_path
149 c.file_path = target_path
149 c.commit_statuses = ChangesetStatus.STATUSES
150 c.commit_statuses = ChangesetStatus.STATUSES
150
151
151 # if partial, returns just compare_commits.html (commits log)
152 # if partial, returns just compare_commits.html (commits log)
152 partial = self.request.is_xhr
153 partial = self.request.is_xhr
153
154
154 # swap url for compare_diff page
155 # swap url for compare_diff page
155 c.swap_url = h.route_path(
156 c.swap_url = h.route_path(
156 'repo_compare',
157 'repo_compare',
157 repo_name=target_repo_name,
158 repo_name=target_repo_name,
158 source_ref_type=target_ref_type,
159 source_ref_type=target_ref_type,
159 source_ref=target_ref,
160 source_ref=target_ref,
160 target_repo=source_repo_name,
161 target_repo=source_repo_name,
161 target_ref_type=source_ref_type,
162 target_ref_type=source_ref_type,
162 target_ref=source_ref,
163 target_ref=source_ref,
163 _query=dict(merge=merge and '1' or '', f_path=target_path))
164 _query=dict(merge=merge and '1' or '', f_path=target_path))
164
165
165 source_repo = Repository.get_by_repo_name(source_repo_name)
166 source_repo = Repository.get_by_repo_name(source_repo_name)
166 target_repo = Repository.get_by_repo_name(target_repo_name)
167 target_repo = Repository.get_by_repo_name(target_repo_name)
167
168
168 if source_repo is None:
169 if source_repo is None:
169 log.error('Could not find the source repo: {}'
170 log.error('Could not find the source repo: {}'
170 .format(source_repo_name))
171 .format(source_repo_name))
171 h.flash(_('Could not find the source repo: `{}`')
172 h.flash(_('Could not find the source repo: `{}`')
172 .format(h.escape(source_repo_name)), category='error')
173 .format(h.escape(source_repo_name)), category='error')
173 raise HTTPFound(
174 raise HTTPFound(
174 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
175 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
175
176
176 if target_repo is None:
177 if target_repo is None:
177 log.error('Could not find the target repo: {}'
178 log.error('Could not find the target repo: {}'
178 .format(source_repo_name))
179 .format(source_repo_name))
179 h.flash(_('Could not find the target repo: `{}`')
180 h.flash(_('Could not find the target repo: `{}`')
180 .format(h.escape(target_repo_name)), category='error')
181 .format(h.escape(target_repo_name)), category='error')
181 raise HTTPFound(
182 raise HTTPFound(
182 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
183 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
183
184
184 source_scm = source_repo.scm_instance()
185 source_scm = source_repo.scm_instance()
185 target_scm = target_repo.scm_instance()
186 target_scm = target_repo.scm_instance()
186
187
187 source_alias = source_scm.alias
188 source_alias = source_scm.alias
188 target_alias = target_scm.alias
189 target_alias = target_scm.alias
189 if source_alias != target_alias:
190 if source_alias != target_alias:
190 msg = _('The comparison of two different kinds of remote repos '
191 msg = _('The comparison of two different kinds of remote repos '
191 'is not available')
192 'is not available')
192 log.error(msg)
193 log.error(msg)
193 h.flash(msg, category='error')
194 h.flash(msg, category='error')
194 raise HTTPFound(
195 raise HTTPFound(
195 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
196 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
196
197
197 source_commit = self._get_commit_or_redirect(
198 source_commit = self._get_commit_or_redirect(
198 ref=source_id, ref_type=source_ref_type, repo=source_repo,
199 ref=source_id, ref_type=source_ref_type, repo=source_repo,
199 partial=partial)
200 partial=partial)
200 target_commit = self._get_commit_or_redirect(
201 target_commit = self._get_commit_or_redirect(
201 ref=target_id, ref_type=target_ref_type, repo=target_repo,
202 ref=target_id, ref_type=target_ref_type, repo=target_repo,
202 partial=partial)
203 partial=partial)
203
204
204 c.compare_home = False
205 c.compare_home = False
205 c.source_repo = source_repo
206 c.source_repo = source_repo
206 c.target_repo = target_repo
207 c.target_repo = target_repo
207 c.source_ref = source_ref
208 c.source_ref = source_ref
208 c.target_ref = target_ref
209 c.target_ref = target_ref
209 c.source_ref_type = source_ref_type
210 c.source_ref_type = source_ref_type
210 c.target_ref_type = target_ref_type
211 c.target_ref_type = target_ref_type
211
212
212 pre_load = ["author", "branch", "date", "message"]
213 pre_load = ["author", "branch", "date", "message"]
213 c.ancestor = None
214 c.ancestor = None
214
215
215 if c.file_path:
216 if c.file_path:
216 if source_commit == target_commit:
217 if source_commit == target_commit:
217 c.commit_ranges = []
218 c.commit_ranges = []
218 else:
219 else:
219 c.commit_ranges = [target_commit]
220 c.commit_ranges = [target_commit]
220 else:
221 else:
221 try:
222 try:
222 c.commit_ranges = source_scm.compare(
223 c.commit_ranges = source_scm.compare(
223 source_commit.raw_id, target_commit.raw_id,
224 source_commit.raw_id, target_commit.raw_id,
224 target_scm, merge, pre_load=pre_load)
225 target_scm, merge, pre_load=pre_load)
225 if merge:
226 if merge:
226 c.ancestor = source_scm.get_common_ancestor(
227 c.ancestor = source_scm.get_common_ancestor(
227 source_commit.raw_id, target_commit.raw_id, target_scm)
228 source_commit.raw_id, target_commit.raw_id, target_scm)
228 except RepositoryRequirementError:
229 except RepositoryRequirementError:
229 msg = _('Could not compare repos with different '
230 msg = _('Could not compare repos with different '
230 'large file settings')
231 'large file settings')
231 log.error(msg)
232 log.error(msg)
232 if partial:
233 if partial:
233 return Response(msg)
234 return Response(msg)
234 h.flash(msg, category='error')
235 h.flash(msg, category='error')
235 raise HTTPFound(
236 raise HTTPFound(
236 h.route_path('repo_compare_select',
237 h.route_path('repo_compare_select',
237 repo_name=self.db_repo_name))
238 repo_name=self.db_repo_name))
238
239
239 c.statuses = self.db_repo.statuses(
240 c.statuses = self.db_repo.statuses(
240 [x.raw_id for x in c.commit_ranges])
241 [x.raw_id for x in c.commit_ranges])
241
242
242 # auto collapse if we have more than limit
243 # auto collapse if we have more than limit
243 collapse_limit = diffs.DiffProcessor._collapse_commits_over
244 collapse_limit = diffs.DiffProcessor._collapse_commits_over
244 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
245 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
245
246
246 if partial: # for PR ajax commits loader
247 if partial: # for PR ajax commits loader
247 if not c.ancestor:
248 if not c.ancestor:
248 return Response('') # cannot merge if there is no ancestor
249 return Response('') # cannot merge if there is no ancestor
249
250
250 html = render(
251 html = render(
251 'rhodecode:templates/compare/compare_commits.mako',
252 'rhodecode:templates/compare/compare_commits.mako',
252 self._get_template_context(c), self.request)
253 self._get_template_context(c), self.request)
253 return Response(html)
254 return Response(html)
254
255
255 if c.ancestor:
256 if c.ancestor:
256 # case we want a simple diff without incoming commits,
257 # case we want a simple diff without incoming commits,
257 # previewing what will be merged.
258 # previewing what will be merged.
258 # Make the diff on target repo (which is known to have target_ref)
259 # Make the diff on target repo (which is known to have target_ref)
259 log.debug('Using ancestor %s as source_ref instead of %s',
260 log.debug('Using ancestor %s as source_ref instead of %s',
260 c.ancestor, source_ref)
261 c.ancestor, source_ref)
261 source_repo = target_repo
262 source_repo = target_repo
262 source_commit = target_repo.get_commit(commit_id=c.ancestor)
263 source_commit = target_repo.get_commit(commit_id=c.ancestor)
263
264
264 # diff_limit will cut off the whole diff if the limit is applied
265 # diff_limit will cut off the whole diff if the limit is applied
265 # otherwise it will just hide the big files from the front-end
266 # otherwise it will just hide the big files from the front-end
266 diff_limit = c.visual.cut_off_limit_diff
267 diff_limit = c.visual.cut_off_limit_diff
267 file_limit = c.visual.cut_off_limit_file
268 file_limit = c.visual.cut_off_limit_file
268
269
269 log.debug('calculating diff between '
270 log.debug('calculating diff between '
270 'source_ref:%s and target_ref:%s for repo `%s`',
271 'source_ref:%s and target_ref:%s for repo `%s`',
271 source_commit, target_commit,
272 source_commit, target_commit,
272 safe_unicode(source_repo.scm_instance().path))
273 safe_unicode(source_repo.scm_instance().path))
273
274
274 if source_commit.repository != target_commit.repository:
275 if source_commit.repository != target_commit.repository:
275 msg = _(
276 msg = _(
276 "Repositories unrelated. "
277 "Repositories unrelated. "
277 "Cannot compare commit %(commit1)s from repository %(repo1)s "
278 "Cannot compare commit %(commit1)s from repository %(repo1)s "
278 "with commit %(commit2)s from repository %(repo2)s.") % {
279 "with commit %(commit2)s from repository %(repo2)s.") % {
279 'commit1': h.show_id(source_commit),
280 'commit1': h.show_id(source_commit),
280 'repo1': source_repo.repo_name,
281 'repo1': source_repo.repo_name,
281 'commit2': h.show_id(target_commit),
282 'commit2': h.show_id(target_commit),
282 'repo2': target_repo.repo_name,
283 'repo2': target_repo.repo_name,
283 }
284 }
284 h.flash(msg, category='error')
285 h.flash(msg, category='error')
285 raise HTTPFound(
286 raise HTTPFound(
286 h.route_path('repo_compare_select',
287 h.route_path('repo_compare_select',
287 repo_name=self.db_repo_name))
288 repo_name=self.db_repo_name))
288
289
289 txt_diff = source_repo.scm_instance().get_diff(
290 txt_diff = source_repo.scm_instance().get_diff(
290 commit1=source_commit, commit2=target_commit,
291 commit1=source_commit, commit2=target_commit,
291 path=target_path, path1=source_path)
292 path=target_path, path1=source_path,
293 ignore_whitespace=hide_whitespace_changes, context=diff_context)
292
294
293 diff_processor = diffs.DiffProcessor(
295 diff_processor = diffs.DiffProcessor(
294 txt_diff, format='newdiff', diff_limit=diff_limit,
296 txt_diff, format='newdiff', diff_limit=diff_limit,
295 file_limit=file_limit, show_full_diff=c.fulldiff)
297 file_limit=file_limit, show_full_diff=c.fulldiff)
296 _parsed = diff_processor.prepare()
298 _parsed = diff_processor.prepare()
297
299
298 diffset = codeblocks.DiffSet(
300 diffset = codeblocks.DiffSet(
299 repo_name=source_repo.repo_name,
301 repo_name=source_repo.repo_name,
300 source_node_getter=codeblocks.diffset_node_getter(source_commit),
302 source_node_getter=codeblocks.diffset_node_getter(source_commit),
301 target_node_getter=codeblocks.diffset_node_getter(target_commit),
303 target_node_getter=codeblocks.diffset_node_getter(target_commit),
302 )
304 )
303 c.diffset = self.path_filter.render_patchset_filtered(
305 c.diffset = self.path_filter.render_patchset_filtered(
304 diffset, _parsed, source_ref, target_ref)
306 diffset, _parsed, source_ref, target_ref)
305
307
306 c.preview_mode = merge
308 c.preview_mode = merge
307 c.source_commit = source_commit
309 c.source_commit = source_commit
308 c.target_commit = target_commit
310 c.target_commit = target_commit
309
311
310 html = render(
312 html = render(
311 'rhodecode:templates/compare/compare_diff.mako',
313 'rhodecode:templates/compare/compare_diff.mako',
312 self._get_template_context(c), self.request)
314 self._get_template_context(c), self.request)
313 return Response(html) No newline at end of file
315 return Response(html)
@@ -1,1393 +1,1401 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 RepositoryRequirementError, EmptyRepositoryError)
45 RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 ChangesetComment, ChangesetStatus, Repository)
49 ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
65 c.renderer = 'plain'
66 return c
66 return c
67
67
68 def _get_pull_requests_list(
68 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
70
70
71 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
75
75
76 # pagination
76 # pagination
77
77
78 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name, source=source, opened_by=opened_by,
80 repo_name, source=source, opened_by=opened_by,
81 statuses=statuses, offset=start, length=limit,
81 statuses=statuses, offset=start, length=limit,
82 order_by=order_by, order_dir=order_dir)
82 order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name, source=source, statuses=statuses,
84 repo_name, source=source, statuses=statuses,
85 opened_by=opened_by)
85 opened_by=opened_by)
86 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, source=source, opened_by=opened_by,
88 repo_name, source=source, opened_by=opened_by,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 offset=start, length=limit, order_by=order_by,
90 offset=start, length=limit, order_by=order_by,
91 order_dir=order_dir)
91 order_dir=order_dir)
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 repo_name, source=source, user_id=self._rhodecode_user.user_id,
94 statuses=statuses, opened_by=opened_by)
94 statuses=statuses, opened_by=opened_by)
95 else:
95 else:
96 pull_requests = PullRequestModel().get_all(
96 pull_requests = PullRequestModel().get_all(
97 repo_name, source=source, opened_by=opened_by,
97 repo_name, source=source, opened_by=opened_by,
98 statuses=statuses, offset=start, length=limit,
98 statuses=statuses, offset=start, length=limit,
99 order_by=order_by, order_dir=order_dir)
99 order_by=order_by, order_dir=order_dir)
100 pull_requests_total_count = PullRequestModel().count_all(
100 pull_requests_total_count = PullRequestModel().count_all(
101 repo_name, source=source, statuses=statuses,
101 repo_name, source=source, statuses=statuses,
102 opened_by=opened_by)
102 opened_by=opened_by)
103
103
104 data = []
104 data = []
105 comments_model = CommentsModel()
105 comments_model = CommentsModel()
106 for pr in pull_requests:
106 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
107 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
108 self.db_repo.repo_id, pull_request=pr)
109
109
110 data.append({
110 data.append({
111 'name': _render('pullrequest_name',
111 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.target_repo.repo_name),
112 pr.pull_request_id, pr.target_repo.repo_name),
113 'name_raw': pr.pull_request_id,
113 'name_raw': pr.pull_request_id,
114 'status': _render('pullrequest_status',
114 'status': _render('pullrequest_status',
115 pr.calculated_review_status()),
115 pr.calculated_review_status()),
116 'title': _render(
116 'title': _render(
117 'pullrequest_title', pr.title, pr.description),
117 'pullrequest_title', pr.title, pr.description),
118 'description': h.escape(pr.description),
118 'description': h.escape(pr.description),
119 'updated_on': _render('pullrequest_updated_on',
119 'updated_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.updated_on)),
120 h.datetime_to_time(pr.updated_on)),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'created_on': _render('pullrequest_updated_on',
122 'created_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.created_on)),
123 h.datetime_to_time(pr.created_on)),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'author': _render('pullrequest_author',
125 'author': _render('pullrequest_author',
126 pr.author.full_contact, ),
126 pr.author.full_contact, ),
127 'author_raw': pr.author.full_name,
127 'author_raw': pr.author.full_name,
128 'comments': _render('pullrequest_comments', len(comments)),
128 'comments': _render('pullrequest_comments', len(comments)),
129 'comments_raw': len(comments),
129 'comments_raw': len(comments),
130 'closed': pr.is_closed(),
130 'closed': pr.is_closed(),
131 })
131 })
132
132
133 data = ({
133 data = ({
134 'draw': draw,
134 'draw': draw,
135 'data': data,
135 'data': data,
136 'recordsTotal': pull_requests_total_count,
136 'recordsTotal': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
138 })
138 })
139 return data
139 return data
140
140
141 def get_recache_flag(self):
141 def get_recache_flag(self):
142 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
142 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
143 flag_val = self.request.GET.get(flag_name)
143 flag_val = self.request.GET.get(flag_name)
144 if str2bool(flag_val):
144 if str2bool(flag_val):
145 return True
145 return True
146 return False
146 return False
147
147
148 @LoginRequired()
148 @LoginRequired()
149 @HasRepoPermissionAnyDecorator(
149 @HasRepoPermissionAnyDecorator(
150 'repository.read', 'repository.write', 'repository.admin')
150 'repository.read', 'repository.write', 'repository.admin')
151 @view_config(
151 @view_config(
152 route_name='pullrequest_show_all', request_method='GET',
152 route_name='pullrequest_show_all', request_method='GET',
153 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
153 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
154 def pull_request_list(self):
154 def pull_request_list(self):
155 c = self.load_default_context()
155 c = self.load_default_context()
156
156
157 req_get = self.request.GET
157 req_get = self.request.GET
158 c.source = str2bool(req_get.get('source'))
158 c.source = str2bool(req_get.get('source'))
159 c.closed = str2bool(req_get.get('closed'))
159 c.closed = str2bool(req_get.get('closed'))
160 c.my = str2bool(req_get.get('my'))
160 c.my = str2bool(req_get.get('my'))
161 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
161 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
162 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
162 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
163
163
164 c.active = 'open'
164 c.active = 'open'
165 if c.my:
165 if c.my:
166 c.active = 'my'
166 c.active = 'my'
167 if c.closed:
167 if c.closed:
168 c.active = 'closed'
168 c.active = 'closed'
169 if c.awaiting_review and not c.source:
169 if c.awaiting_review and not c.source:
170 c.active = 'awaiting'
170 c.active = 'awaiting'
171 if c.source and not c.awaiting_review:
171 if c.source and not c.awaiting_review:
172 c.active = 'source'
172 c.active = 'source'
173 if c.awaiting_my_review:
173 if c.awaiting_my_review:
174 c.active = 'awaiting_my'
174 c.active = 'awaiting_my'
175
175
176 return self._get_template_context(c)
176 return self._get_template_context(c)
177
177
178 @LoginRequired()
178 @LoginRequired()
179 @HasRepoPermissionAnyDecorator(
179 @HasRepoPermissionAnyDecorator(
180 'repository.read', 'repository.write', 'repository.admin')
180 'repository.read', 'repository.write', 'repository.admin')
181 @view_config(
181 @view_config(
182 route_name='pullrequest_show_all_data', request_method='GET',
182 route_name='pullrequest_show_all_data', request_method='GET',
183 renderer='json_ext', xhr=True)
183 renderer='json_ext', xhr=True)
184 def pull_request_list_data(self):
184 def pull_request_list_data(self):
185 self.load_default_context()
185 self.load_default_context()
186
186
187 # additional filters
187 # additional filters
188 req_get = self.request.GET
188 req_get = self.request.GET
189 source = str2bool(req_get.get('source'))
189 source = str2bool(req_get.get('source'))
190 closed = str2bool(req_get.get('closed'))
190 closed = str2bool(req_get.get('closed'))
191 my = str2bool(req_get.get('my'))
191 my = str2bool(req_get.get('my'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194
194
195 filter_type = 'awaiting_review' if awaiting_review \
195 filter_type = 'awaiting_review' if awaiting_review \
196 else 'awaiting_my_review' if awaiting_my_review \
196 else 'awaiting_my_review' if awaiting_my_review \
197 else None
197 else None
198
198
199 opened_by = None
199 opened_by = None
200 if my:
200 if my:
201 opened_by = [self._rhodecode_user.user_id]
201 opened_by = [self._rhodecode_user.user_id]
202
202
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 if closed:
204 if closed:
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 data = self._get_pull_requests_list(
207 data = self._get_pull_requests_list(
208 repo_name=self.db_repo_name, source=source,
208 repo_name=self.db_repo_name, source=source,
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210
210
211 return data
211 return data
212
212
213 def _is_diff_cache_enabled(self, target_repo):
213 def _is_diff_cache_enabled(self, target_repo):
214 caching_enabled = self._get_general_setting(
214 caching_enabled = self._get_general_setting(
215 target_repo, 'rhodecode_diff_cache')
215 target_repo, 'rhodecode_diff_cache')
216 log.debug('Diff caching enabled: %s', caching_enabled)
216 log.debug('Diff caching enabled: %s', caching_enabled)
217 return caching_enabled
217 return caching_enabled
218
218
219 def _get_diffset(self, source_repo_name, source_repo,
219 def _get_diffset(self, source_repo_name, source_repo,
220 source_ref_id, target_ref_id,
220 source_ref_id, target_ref_id,
221 target_commit, source_commit, diff_limit, file_limit,
221 target_commit, source_commit, diff_limit, file_limit,
222 fulldiff):
222 fulldiff, hide_whitespace_changes, diff_context):
223
223
224 vcs_diff = PullRequestModel().get_diff(
224 vcs_diff = PullRequestModel().get_diff(
225 source_repo, source_ref_id, target_ref_id)
225 source_repo, source_ref_id, target_ref_id,
226 hide_whitespace_changes, diff_context)
226
227
227 diff_processor = diffs.DiffProcessor(
228 diff_processor = diffs.DiffProcessor(
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 file_limit=file_limit, show_full_diff=fulldiff)
230 file_limit=file_limit, show_full_diff=fulldiff)
230
231
231 _parsed = diff_processor.prepare()
232 _parsed = diff_processor.prepare()
232
233
233 diffset = codeblocks.DiffSet(
234 diffset = codeblocks.DiffSet(
234 repo_name=self.db_repo_name,
235 repo_name=self.db_repo_name,
235 source_repo_name=source_repo_name,
236 source_repo_name=source_repo_name,
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 )
239 )
239 diffset = self.path_filter.render_patchset_filtered(
240 diffset = self.path_filter.render_patchset_filtered(
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241
242
242 return diffset
243 return diffset
243
244
244 def _get_range_diffset(self, source_scm, source_repo,
245 def _get_range_diffset(self, source_scm, source_repo,
245 commit1, commit2, diff_limit, file_limit,
246 commit1, commit2, diff_limit, file_limit,
246 fulldiff, ign_whitespace_lcl, context_lcl):
247 fulldiff, hide_whitespace_changes, diff_context):
247 vcs_diff = source_scm.get_diff(
248 vcs_diff = source_scm.get_diff(
248 commit1, commit2,
249 commit1, commit2,
249 ignore_whitespace=ign_whitespace_lcl,
250 ignore_whitespace=hide_whitespace_changes,
250 context=context_lcl)
251 context=diff_context)
251
252
252 diff_processor = diffs.DiffProcessor(
253 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=fulldiff)
255 file_limit=file_limit, show_full_diff=fulldiff)
255
256
256 _parsed = diff_processor.prepare()
257 _parsed = diff_processor.prepare()
257
258
258 diffset = codeblocks.DiffSet(
259 diffset = codeblocks.DiffSet(
259 repo_name=source_repo.repo_name,
260 repo_name=source_repo.repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
263
263 diffset = self.path_filter.render_patchset_filtered(
264 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
266
266 return diffset
267 return diffset
267
268
268 @LoginRequired()
269 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
270 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
271 'repository.read', 'repository.write', 'repository.admin')
271 @view_config(
272 @view_config(
272 route_name='pullrequest_show', request_method='GET',
273 route_name='pullrequest_show', request_method='GET',
273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 def pull_request_show(self):
275 def pull_request_show(self):
275 pull_request_id = self.request.matchdict['pull_request_id']
276 pull_request_id = self.request.matchdict['pull_request_id']
276
277
277 c = self.load_default_context()
278 c = self.load_default_context()
278
279
279 version = self.request.GET.get('version')
280 version = self.request.GET.get('version')
280 from_version = self.request.GET.get('from_version') or version
281 from_version = self.request.GET.get('from_version') or version
281 merge_checks = self.request.GET.get('merge_checks')
282 merge_checks = self.request.GET.get('merge_checks')
282 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
283 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
284
285 # fetch global flags of ignore ws or context lines
286 diff_context = diffs.get_diff_context(self.request)
287 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
288
283 force_refresh = str2bool(self.request.GET.get('force_refresh'))
289 force_refresh = str2bool(self.request.GET.get('force_refresh'))
284
290
285 (pull_request_latest,
291 (pull_request_latest,
286 pull_request_at_ver,
292 pull_request_at_ver,
287 pull_request_display_obj,
293 pull_request_display_obj,
288 at_version) = PullRequestModel().get_pr_version(
294 at_version) = PullRequestModel().get_pr_version(
289 pull_request_id, version=version)
295 pull_request_id, version=version)
290 pr_closed = pull_request_latest.is_closed()
296 pr_closed = pull_request_latest.is_closed()
291
297
292 if pr_closed and (version or from_version):
298 if pr_closed and (version or from_version):
293 # not allow to browse versions
299 # not allow to browse versions
294 raise HTTPFound(h.route_path(
300 raise HTTPFound(h.route_path(
295 'pullrequest_show', repo_name=self.db_repo_name,
301 'pullrequest_show', repo_name=self.db_repo_name,
296 pull_request_id=pull_request_id))
302 pull_request_id=pull_request_id))
297
303
298 versions = pull_request_display_obj.versions()
304 versions = pull_request_display_obj.versions()
299 # used to store per-commit range diffs
305 # used to store per-commit range diffs
300 c.changes = collections.OrderedDict()
306 c.changes = collections.OrderedDict()
301 c.range_diff_on = self.request.GET.get('range-diff') == "1"
307 c.range_diff_on = self.request.GET.get('range-diff') == "1"
302
308
303 c.at_version = at_version
309 c.at_version = at_version
304 c.at_version_num = (at_version
310 c.at_version_num = (at_version
305 if at_version and at_version != 'latest'
311 if at_version and at_version != 'latest'
306 else None)
312 else None)
307 c.at_version_pos = ChangesetComment.get_index_from_version(
313 c.at_version_pos = ChangesetComment.get_index_from_version(
308 c.at_version_num, versions)
314 c.at_version_num, versions)
309
315
310 (prev_pull_request_latest,
316 (prev_pull_request_latest,
311 prev_pull_request_at_ver,
317 prev_pull_request_at_ver,
312 prev_pull_request_display_obj,
318 prev_pull_request_display_obj,
313 prev_at_version) = PullRequestModel().get_pr_version(
319 prev_at_version) = PullRequestModel().get_pr_version(
314 pull_request_id, version=from_version)
320 pull_request_id, version=from_version)
315
321
316 c.from_version = prev_at_version
322 c.from_version = prev_at_version
317 c.from_version_num = (prev_at_version
323 c.from_version_num = (prev_at_version
318 if prev_at_version and prev_at_version != 'latest'
324 if prev_at_version and prev_at_version != 'latest'
319 else None)
325 else None)
320 c.from_version_pos = ChangesetComment.get_index_from_version(
326 c.from_version_pos = ChangesetComment.get_index_from_version(
321 c.from_version_num, versions)
327 c.from_version_num, versions)
322
328
323 # define if we're in COMPARE mode or VIEW at version mode
329 # define if we're in COMPARE mode or VIEW at version mode
324 compare = at_version != prev_at_version
330 compare = at_version != prev_at_version
325
331
326 # pull_requests repo_name we opened it against
332 # pull_requests repo_name we opened it against
327 # ie. target_repo must match
333 # ie. target_repo must match
328 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
334 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
329 raise HTTPNotFound()
335 raise HTTPNotFound()
330
336
331 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
337 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
332 pull_request_at_ver)
338 pull_request_at_ver)
333
339
334 c.pull_request = pull_request_display_obj
340 c.pull_request = pull_request_display_obj
335 c.renderer = pull_request_at_ver.description_renderer or c.renderer
341 c.renderer = pull_request_at_ver.description_renderer or c.renderer
336 c.pull_request_latest = pull_request_latest
342 c.pull_request_latest = pull_request_latest
337
343
338 if compare or (at_version and not at_version == 'latest'):
344 if compare or (at_version and not at_version == 'latest'):
339 c.allowed_to_change_status = False
345 c.allowed_to_change_status = False
340 c.allowed_to_update = False
346 c.allowed_to_update = False
341 c.allowed_to_merge = False
347 c.allowed_to_merge = False
342 c.allowed_to_delete = False
348 c.allowed_to_delete = False
343 c.allowed_to_comment = False
349 c.allowed_to_comment = False
344 c.allowed_to_close = False
350 c.allowed_to_close = False
345 else:
351 else:
346 can_change_status = PullRequestModel().check_user_change_status(
352 can_change_status = PullRequestModel().check_user_change_status(
347 pull_request_at_ver, self._rhodecode_user)
353 pull_request_at_ver, self._rhodecode_user)
348 c.allowed_to_change_status = can_change_status and not pr_closed
354 c.allowed_to_change_status = can_change_status and not pr_closed
349
355
350 c.allowed_to_update = PullRequestModel().check_user_update(
356 c.allowed_to_update = PullRequestModel().check_user_update(
351 pull_request_latest, self._rhodecode_user) and not pr_closed
357 pull_request_latest, self._rhodecode_user) and not pr_closed
352 c.allowed_to_merge = PullRequestModel().check_user_merge(
358 c.allowed_to_merge = PullRequestModel().check_user_merge(
353 pull_request_latest, self._rhodecode_user) and not pr_closed
359 pull_request_latest, self._rhodecode_user) and not pr_closed
354 c.allowed_to_delete = PullRequestModel().check_user_delete(
360 c.allowed_to_delete = PullRequestModel().check_user_delete(
355 pull_request_latest, self._rhodecode_user) and not pr_closed
361 pull_request_latest, self._rhodecode_user) and not pr_closed
356 c.allowed_to_comment = not pr_closed
362 c.allowed_to_comment = not pr_closed
357 c.allowed_to_close = c.allowed_to_merge and not pr_closed
363 c.allowed_to_close = c.allowed_to_merge and not pr_closed
358
364
359 c.forbid_adding_reviewers = False
365 c.forbid_adding_reviewers = False
360 c.forbid_author_to_review = False
366 c.forbid_author_to_review = False
361 c.forbid_commit_author_to_review = False
367 c.forbid_commit_author_to_review = False
362
368
363 if pull_request_latest.reviewer_data and \
369 if pull_request_latest.reviewer_data and \
364 'rules' in pull_request_latest.reviewer_data:
370 'rules' in pull_request_latest.reviewer_data:
365 rules = pull_request_latest.reviewer_data['rules'] or {}
371 rules = pull_request_latest.reviewer_data['rules'] or {}
366 try:
372 try:
367 c.forbid_adding_reviewers = rules.get(
373 c.forbid_adding_reviewers = rules.get(
368 'forbid_adding_reviewers')
374 'forbid_adding_reviewers')
369 c.forbid_author_to_review = rules.get(
375 c.forbid_author_to_review = rules.get(
370 'forbid_author_to_review')
376 'forbid_author_to_review')
371 c.forbid_commit_author_to_review = rules.get(
377 c.forbid_commit_author_to_review = rules.get(
372 'forbid_commit_author_to_review')
378 'forbid_commit_author_to_review')
373 except Exception:
379 except Exception:
374 pass
380 pass
375
381
376 # check merge capabilities
382 # check merge capabilities
377 _merge_check = MergeCheck.validate(
383 _merge_check = MergeCheck.validate(
378 pull_request_latest, auth_user=self._rhodecode_user,
384 pull_request_latest, auth_user=self._rhodecode_user,
379 translator=self.request.translate,
385 translator=self.request.translate,
380 force_shadow_repo_refresh=force_refresh)
386 force_shadow_repo_refresh=force_refresh)
381 c.pr_merge_errors = _merge_check.error_details
387 c.pr_merge_errors = _merge_check.error_details
382 c.pr_merge_possible = not _merge_check.failed
388 c.pr_merge_possible = not _merge_check.failed
383 c.pr_merge_message = _merge_check.merge_msg
389 c.pr_merge_message = _merge_check.merge_msg
384
390
385 c.pr_merge_info = MergeCheck.get_merge_conditions(
391 c.pr_merge_info = MergeCheck.get_merge_conditions(
386 pull_request_latest, translator=self.request.translate)
392 pull_request_latest, translator=self.request.translate)
387
393
388 c.pull_request_review_status = _merge_check.review_status
394 c.pull_request_review_status = _merge_check.review_status
389 if merge_checks:
395 if merge_checks:
390 self.request.override_renderer = \
396 self.request.override_renderer = \
391 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
397 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
392 return self._get_template_context(c)
398 return self._get_template_context(c)
393
399
394 comments_model = CommentsModel()
400 comments_model = CommentsModel()
395
401
396 # reviewers and statuses
402 # reviewers and statuses
397 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
403 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
398 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
404 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
399
405
400 # GENERAL COMMENTS with versions #
406 # GENERAL COMMENTS with versions #
401 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
407 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
402 q = q.order_by(ChangesetComment.comment_id.asc())
408 q = q.order_by(ChangesetComment.comment_id.asc())
403 general_comments = q
409 general_comments = q
404
410
405 # pick comments we want to render at current version
411 # pick comments we want to render at current version
406 c.comment_versions = comments_model.aggregate_comments(
412 c.comment_versions = comments_model.aggregate_comments(
407 general_comments, versions, c.at_version_num)
413 general_comments, versions, c.at_version_num)
408 c.comments = c.comment_versions[c.at_version_num]['until']
414 c.comments = c.comment_versions[c.at_version_num]['until']
409
415
410 # INLINE COMMENTS with versions #
416 # INLINE COMMENTS with versions #
411 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
417 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
412 q = q.order_by(ChangesetComment.comment_id.asc())
418 q = q.order_by(ChangesetComment.comment_id.asc())
413 inline_comments = q
419 inline_comments = q
414
420
415 c.inline_versions = comments_model.aggregate_comments(
421 c.inline_versions = comments_model.aggregate_comments(
416 inline_comments, versions, c.at_version_num, inline=True)
422 inline_comments, versions, c.at_version_num, inline=True)
417
423
418 # inject latest version
424 # inject latest version
419 latest_ver = PullRequest.get_pr_display_object(
425 latest_ver = PullRequest.get_pr_display_object(
420 pull_request_latest, pull_request_latest)
426 pull_request_latest, pull_request_latest)
421
427
422 c.versions = versions + [latest_ver]
428 c.versions = versions + [latest_ver]
423
429
424 # if we use version, then do not show later comments
430 # if we use version, then do not show later comments
425 # than current version
431 # than current version
426 display_inline_comments = collections.defaultdict(
432 display_inline_comments = collections.defaultdict(
427 lambda: collections.defaultdict(list))
433 lambda: collections.defaultdict(list))
428 for co in inline_comments:
434 for co in inline_comments:
429 if c.at_version_num:
435 if c.at_version_num:
430 # pick comments that are at least UPTO given version, so we
436 # pick comments that are at least UPTO given version, so we
431 # don't render comments for higher version
437 # don't render comments for higher version
432 should_render = co.pull_request_version_id and \
438 should_render = co.pull_request_version_id and \
433 co.pull_request_version_id <= c.at_version_num
439 co.pull_request_version_id <= c.at_version_num
434 else:
440 else:
435 # showing all, for 'latest'
441 # showing all, for 'latest'
436 should_render = True
442 should_render = True
437
443
438 if should_render:
444 if should_render:
439 display_inline_comments[co.f_path][co.line_no].append(co)
445 display_inline_comments[co.f_path][co.line_no].append(co)
440
446
441 # load diff data into template context, if we use compare mode then
447 # load diff data into template context, if we use compare mode then
442 # diff is calculated based on changes between versions of PR
448 # diff is calculated based on changes between versions of PR
443
449
444 source_repo = pull_request_at_ver.source_repo
450 source_repo = pull_request_at_ver.source_repo
445 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
451 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
446
452
447 target_repo = pull_request_at_ver.target_repo
453 target_repo = pull_request_at_ver.target_repo
448 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
454 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
449
455
450 if compare:
456 if compare:
451 # in compare switch the diff base to latest commit from prev version
457 # in compare switch the diff base to latest commit from prev version
452 target_ref_id = prev_pull_request_display_obj.revisions[0]
458 target_ref_id = prev_pull_request_display_obj.revisions[0]
453
459
454 # despite opening commits for bookmarks/branches/tags, we always
460 # despite opening commits for bookmarks/branches/tags, we always
455 # convert this to rev to prevent changes after bookmark or branch change
461 # convert this to rev to prevent changes after bookmark or branch change
456 c.source_ref_type = 'rev'
462 c.source_ref_type = 'rev'
457 c.source_ref = source_ref_id
463 c.source_ref = source_ref_id
458
464
459 c.target_ref_type = 'rev'
465 c.target_ref_type = 'rev'
460 c.target_ref = target_ref_id
466 c.target_ref = target_ref_id
461
467
462 c.source_repo = source_repo
468 c.source_repo = source_repo
463 c.target_repo = target_repo
469 c.target_repo = target_repo
464
470
465 c.commit_ranges = []
471 c.commit_ranges = []
466 source_commit = EmptyCommit()
472 source_commit = EmptyCommit()
467 target_commit = EmptyCommit()
473 target_commit = EmptyCommit()
468 c.missing_requirements = False
474 c.missing_requirements = False
469
475
470 source_scm = source_repo.scm_instance()
476 source_scm = source_repo.scm_instance()
471 target_scm = target_repo.scm_instance()
477 target_scm = target_repo.scm_instance()
472
478
473 shadow_scm = None
479 shadow_scm = None
474 try:
480 try:
475 shadow_scm = pull_request_latest.get_shadow_repo()
481 shadow_scm = pull_request_latest.get_shadow_repo()
476 except Exception:
482 except Exception:
477 log.debug('Failed to get shadow repo', exc_info=True)
483 log.debug('Failed to get shadow repo', exc_info=True)
478 # try first the existing source_repo, and then shadow
484 # try first the existing source_repo, and then shadow
479 # repo if we can obtain one
485 # repo if we can obtain one
480 commits_source_repo = source_scm or shadow_scm
486 commits_source_repo = source_scm or shadow_scm
481
487
482 c.commits_source_repo = commits_source_repo
488 c.commits_source_repo = commits_source_repo
483 c.ancestor = None # set it to None, to hide it from PR view
489 c.ancestor = None # set it to None, to hide it from PR view
484
490
485 # empty version means latest, so we keep this to prevent
491 # empty version means latest, so we keep this to prevent
486 # double caching
492 # double caching
487 version_normalized = version or 'latest'
493 version_normalized = version or 'latest'
488 from_version_normalized = from_version or 'latest'
494 from_version_normalized = from_version or 'latest'
489
495
490 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
496 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
491 cache_file_path = diff_cache_exist(
497 cache_file_path = diff_cache_exist(
492 cache_path, 'pull_request', pull_request_id, version_normalized,
498 cache_path, 'pull_request', pull_request_id, version_normalized,
493 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
499 from_version_normalized, source_ref_id, target_ref_id,
500 hide_whitespace_changes, diff_context, c.fulldiff)
494
501
495 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
502 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
496 force_recache = self.get_recache_flag()
503 force_recache = self.get_recache_flag()
497
504
498 cached_diff = None
505 cached_diff = None
499 if caching_enabled:
506 if caching_enabled:
500 cached_diff = load_cached_diff(cache_file_path)
507 cached_diff = load_cached_diff(cache_file_path)
501
508
502 has_proper_commit_cache = (
509 has_proper_commit_cache = (
503 cached_diff and cached_diff.get('commits')
510 cached_diff and cached_diff.get('commits')
504 and len(cached_diff.get('commits', [])) == 5
511 and len(cached_diff.get('commits', [])) == 5
505 and cached_diff.get('commits')[0]
512 and cached_diff.get('commits')[0]
506 and cached_diff.get('commits')[3])
513 and cached_diff.get('commits')[3])
507
514
508 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
515 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
509 diff_commit_cache = \
516 diff_commit_cache = \
510 (ancestor_commit, commit_cache, missing_requirements,
517 (ancestor_commit, commit_cache, missing_requirements,
511 source_commit, target_commit) = cached_diff['commits']
518 source_commit, target_commit) = cached_diff['commits']
512 else:
519 else:
513 diff_commit_cache = \
520 diff_commit_cache = \
514 (ancestor_commit, commit_cache, missing_requirements,
521 (ancestor_commit, commit_cache, missing_requirements,
515 source_commit, target_commit) = self.get_commits(
522 source_commit, target_commit) = self.get_commits(
516 commits_source_repo,
523 commits_source_repo,
517 pull_request_at_ver,
524 pull_request_at_ver,
518 source_commit,
525 source_commit,
519 source_ref_id,
526 source_ref_id,
520 source_scm,
527 source_scm,
521 target_commit,
528 target_commit,
522 target_ref_id,
529 target_ref_id,
523 target_scm)
530 target_scm)
524
531
525 # register our commit range
532 # register our commit range
526 for comm in commit_cache.values():
533 for comm in commit_cache.values():
527 c.commit_ranges.append(comm)
534 c.commit_ranges.append(comm)
528
535
529 c.missing_requirements = missing_requirements
536 c.missing_requirements = missing_requirements
530 c.ancestor_commit = ancestor_commit
537 c.ancestor_commit = ancestor_commit
531 c.statuses = source_repo.statuses(
538 c.statuses = source_repo.statuses(
532 [x.raw_id for x in c.commit_ranges])
539 [x.raw_id for x in c.commit_ranges])
533
540
534 # auto collapse if we have more than limit
541 # auto collapse if we have more than limit
535 collapse_limit = diffs.DiffProcessor._collapse_commits_over
542 collapse_limit = diffs.DiffProcessor._collapse_commits_over
536 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
543 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
537 c.compare_mode = compare
544 c.compare_mode = compare
538
545
539 # diff_limit is the old behavior, will cut off the whole diff
546 # diff_limit is the old behavior, will cut off the whole diff
540 # if the limit is applied otherwise will just hide the
547 # if the limit is applied otherwise will just hide the
541 # big files from the front-end
548 # big files from the front-end
542 diff_limit = c.visual.cut_off_limit_diff
549 diff_limit = c.visual.cut_off_limit_diff
543 file_limit = c.visual.cut_off_limit_file
550 file_limit = c.visual.cut_off_limit_file
544
551
545 c.missing_commits = False
552 c.missing_commits = False
546 if (c.missing_requirements
553 if (c.missing_requirements
547 or isinstance(source_commit, EmptyCommit)
554 or isinstance(source_commit, EmptyCommit)
548 or source_commit == target_commit):
555 or source_commit == target_commit):
549
556
550 c.missing_commits = True
557 c.missing_commits = True
551 else:
558 else:
552 c.inline_comments = display_inline_comments
559 c.inline_comments = display_inline_comments
553
560
554 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
561 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
555 if not force_recache and has_proper_diff_cache:
562 if not force_recache and has_proper_diff_cache:
556 c.diffset = cached_diff['diff']
563 c.diffset = cached_diff['diff']
557 (ancestor_commit, commit_cache, missing_requirements,
564 (ancestor_commit, commit_cache, missing_requirements,
558 source_commit, target_commit) = cached_diff['commits']
565 source_commit, target_commit) = cached_diff['commits']
559 else:
566 else:
560 c.diffset = self._get_diffset(
567 c.diffset = self._get_diffset(
561 c.source_repo.repo_name, commits_source_repo,
568 c.source_repo.repo_name, commits_source_repo,
562 source_ref_id, target_ref_id,
569 source_ref_id, target_ref_id,
563 target_commit, source_commit,
570 target_commit, source_commit,
564 diff_limit, file_limit, c.fulldiff)
571 diff_limit, file_limit, c.fulldiff,
572 hide_whitespace_changes, diff_context)
565
573
566 # save cached diff
574 # save cached diff
567 if caching_enabled:
575 if caching_enabled:
568 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
576 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
569
577
570 c.limited_diff = c.diffset.limited_diff
578 c.limited_diff = c.diffset.limited_diff
571
579
572 # calculate removed files that are bound to comments
580 # calculate removed files that are bound to comments
573 comment_deleted_files = [
581 comment_deleted_files = [
574 fname for fname in display_inline_comments
582 fname for fname in display_inline_comments
575 if fname not in c.diffset.file_stats]
583 if fname not in c.diffset.file_stats]
576
584
577 c.deleted_files_comments = collections.defaultdict(dict)
585 c.deleted_files_comments = collections.defaultdict(dict)
578 for fname, per_line_comments in display_inline_comments.items():
586 for fname, per_line_comments in display_inline_comments.items():
579 if fname in comment_deleted_files:
587 if fname in comment_deleted_files:
580 c.deleted_files_comments[fname]['stats'] = 0
588 c.deleted_files_comments[fname]['stats'] = 0
581 c.deleted_files_comments[fname]['comments'] = list()
589 c.deleted_files_comments[fname]['comments'] = list()
582 for lno, comments in per_line_comments.items():
590 for lno, comments in per_line_comments.items():
583 c.deleted_files_comments[fname]['comments'].extend(comments)
591 c.deleted_files_comments[fname]['comments'].extend(comments)
584
592
585 # maybe calculate the range diff
593 # maybe calculate the range diff
586 if c.range_diff_on:
594 if c.range_diff_on:
587 # TODO(marcink): set whitespace/context
595 # TODO(marcink): set whitespace/context
588 context_lcl = 3
596 context_lcl = 3
589 ign_whitespace_lcl = False
597 ign_whitespace_lcl = False
590
598
591 for commit in c.commit_ranges:
599 for commit in c.commit_ranges:
592 commit2 = commit
600 commit2 = commit
593 commit1 = commit.first_parent
601 commit1 = commit.first_parent
594
602
595 range_diff_cache_file_path = diff_cache_exist(
603 range_diff_cache_file_path = diff_cache_exist(
596 cache_path, 'diff', commit.raw_id,
604 cache_path, 'diff', commit.raw_id,
597 ign_whitespace_lcl, context_lcl, c.fulldiff)
605 ign_whitespace_lcl, context_lcl, c.fulldiff)
598
606
599 cached_diff = None
607 cached_diff = None
600 if caching_enabled:
608 if caching_enabled:
601 cached_diff = load_cached_diff(range_diff_cache_file_path)
609 cached_diff = load_cached_diff(range_diff_cache_file_path)
602
610
603 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
611 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
604 if not force_recache and has_proper_diff_cache:
612 if not force_recache and has_proper_diff_cache:
605 diffset = cached_diff['diff']
613 diffset = cached_diff['diff']
606 else:
614 else:
607 diffset = self._get_range_diffset(
615 diffset = self._get_range_diffset(
608 source_scm, source_repo,
616 source_scm, source_repo,
609 commit1, commit2, diff_limit, file_limit,
617 commit1, commit2, diff_limit, file_limit,
610 c.fulldiff, ign_whitespace_lcl, context_lcl
618 c.fulldiff, ign_whitespace_lcl, context_lcl
611 )
619 )
612
620
613 # save cached diff
621 # save cached diff
614 if caching_enabled:
622 if caching_enabled:
615 cache_diff(range_diff_cache_file_path, diffset, None)
623 cache_diff(range_diff_cache_file_path, diffset, None)
616
624
617 c.changes[commit.raw_id] = diffset
625 c.changes[commit.raw_id] = diffset
618
626
619 # this is a hack to properly display links, when creating PR, the
627 # this is a hack to properly display links, when creating PR, the
620 # compare view and others uses different notation, and
628 # compare view and others uses different notation, and
621 # compare_commits.mako renders links based on the target_repo.
629 # compare_commits.mako renders links based on the target_repo.
622 # We need to swap that here to generate it properly on the html side
630 # We need to swap that here to generate it properly on the html side
623 c.target_repo = c.source_repo
631 c.target_repo = c.source_repo
624
632
625 c.commit_statuses = ChangesetStatus.STATUSES
633 c.commit_statuses = ChangesetStatus.STATUSES
626
634
627 c.show_version_changes = not pr_closed
635 c.show_version_changes = not pr_closed
628 if c.show_version_changes:
636 if c.show_version_changes:
629 cur_obj = pull_request_at_ver
637 cur_obj = pull_request_at_ver
630 prev_obj = prev_pull_request_at_ver
638 prev_obj = prev_pull_request_at_ver
631
639
632 old_commit_ids = prev_obj.revisions
640 old_commit_ids = prev_obj.revisions
633 new_commit_ids = cur_obj.revisions
641 new_commit_ids = cur_obj.revisions
634 commit_changes = PullRequestModel()._calculate_commit_id_changes(
642 commit_changes = PullRequestModel()._calculate_commit_id_changes(
635 old_commit_ids, new_commit_ids)
643 old_commit_ids, new_commit_ids)
636 c.commit_changes_summary = commit_changes
644 c.commit_changes_summary = commit_changes
637
645
638 # calculate the diff for commits between versions
646 # calculate the diff for commits between versions
639 c.commit_changes = []
647 c.commit_changes = []
640 mark = lambda cs, fw: list(
648 mark = lambda cs, fw: list(
641 h.itertools.izip_longest([], cs, fillvalue=fw))
649 h.itertools.izip_longest([], cs, fillvalue=fw))
642 for c_type, raw_id in mark(commit_changes.added, 'a') \
650 for c_type, raw_id in mark(commit_changes.added, 'a') \
643 + mark(commit_changes.removed, 'r') \
651 + mark(commit_changes.removed, 'r') \
644 + mark(commit_changes.common, 'c'):
652 + mark(commit_changes.common, 'c'):
645
653
646 if raw_id in commit_cache:
654 if raw_id in commit_cache:
647 commit = commit_cache[raw_id]
655 commit = commit_cache[raw_id]
648 else:
656 else:
649 try:
657 try:
650 commit = commits_source_repo.get_commit(raw_id)
658 commit = commits_source_repo.get_commit(raw_id)
651 except CommitDoesNotExistError:
659 except CommitDoesNotExistError:
652 # in case we fail extracting still use "dummy" commit
660 # in case we fail extracting still use "dummy" commit
653 # for display in commit diff
661 # for display in commit diff
654 commit = h.AttributeDict(
662 commit = h.AttributeDict(
655 {'raw_id': raw_id,
663 {'raw_id': raw_id,
656 'message': 'EMPTY or MISSING COMMIT'})
664 'message': 'EMPTY or MISSING COMMIT'})
657 c.commit_changes.append([c_type, commit])
665 c.commit_changes.append([c_type, commit])
658
666
659 # current user review statuses for each version
667 # current user review statuses for each version
660 c.review_versions = {}
668 c.review_versions = {}
661 if self._rhodecode_user.user_id in allowed_reviewers:
669 if self._rhodecode_user.user_id in allowed_reviewers:
662 for co in general_comments:
670 for co in general_comments:
663 if co.author.user_id == self._rhodecode_user.user_id:
671 if co.author.user_id == self._rhodecode_user.user_id:
664 status = co.status_change
672 status = co.status_change
665 if status:
673 if status:
666 _ver_pr = status[0].comment.pull_request_version_id
674 _ver_pr = status[0].comment.pull_request_version_id
667 c.review_versions[_ver_pr] = status[0]
675 c.review_versions[_ver_pr] = status[0]
668
676
669 return self._get_template_context(c)
677 return self._get_template_context(c)
670
678
671 def get_commits(
679 def get_commits(
672 self, commits_source_repo, pull_request_at_ver, source_commit,
680 self, commits_source_repo, pull_request_at_ver, source_commit,
673 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
681 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
674 commit_cache = collections.OrderedDict()
682 commit_cache = collections.OrderedDict()
675 missing_requirements = False
683 missing_requirements = False
676 try:
684 try:
677 pre_load = ["author", "branch", "date", "message", "parents"]
685 pre_load = ["author", "branch", "date", "message", "parents"]
678 show_revs = pull_request_at_ver.revisions
686 show_revs = pull_request_at_ver.revisions
679 for rev in show_revs:
687 for rev in show_revs:
680 comm = commits_source_repo.get_commit(
688 comm = commits_source_repo.get_commit(
681 commit_id=rev, pre_load=pre_load)
689 commit_id=rev, pre_load=pre_load)
682 commit_cache[comm.raw_id] = comm
690 commit_cache[comm.raw_id] = comm
683
691
684 # Order here matters, we first need to get target, and then
692 # Order here matters, we first need to get target, and then
685 # the source
693 # the source
686 target_commit = commits_source_repo.get_commit(
694 target_commit = commits_source_repo.get_commit(
687 commit_id=safe_str(target_ref_id))
695 commit_id=safe_str(target_ref_id))
688
696
689 source_commit = commits_source_repo.get_commit(
697 source_commit = commits_source_repo.get_commit(
690 commit_id=safe_str(source_ref_id))
698 commit_id=safe_str(source_ref_id))
691 except CommitDoesNotExistError:
699 except CommitDoesNotExistError:
692 log.warning(
700 log.warning(
693 'Failed to get commit from `{}` repo'.format(
701 'Failed to get commit from `{}` repo'.format(
694 commits_source_repo), exc_info=True)
702 commits_source_repo), exc_info=True)
695 except RepositoryRequirementError:
703 except RepositoryRequirementError:
696 log.warning(
704 log.warning(
697 'Failed to get all required data from repo', exc_info=True)
705 'Failed to get all required data from repo', exc_info=True)
698 missing_requirements = True
706 missing_requirements = True
699 ancestor_commit = None
707 ancestor_commit = None
700 try:
708 try:
701 ancestor_id = source_scm.get_common_ancestor(
709 ancestor_id = source_scm.get_common_ancestor(
702 source_commit.raw_id, target_commit.raw_id, target_scm)
710 source_commit.raw_id, target_commit.raw_id, target_scm)
703 ancestor_commit = source_scm.get_commit(ancestor_id)
711 ancestor_commit = source_scm.get_commit(ancestor_id)
704 except Exception:
712 except Exception:
705 ancestor_commit = None
713 ancestor_commit = None
706 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
714 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
707
715
708 def assure_not_empty_repo(self):
716 def assure_not_empty_repo(self):
709 _ = self.request.translate
717 _ = self.request.translate
710
718
711 try:
719 try:
712 self.db_repo.scm_instance().get_commit()
720 self.db_repo.scm_instance().get_commit()
713 except EmptyRepositoryError:
721 except EmptyRepositoryError:
714 h.flash(h.literal(_('There are no commits yet')),
722 h.flash(h.literal(_('There are no commits yet')),
715 category='warning')
723 category='warning')
716 raise HTTPFound(
724 raise HTTPFound(
717 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
725 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
718
726
719 @LoginRequired()
727 @LoginRequired()
720 @NotAnonymous()
728 @NotAnonymous()
721 @HasRepoPermissionAnyDecorator(
729 @HasRepoPermissionAnyDecorator(
722 'repository.read', 'repository.write', 'repository.admin')
730 'repository.read', 'repository.write', 'repository.admin')
723 @view_config(
731 @view_config(
724 route_name='pullrequest_new', request_method='GET',
732 route_name='pullrequest_new', request_method='GET',
725 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
733 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
726 def pull_request_new(self):
734 def pull_request_new(self):
727 _ = self.request.translate
735 _ = self.request.translate
728 c = self.load_default_context()
736 c = self.load_default_context()
729
737
730 self.assure_not_empty_repo()
738 self.assure_not_empty_repo()
731 source_repo = self.db_repo
739 source_repo = self.db_repo
732
740
733 commit_id = self.request.GET.get('commit')
741 commit_id = self.request.GET.get('commit')
734 branch_ref = self.request.GET.get('branch')
742 branch_ref = self.request.GET.get('branch')
735 bookmark_ref = self.request.GET.get('bookmark')
743 bookmark_ref = self.request.GET.get('bookmark')
736
744
737 try:
745 try:
738 source_repo_data = PullRequestModel().generate_repo_data(
746 source_repo_data = PullRequestModel().generate_repo_data(
739 source_repo, commit_id=commit_id,
747 source_repo, commit_id=commit_id,
740 branch=branch_ref, bookmark=bookmark_ref,
748 branch=branch_ref, bookmark=bookmark_ref,
741 translator=self.request.translate)
749 translator=self.request.translate)
742 except CommitDoesNotExistError as e:
750 except CommitDoesNotExistError as e:
743 log.exception(e)
751 log.exception(e)
744 h.flash(_('Commit does not exist'), 'error')
752 h.flash(_('Commit does not exist'), 'error')
745 raise HTTPFound(
753 raise HTTPFound(
746 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
754 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
747
755
748 default_target_repo = source_repo
756 default_target_repo = source_repo
749
757
750 if source_repo.parent:
758 if source_repo.parent:
751 parent_vcs_obj = source_repo.parent.scm_instance()
759 parent_vcs_obj = source_repo.parent.scm_instance()
752 if parent_vcs_obj and not parent_vcs_obj.is_empty():
760 if parent_vcs_obj and not parent_vcs_obj.is_empty():
753 # change default if we have a parent repo
761 # change default if we have a parent repo
754 default_target_repo = source_repo.parent
762 default_target_repo = source_repo.parent
755
763
756 target_repo_data = PullRequestModel().generate_repo_data(
764 target_repo_data = PullRequestModel().generate_repo_data(
757 default_target_repo, translator=self.request.translate)
765 default_target_repo, translator=self.request.translate)
758
766
759 selected_source_ref = source_repo_data['refs']['selected_ref']
767 selected_source_ref = source_repo_data['refs']['selected_ref']
760 title_source_ref = ''
768 title_source_ref = ''
761 if selected_source_ref:
769 if selected_source_ref:
762 title_source_ref = selected_source_ref.split(':', 2)[1]
770 title_source_ref = selected_source_ref.split(':', 2)[1]
763 c.default_title = PullRequestModel().generate_pullrequest_title(
771 c.default_title = PullRequestModel().generate_pullrequest_title(
764 source=source_repo.repo_name,
772 source=source_repo.repo_name,
765 source_ref=title_source_ref,
773 source_ref=title_source_ref,
766 target=default_target_repo.repo_name
774 target=default_target_repo.repo_name
767 )
775 )
768
776
769 c.default_repo_data = {
777 c.default_repo_data = {
770 'source_repo_name': source_repo.repo_name,
778 'source_repo_name': source_repo.repo_name,
771 'source_refs_json': json.dumps(source_repo_data),
779 'source_refs_json': json.dumps(source_repo_data),
772 'target_repo_name': default_target_repo.repo_name,
780 'target_repo_name': default_target_repo.repo_name,
773 'target_refs_json': json.dumps(target_repo_data),
781 'target_refs_json': json.dumps(target_repo_data),
774 }
782 }
775 c.default_source_ref = selected_source_ref
783 c.default_source_ref = selected_source_ref
776
784
777 return self._get_template_context(c)
785 return self._get_template_context(c)
778
786
779 @LoginRequired()
787 @LoginRequired()
780 @NotAnonymous()
788 @NotAnonymous()
781 @HasRepoPermissionAnyDecorator(
789 @HasRepoPermissionAnyDecorator(
782 'repository.read', 'repository.write', 'repository.admin')
790 'repository.read', 'repository.write', 'repository.admin')
783 @view_config(
791 @view_config(
784 route_name='pullrequest_repo_refs', request_method='GET',
792 route_name='pullrequest_repo_refs', request_method='GET',
785 renderer='json_ext', xhr=True)
793 renderer='json_ext', xhr=True)
786 def pull_request_repo_refs(self):
794 def pull_request_repo_refs(self):
787 self.load_default_context()
795 self.load_default_context()
788 target_repo_name = self.request.matchdict['target_repo_name']
796 target_repo_name = self.request.matchdict['target_repo_name']
789 repo = Repository.get_by_repo_name(target_repo_name)
797 repo = Repository.get_by_repo_name(target_repo_name)
790 if not repo:
798 if not repo:
791 raise HTTPNotFound()
799 raise HTTPNotFound()
792
800
793 target_perm = HasRepoPermissionAny(
801 target_perm = HasRepoPermissionAny(
794 'repository.read', 'repository.write', 'repository.admin')(
802 'repository.read', 'repository.write', 'repository.admin')(
795 target_repo_name)
803 target_repo_name)
796 if not target_perm:
804 if not target_perm:
797 raise HTTPNotFound()
805 raise HTTPNotFound()
798
806
799 return PullRequestModel().generate_repo_data(
807 return PullRequestModel().generate_repo_data(
800 repo, translator=self.request.translate)
808 repo, translator=self.request.translate)
801
809
802 @LoginRequired()
810 @LoginRequired()
803 @NotAnonymous()
811 @NotAnonymous()
804 @HasRepoPermissionAnyDecorator(
812 @HasRepoPermissionAnyDecorator(
805 'repository.read', 'repository.write', 'repository.admin')
813 'repository.read', 'repository.write', 'repository.admin')
806 @view_config(
814 @view_config(
807 route_name='pullrequest_repo_destinations', request_method='GET',
815 route_name='pullrequest_repo_destinations', request_method='GET',
808 renderer='json_ext', xhr=True)
816 renderer='json_ext', xhr=True)
809 def pull_request_repo_destinations(self):
817 def pull_request_repo_destinations(self):
810 _ = self.request.translate
818 _ = self.request.translate
811 filter_query = self.request.GET.get('query')
819 filter_query = self.request.GET.get('query')
812
820
813 query = Repository.query() \
821 query = Repository.query() \
814 .order_by(func.length(Repository.repo_name)) \
822 .order_by(func.length(Repository.repo_name)) \
815 .filter(
823 .filter(
816 or_(Repository.repo_name == self.db_repo.repo_name,
824 or_(Repository.repo_name == self.db_repo.repo_name,
817 Repository.fork_id == self.db_repo.repo_id))
825 Repository.fork_id == self.db_repo.repo_id))
818
826
819 if filter_query:
827 if filter_query:
820 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
828 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
821 query = query.filter(
829 query = query.filter(
822 Repository.repo_name.ilike(ilike_expression))
830 Repository.repo_name.ilike(ilike_expression))
823
831
824 add_parent = False
832 add_parent = False
825 if self.db_repo.parent:
833 if self.db_repo.parent:
826 if filter_query in self.db_repo.parent.repo_name:
834 if filter_query in self.db_repo.parent.repo_name:
827 parent_vcs_obj = self.db_repo.parent.scm_instance()
835 parent_vcs_obj = self.db_repo.parent.scm_instance()
828 if parent_vcs_obj and not parent_vcs_obj.is_empty():
836 if parent_vcs_obj and not parent_vcs_obj.is_empty():
829 add_parent = True
837 add_parent = True
830
838
831 limit = 20 - 1 if add_parent else 20
839 limit = 20 - 1 if add_parent else 20
832 all_repos = query.limit(limit).all()
840 all_repos = query.limit(limit).all()
833 if add_parent:
841 if add_parent:
834 all_repos += [self.db_repo.parent]
842 all_repos += [self.db_repo.parent]
835
843
836 repos = []
844 repos = []
837 for obj in ScmModel().get_repos(all_repos):
845 for obj in ScmModel().get_repos(all_repos):
838 repos.append({
846 repos.append({
839 'id': obj['name'],
847 'id': obj['name'],
840 'text': obj['name'],
848 'text': obj['name'],
841 'type': 'repo',
849 'type': 'repo',
842 'repo_id': obj['dbrepo']['repo_id'],
850 'repo_id': obj['dbrepo']['repo_id'],
843 'repo_type': obj['dbrepo']['repo_type'],
851 'repo_type': obj['dbrepo']['repo_type'],
844 'private': obj['dbrepo']['private'],
852 'private': obj['dbrepo']['private'],
845
853
846 })
854 })
847
855
848 data = {
856 data = {
849 'more': False,
857 'more': False,
850 'results': [{
858 'results': [{
851 'text': _('Repositories'),
859 'text': _('Repositories'),
852 'children': repos
860 'children': repos
853 }] if repos else []
861 }] if repos else []
854 }
862 }
855 return data
863 return data
856
864
857 @LoginRequired()
865 @LoginRequired()
858 @NotAnonymous()
866 @NotAnonymous()
859 @HasRepoPermissionAnyDecorator(
867 @HasRepoPermissionAnyDecorator(
860 'repository.read', 'repository.write', 'repository.admin')
868 'repository.read', 'repository.write', 'repository.admin')
861 @CSRFRequired()
869 @CSRFRequired()
862 @view_config(
870 @view_config(
863 route_name='pullrequest_create', request_method='POST',
871 route_name='pullrequest_create', request_method='POST',
864 renderer=None)
872 renderer=None)
865 def pull_request_create(self):
873 def pull_request_create(self):
866 _ = self.request.translate
874 _ = self.request.translate
867 self.assure_not_empty_repo()
875 self.assure_not_empty_repo()
868 self.load_default_context()
876 self.load_default_context()
869
877
870 controls = peppercorn.parse(self.request.POST.items())
878 controls = peppercorn.parse(self.request.POST.items())
871
879
872 try:
880 try:
873 form = PullRequestForm(
881 form = PullRequestForm(
874 self.request.translate, self.db_repo.repo_id)()
882 self.request.translate, self.db_repo.repo_id)()
875 _form = form.to_python(controls)
883 _form = form.to_python(controls)
876 except formencode.Invalid as errors:
884 except formencode.Invalid as errors:
877 if errors.error_dict.get('revisions'):
885 if errors.error_dict.get('revisions'):
878 msg = 'Revisions: %s' % errors.error_dict['revisions']
886 msg = 'Revisions: %s' % errors.error_dict['revisions']
879 elif errors.error_dict.get('pullrequest_title'):
887 elif errors.error_dict.get('pullrequest_title'):
880 msg = errors.error_dict.get('pullrequest_title')
888 msg = errors.error_dict.get('pullrequest_title')
881 else:
889 else:
882 msg = _('Error creating pull request: {}').format(errors)
890 msg = _('Error creating pull request: {}').format(errors)
883 log.exception(msg)
891 log.exception(msg)
884 h.flash(msg, 'error')
892 h.flash(msg, 'error')
885
893
886 # would rather just go back to form ...
894 # would rather just go back to form ...
887 raise HTTPFound(
895 raise HTTPFound(
888 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
896 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
889
897
890 source_repo = _form['source_repo']
898 source_repo = _form['source_repo']
891 source_ref = _form['source_ref']
899 source_ref = _form['source_ref']
892 target_repo = _form['target_repo']
900 target_repo = _form['target_repo']
893 target_ref = _form['target_ref']
901 target_ref = _form['target_ref']
894 commit_ids = _form['revisions'][::-1]
902 commit_ids = _form['revisions'][::-1]
895
903
896 # find the ancestor for this pr
904 # find the ancestor for this pr
897 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
905 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
898 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
906 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
899
907
900 # re-check permissions again here
908 # re-check permissions again here
901 # source_repo we must have read permissions
909 # source_repo we must have read permissions
902
910
903 source_perm = HasRepoPermissionAny(
911 source_perm = HasRepoPermissionAny(
904 'repository.read',
912 'repository.read',
905 'repository.write', 'repository.admin')(source_db_repo.repo_name)
913 'repository.write', 'repository.admin')(source_db_repo.repo_name)
906 if not source_perm:
914 if not source_perm:
907 msg = _('Not Enough permissions to source repo `{}`.'.format(
915 msg = _('Not Enough permissions to source repo `{}`.'.format(
908 source_db_repo.repo_name))
916 source_db_repo.repo_name))
909 h.flash(msg, category='error')
917 h.flash(msg, category='error')
910 # copy the args back to redirect
918 # copy the args back to redirect
911 org_query = self.request.GET.mixed()
919 org_query = self.request.GET.mixed()
912 raise HTTPFound(
920 raise HTTPFound(
913 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
921 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
914 _query=org_query))
922 _query=org_query))
915
923
916 # target repo we must have read permissions, and also later on
924 # target repo we must have read permissions, and also later on
917 # we want to check branch permissions here
925 # we want to check branch permissions here
918 target_perm = HasRepoPermissionAny(
926 target_perm = HasRepoPermissionAny(
919 'repository.read',
927 'repository.read',
920 'repository.write', 'repository.admin')(target_db_repo.repo_name)
928 'repository.write', 'repository.admin')(target_db_repo.repo_name)
921 if not target_perm:
929 if not target_perm:
922 msg = _('Not Enough permissions to target repo `{}`.'.format(
930 msg = _('Not Enough permissions to target repo `{}`.'.format(
923 target_db_repo.repo_name))
931 target_db_repo.repo_name))
924 h.flash(msg, category='error')
932 h.flash(msg, category='error')
925 # copy the args back to redirect
933 # copy the args back to redirect
926 org_query = self.request.GET.mixed()
934 org_query = self.request.GET.mixed()
927 raise HTTPFound(
935 raise HTTPFound(
928 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
936 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
929 _query=org_query))
937 _query=org_query))
930
938
931 source_scm = source_db_repo.scm_instance()
939 source_scm = source_db_repo.scm_instance()
932 target_scm = target_db_repo.scm_instance()
940 target_scm = target_db_repo.scm_instance()
933
941
934 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
942 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
935 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
943 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
936
944
937 ancestor = source_scm.get_common_ancestor(
945 ancestor = source_scm.get_common_ancestor(
938 source_commit.raw_id, target_commit.raw_id, target_scm)
946 source_commit.raw_id, target_commit.raw_id, target_scm)
939
947
940 # recalculate target ref based on ancestor
948 # recalculate target ref based on ancestor
941 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
949 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
942 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
950 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
943
951
944 get_default_reviewers_data, validate_default_reviewers = \
952 get_default_reviewers_data, validate_default_reviewers = \
945 PullRequestModel().get_reviewer_functions()
953 PullRequestModel().get_reviewer_functions()
946
954
947 # recalculate reviewers logic, to make sure we can validate this
955 # recalculate reviewers logic, to make sure we can validate this
948 reviewer_rules = get_default_reviewers_data(
956 reviewer_rules = get_default_reviewers_data(
949 self._rhodecode_db_user, source_db_repo,
957 self._rhodecode_db_user, source_db_repo,
950 source_commit, target_db_repo, target_commit)
958 source_commit, target_db_repo, target_commit)
951
959
952 given_reviewers = _form['review_members']
960 given_reviewers = _form['review_members']
953 reviewers = validate_default_reviewers(
961 reviewers = validate_default_reviewers(
954 given_reviewers, reviewer_rules)
962 given_reviewers, reviewer_rules)
955
963
956 pullrequest_title = _form['pullrequest_title']
964 pullrequest_title = _form['pullrequest_title']
957 title_source_ref = source_ref.split(':', 2)[1]
965 title_source_ref = source_ref.split(':', 2)[1]
958 if not pullrequest_title:
966 if not pullrequest_title:
959 pullrequest_title = PullRequestModel().generate_pullrequest_title(
967 pullrequest_title = PullRequestModel().generate_pullrequest_title(
960 source=source_repo,
968 source=source_repo,
961 source_ref=title_source_ref,
969 source_ref=title_source_ref,
962 target=target_repo
970 target=target_repo
963 )
971 )
964
972
965 description = _form['pullrequest_desc']
973 description = _form['pullrequest_desc']
966 description_renderer = _form['description_renderer']
974 description_renderer = _form['description_renderer']
967
975
968 try:
976 try:
969 pull_request = PullRequestModel().create(
977 pull_request = PullRequestModel().create(
970 created_by=self._rhodecode_user.user_id,
978 created_by=self._rhodecode_user.user_id,
971 source_repo=source_repo,
979 source_repo=source_repo,
972 source_ref=source_ref,
980 source_ref=source_ref,
973 target_repo=target_repo,
981 target_repo=target_repo,
974 target_ref=target_ref,
982 target_ref=target_ref,
975 revisions=commit_ids,
983 revisions=commit_ids,
976 reviewers=reviewers,
984 reviewers=reviewers,
977 title=pullrequest_title,
985 title=pullrequest_title,
978 description=description,
986 description=description,
979 description_renderer=description_renderer,
987 description_renderer=description_renderer,
980 reviewer_data=reviewer_rules,
988 reviewer_data=reviewer_rules,
981 auth_user=self._rhodecode_user
989 auth_user=self._rhodecode_user
982 )
990 )
983 Session().commit()
991 Session().commit()
984
992
985 h.flash(_('Successfully opened new pull request'),
993 h.flash(_('Successfully opened new pull request'),
986 category='success')
994 category='success')
987 except Exception:
995 except Exception:
988 msg = _('Error occurred during creation of this pull request.')
996 msg = _('Error occurred during creation of this pull request.')
989 log.exception(msg)
997 log.exception(msg)
990 h.flash(msg, category='error')
998 h.flash(msg, category='error')
991
999
992 # copy the args back to redirect
1000 # copy the args back to redirect
993 org_query = self.request.GET.mixed()
1001 org_query = self.request.GET.mixed()
994 raise HTTPFound(
1002 raise HTTPFound(
995 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1003 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
996 _query=org_query))
1004 _query=org_query))
997
1005
998 raise HTTPFound(
1006 raise HTTPFound(
999 h.route_path('pullrequest_show', repo_name=target_repo,
1007 h.route_path('pullrequest_show', repo_name=target_repo,
1000 pull_request_id=pull_request.pull_request_id))
1008 pull_request_id=pull_request.pull_request_id))
1001
1009
1002 @LoginRequired()
1010 @LoginRequired()
1003 @NotAnonymous()
1011 @NotAnonymous()
1004 @HasRepoPermissionAnyDecorator(
1012 @HasRepoPermissionAnyDecorator(
1005 'repository.read', 'repository.write', 'repository.admin')
1013 'repository.read', 'repository.write', 'repository.admin')
1006 @CSRFRequired()
1014 @CSRFRequired()
1007 @view_config(
1015 @view_config(
1008 route_name='pullrequest_update', request_method='POST',
1016 route_name='pullrequest_update', request_method='POST',
1009 renderer='json_ext')
1017 renderer='json_ext')
1010 def pull_request_update(self):
1018 def pull_request_update(self):
1011 pull_request = PullRequest.get_or_404(
1019 pull_request = PullRequest.get_or_404(
1012 self.request.matchdict['pull_request_id'])
1020 self.request.matchdict['pull_request_id'])
1013 _ = self.request.translate
1021 _ = self.request.translate
1014
1022
1015 self.load_default_context()
1023 self.load_default_context()
1016
1024
1017 if pull_request.is_closed():
1025 if pull_request.is_closed():
1018 log.debug('update: forbidden because pull request is closed')
1026 log.debug('update: forbidden because pull request is closed')
1019 msg = _(u'Cannot update closed pull requests.')
1027 msg = _(u'Cannot update closed pull requests.')
1020 h.flash(msg, category='error')
1028 h.flash(msg, category='error')
1021 return True
1029 return True
1022
1030
1023 # only owner or admin can update it
1031 # only owner or admin can update it
1024 allowed_to_update = PullRequestModel().check_user_update(
1032 allowed_to_update = PullRequestModel().check_user_update(
1025 pull_request, self._rhodecode_user)
1033 pull_request, self._rhodecode_user)
1026 if allowed_to_update:
1034 if allowed_to_update:
1027 controls = peppercorn.parse(self.request.POST.items())
1035 controls = peppercorn.parse(self.request.POST.items())
1028
1036
1029 if 'review_members' in controls:
1037 if 'review_members' in controls:
1030 self._update_reviewers(
1038 self._update_reviewers(
1031 pull_request, controls['review_members'],
1039 pull_request, controls['review_members'],
1032 pull_request.reviewer_data)
1040 pull_request.reviewer_data)
1033 elif str2bool(self.request.POST.get('update_commits', 'false')):
1041 elif str2bool(self.request.POST.get('update_commits', 'false')):
1034 self._update_commits(pull_request)
1042 self._update_commits(pull_request)
1035 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1043 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1036 self._edit_pull_request(pull_request)
1044 self._edit_pull_request(pull_request)
1037 else:
1045 else:
1038 raise HTTPBadRequest()
1046 raise HTTPBadRequest()
1039 return True
1047 return True
1040 raise HTTPForbidden()
1048 raise HTTPForbidden()
1041
1049
1042 def _edit_pull_request(self, pull_request):
1050 def _edit_pull_request(self, pull_request):
1043 _ = self.request.translate
1051 _ = self.request.translate
1044
1052
1045 try:
1053 try:
1046 PullRequestModel().edit(
1054 PullRequestModel().edit(
1047 pull_request,
1055 pull_request,
1048 self.request.POST.get('title'),
1056 self.request.POST.get('title'),
1049 self.request.POST.get('description'),
1057 self.request.POST.get('description'),
1050 self.request.POST.get('description_renderer'),
1058 self.request.POST.get('description_renderer'),
1051 self._rhodecode_user)
1059 self._rhodecode_user)
1052 except ValueError:
1060 except ValueError:
1053 msg = _(u'Cannot update closed pull requests.')
1061 msg = _(u'Cannot update closed pull requests.')
1054 h.flash(msg, category='error')
1062 h.flash(msg, category='error')
1055 return
1063 return
1056 else:
1064 else:
1057 Session().commit()
1065 Session().commit()
1058
1066
1059 msg = _(u'Pull request title & description updated.')
1067 msg = _(u'Pull request title & description updated.')
1060 h.flash(msg, category='success')
1068 h.flash(msg, category='success')
1061 return
1069 return
1062
1070
1063 def _update_commits(self, pull_request):
1071 def _update_commits(self, pull_request):
1064 _ = self.request.translate
1072 _ = self.request.translate
1065 resp = PullRequestModel().update_commits(pull_request)
1073 resp = PullRequestModel().update_commits(pull_request)
1066
1074
1067 if resp.executed:
1075 if resp.executed:
1068
1076
1069 if resp.target_changed and resp.source_changed:
1077 if resp.target_changed and resp.source_changed:
1070 changed = 'target and source repositories'
1078 changed = 'target and source repositories'
1071 elif resp.target_changed and not resp.source_changed:
1079 elif resp.target_changed and not resp.source_changed:
1072 changed = 'target repository'
1080 changed = 'target repository'
1073 elif not resp.target_changed and resp.source_changed:
1081 elif not resp.target_changed and resp.source_changed:
1074 changed = 'source repository'
1082 changed = 'source repository'
1075 else:
1083 else:
1076 changed = 'nothing'
1084 changed = 'nothing'
1077
1085
1078 msg = _(
1086 msg = _(
1079 u'Pull request updated to "{source_commit_id}" with '
1087 u'Pull request updated to "{source_commit_id}" with '
1080 u'{count_added} added, {count_removed} removed commits. '
1088 u'{count_added} added, {count_removed} removed commits. '
1081 u'Source of changes: {change_source}')
1089 u'Source of changes: {change_source}')
1082 msg = msg.format(
1090 msg = msg.format(
1083 source_commit_id=pull_request.source_ref_parts.commit_id,
1091 source_commit_id=pull_request.source_ref_parts.commit_id,
1084 count_added=len(resp.changes.added),
1092 count_added=len(resp.changes.added),
1085 count_removed=len(resp.changes.removed),
1093 count_removed=len(resp.changes.removed),
1086 change_source=changed)
1094 change_source=changed)
1087 h.flash(msg, category='success')
1095 h.flash(msg, category='success')
1088
1096
1089 channel = '/repo${}$/pr/{}'.format(
1097 channel = '/repo${}$/pr/{}'.format(
1090 pull_request.target_repo.repo_name,
1098 pull_request.target_repo.repo_name,
1091 pull_request.pull_request_id)
1099 pull_request.pull_request_id)
1092 message = msg + (
1100 message = msg + (
1093 ' - <a onclick="window.location.reload()">'
1101 ' - <a onclick="window.location.reload()">'
1094 '<strong>{}</strong></a>'.format(_('Reload page')))
1102 '<strong>{}</strong></a>'.format(_('Reload page')))
1095 channelstream.post_message(
1103 channelstream.post_message(
1096 channel, message, self._rhodecode_user.username,
1104 channel, message, self._rhodecode_user.username,
1097 registry=self.request.registry)
1105 registry=self.request.registry)
1098 else:
1106 else:
1099 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1107 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1100 warning_reasons = [
1108 warning_reasons = [
1101 UpdateFailureReason.NO_CHANGE,
1109 UpdateFailureReason.NO_CHANGE,
1102 UpdateFailureReason.WRONG_REF_TYPE,
1110 UpdateFailureReason.WRONG_REF_TYPE,
1103 ]
1111 ]
1104 category = 'warning' if resp.reason in warning_reasons else 'error'
1112 category = 'warning' if resp.reason in warning_reasons else 'error'
1105 h.flash(msg, category=category)
1113 h.flash(msg, category=category)
1106
1114
1107 @LoginRequired()
1115 @LoginRequired()
1108 @NotAnonymous()
1116 @NotAnonymous()
1109 @HasRepoPermissionAnyDecorator(
1117 @HasRepoPermissionAnyDecorator(
1110 'repository.read', 'repository.write', 'repository.admin')
1118 'repository.read', 'repository.write', 'repository.admin')
1111 @CSRFRequired()
1119 @CSRFRequired()
1112 @view_config(
1120 @view_config(
1113 route_name='pullrequest_merge', request_method='POST',
1121 route_name='pullrequest_merge', request_method='POST',
1114 renderer='json_ext')
1122 renderer='json_ext')
1115 def pull_request_merge(self):
1123 def pull_request_merge(self):
1116 """
1124 """
1117 Merge will perform a server-side merge of the specified
1125 Merge will perform a server-side merge of the specified
1118 pull request, if the pull request is approved and mergeable.
1126 pull request, if the pull request is approved and mergeable.
1119 After successful merging, the pull request is automatically
1127 After successful merging, the pull request is automatically
1120 closed, with a relevant comment.
1128 closed, with a relevant comment.
1121 """
1129 """
1122 pull_request = PullRequest.get_or_404(
1130 pull_request = PullRequest.get_or_404(
1123 self.request.matchdict['pull_request_id'])
1131 self.request.matchdict['pull_request_id'])
1124
1132
1125 self.load_default_context()
1133 self.load_default_context()
1126 check = MergeCheck.validate(
1134 check = MergeCheck.validate(
1127 pull_request, auth_user=self._rhodecode_user,
1135 pull_request, auth_user=self._rhodecode_user,
1128 translator=self.request.translate)
1136 translator=self.request.translate)
1129 merge_possible = not check.failed
1137 merge_possible = not check.failed
1130
1138
1131 for err_type, error_msg in check.errors:
1139 for err_type, error_msg in check.errors:
1132 h.flash(error_msg, category=err_type)
1140 h.flash(error_msg, category=err_type)
1133
1141
1134 if merge_possible:
1142 if merge_possible:
1135 log.debug("Pre-conditions checked, trying to merge.")
1143 log.debug("Pre-conditions checked, trying to merge.")
1136 extras = vcs_operation_context(
1144 extras = vcs_operation_context(
1137 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1145 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1138 username=self._rhodecode_db_user.username, action='push',
1146 username=self._rhodecode_db_user.username, action='push',
1139 scm=pull_request.target_repo.repo_type)
1147 scm=pull_request.target_repo.repo_type)
1140 self._merge_pull_request(
1148 self._merge_pull_request(
1141 pull_request, self._rhodecode_db_user, extras)
1149 pull_request, self._rhodecode_db_user, extras)
1142 else:
1150 else:
1143 log.debug("Pre-conditions failed, NOT merging.")
1151 log.debug("Pre-conditions failed, NOT merging.")
1144
1152
1145 raise HTTPFound(
1153 raise HTTPFound(
1146 h.route_path('pullrequest_show',
1154 h.route_path('pullrequest_show',
1147 repo_name=pull_request.target_repo.repo_name,
1155 repo_name=pull_request.target_repo.repo_name,
1148 pull_request_id=pull_request.pull_request_id))
1156 pull_request_id=pull_request.pull_request_id))
1149
1157
1150 def _merge_pull_request(self, pull_request, user, extras):
1158 def _merge_pull_request(self, pull_request, user, extras):
1151 _ = self.request.translate
1159 _ = self.request.translate
1152 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1160 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1153
1161
1154 if merge_resp.executed:
1162 if merge_resp.executed:
1155 log.debug("The merge was successful, closing the pull request.")
1163 log.debug("The merge was successful, closing the pull request.")
1156 PullRequestModel().close_pull_request(
1164 PullRequestModel().close_pull_request(
1157 pull_request.pull_request_id, user)
1165 pull_request.pull_request_id, user)
1158 Session().commit()
1166 Session().commit()
1159 msg = _('Pull request was successfully merged and closed.')
1167 msg = _('Pull request was successfully merged and closed.')
1160 h.flash(msg, category='success')
1168 h.flash(msg, category='success')
1161 else:
1169 else:
1162 log.debug(
1170 log.debug(
1163 "The merge was not successful. Merge response: %s",
1171 "The merge was not successful. Merge response: %s",
1164 merge_resp)
1172 merge_resp)
1165 msg = PullRequestModel().merge_status_message(
1173 msg = PullRequestModel().merge_status_message(
1166 merge_resp.failure_reason)
1174 merge_resp.failure_reason)
1167 h.flash(msg, category='error')
1175 h.flash(msg, category='error')
1168
1176
1169 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1177 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1170 _ = self.request.translate
1178 _ = self.request.translate
1171 get_default_reviewers_data, validate_default_reviewers = \
1179 get_default_reviewers_data, validate_default_reviewers = \
1172 PullRequestModel().get_reviewer_functions()
1180 PullRequestModel().get_reviewer_functions()
1173
1181
1174 try:
1182 try:
1175 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1183 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1176 except ValueError as e:
1184 except ValueError as e:
1177 log.error('Reviewers Validation: {}'.format(e))
1185 log.error('Reviewers Validation: {}'.format(e))
1178 h.flash(e, category='error')
1186 h.flash(e, category='error')
1179 return
1187 return
1180
1188
1181 PullRequestModel().update_reviewers(
1189 PullRequestModel().update_reviewers(
1182 pull_request, reviewers, self._rhodecode_user)
1190 pull_request, reviewers, self._rhodecode_user)
1183 h.flash(_('Pull request reviewers updated.'), category='success')
1191 h.flash(_('Pull request reviewers updated.'), category='success')
1184 Session().commit()
1192 Session().commit()
1185
1193
1186 @LoginRequired()
1194 @LoginRequired()
1187 @NotAnonymous()
1195 @NotAnonymous()
1188 @HasRepoPermissionAnyDecorator(
1196 @HasRepoPermissionAnyDecorator(
1189 'repository.read', 'repository.write', 'repository.admin')
1197 'repository.read', 'repository.write', 'repository.admin')
1190 @CSRFRequired()
1198 @CSRFRequired()
1191 @view_config(
1199 @view_config(
1192 route_name='pullrequest_delete', request_method='POST',
1200 route_name='pullrequest_delete', request_method='POST',
1193 renderer='json_ext')
1201 renderer='json_ext')
1194 def pull_request_delete(self):
1202 def pull_request_delete(self):
1195 _ = self.request.translate
1203 _ = self.request.translate
1196
1204
1197 pull_request = PullRequest.get_or_404(
1205 pull_request = PullRequest.get_or_404(
1198 self.request.matchdict['pull_request_id'])
1206 self.request.matchdict['pull_request_id'])
1199 self.load_default_context()
1207 self.load_default_context()
1200
1208
1201 pr_closed = pull_request.is_closed()
1209 pr_closed = pull_request.is_closed()
1202 allowed_to_delete = PullRequestModel().check_user_delete(
1210 allowed_to_delete = PullRequestModel().check_user_delete(
1203 pull_request, self._rhodecode_user) and not pr_closed
1211 pull_request, self._rhodecode_user) and not pr_closed
1204
1212
1205 # only owner can delete it !
1213 # only owner can delete it !
1206 if allowed_to_delete:
1214 if allowed_to_delete:
1207 PullRequestModel().delete(pull_request, self._rhodecode_user)
1215 PullRequestModel().delete(pull_request, self._rhodecode_user)
1208 Session().commit()
1216 Session().commit()
1209 h.flash(_('Successfully deleted pull request'),
1217 h.flash(_('Successfully deleted pull request'),
1210 category='success')
1218 category='success')
1211 raise HTTPFound(h.route_path('pullrequest_show_all',
1219 raise HTTPFound(h.route_path('pullrequest_show_all',
1212 repo_name=self.db_repo_name))
1220 repo_name=self.db_repo_name))
1213
1221
1214 log.warning('user %s tried to delete pull request without access',
1222 log.warning('user %s tried to delete pull request without access',
1215 self._rhodecode_user)
1223 self._rhodecode_user)
1216 raise HTTPNotFound()
1224 raise HTTPNotFound()
1217
1225
1218 @LoginRequired()
1226 @LoginRequired()
1219 @NotAnonymous()
1227 @NotAnonymous()
1220 @HasRepoPermissionAnyDecorator(
1228 @HasRepoPermissionAnyDecorator(
1221 'repository.read', 'repository.write', 'repository.admin')
1229 'repository.read', 'repository.write', 'repository.admin')
1222 @CSRFRequired()
1230 @CSRFRequired()
1223 @view_config(
1231 @view_config(
1224 route_name='pullrequest_comment_create', request_method='POST',
1232 route_name='pullrequest_comment_create', request_method='POST',
1225 renderer='json_ext')
1233 renderer='json_ext')
1226 def pull_request_comment_create(self):
1234 def pull_request_comment_create(self):
1227 _ = self.request.translate
1235 _ = self.request.translate
1228
1236
1229 pull_request = PullRequest.get_or_404(
1237 pull_request = PullRequest.get_or_404(
1230 self.request.matchdict['pull_request_id'])
1238 self.request.matchdict['pull_request_id'])
1231 pull_request_id = pull_request.pull_request_id
1239 pull_request_id = pull_request.pull_request_id
1232
1240
1233 if pull_request.is_closed():
1241 if pull_request.is_closed():
1234 log.debug('comment: forbidden because pull request is closed')
1242 log.debug('comment: forbidden because pull request is closed')
1235 raise HTTPForbidden()
1243 raise HTTPForbidden()
1236
1244
1237 allowed_to_comment = PullRequestModel().check_user_comment(
1245 allowed_to_comment = PullRequestModel().check_user_comment(
1238 pull_request, self._rhodecode_user)
1246 pull_request, self._rhodecode_user)
1239 if not allowed_to_comment:
1247 if not allowed_to_comment:
1240 log.debug(
1248 log.debug(
1241 'comment: forbidden because pull request is from forbidden repo')
1249 'comment: forbidden because pull request is from forbidden repo')
1242 raise HTTPForbidden()
1250 raise HTTPForbidden()
1243
1251
1244 c = self.load_default_context()
1252 c = self.load_default_context()
1245
1253
1246 status = self.request.POST.get('changeset_status', None)
1254 status = self.request.POST.get('changeset_status', None)
1247 text = self.request.POST.get('text')
1255 text = self.request.POST.get('text')
1248 comment_type = self.request.POST.get('comment_type')
1256 comment_type = self.request.POST.get('comment_type')
1249 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1257 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1250 close_pull_request = self.request.POST.get('close_pull_request')
1258 close_pull_request = self.request.POST.get('close_pull_request')
1251
1259
1252 # the logic here should work like following, if we submit close
1260 # the logic here should work like following, if we submit close
1253 # pr comment, use `close_pull_request_with_comment` function
1261 # pr comment, use `close_pull_request_with_comment` function
1254 # else handle regular comment logic
1262 # else handle regular comment logic
1255
1263
1256 if close_pull_request:
1264 if close_pull_request:
1257 # only owner or admin or person with write permissions
1265 # only owner or admin or person with write permissions
1258 allowed_to_close = PullRequestModel().check_user_update(
1266 allowed_to_close = PullRequestModel().check_user_update(
1259 pull_request, self._rhodecode_user)
1267 pull_request, self._rhodecode_user)
1260 if not allowed_to_close:
1268 if not allowed_to_close:
1261 log.debug('comment: forbidden because not allowed to close '
1269 log.debug('comment: forbidden because not allowed to close '
1262 'pull request %s', pull_request_id)
1270 'pull request %s', pull_request_id)
1263 raise HTTPForbidden()
1271 raise HTTPForbidden()
1264 comment, status = PullRequestModel().close_pull_request_with_comment(
1272 comment, status = PullRequestModel().close_pull_request_with_comment(
1265 pull_request, self._rhodecode_user, self.db_repo, message=text,
1273 pull_request, self._rhodecode_user, self.db_repo, message=text,
1266 auth_user=self._rhodecode_user)
1274 auth_user=self._rhodecode_user)
1267 Session().flush()
1275 Session().flush()
1268 events.trigger(
1276 events.trigger(
1269 events.PullRequestCommentEvent(pull_request, comment))
1277 events.PullRequestCommentEvent(pull_request, comment))
1270
1278
1271 else:
1279 else:
1272 # regular comment case, could be inline, or one with status.
1280 # regular comment case, could be inline, or one with status.
1273 # for that one we check also permissions
1281 # for that one we check also permissions
1274
1282
1275 allowed_to_change_status = PullRequestModel().check_user_change_status(
1283 allowed_to_change_status = PullRequestModel().check_user_change_status(
1276 pull_request, self._rhodecode_user)
1284 pull_request, self._rhodecode_user)
1277
1285
1278 if status and allowed_to_change_status:
1286 if status and allowed_to_change_status:
1279 message = (_('Status change %(transition_icon)s %(status)s')
1287 message = (_('Status change %(transition_icon)s %(status)s')
1280 % {'transition_icon': '>',
1288 % {'transition_icon': '>',
1281 'status': ChangesetStatus.get_status_lbl(status)})
1289 'status': ChangesetStatus.get_status_lbl(status)})
1282 text = text or message
1290 text = text or message
1283
1291
1284 comment = CommentsModel().create(
1292 comment = CommentsModel().create(
1285 text=text,
1293 text=text,
1286 repo=self.db_repo.repo_id,
1294 repo=self.db_repo.repo_id,
1287 user=self._rhodecode_user.user_id,
1295 user=self._rhodecode_user.user_id,
1288 pull_request=pull_request,
1296 pull_request=pull_request,
1289 f_path=self.request.POST.get('f_path'),
1297 f_path=self.request.POST.get('f_path'),
1290 line_no=self.request.POST.get('line'),
1298 line_no=self.request.POST.get('line'),
1291 status_change=(ChangesetStatus.get_status_lbl(status)
1299 status_change=(ChangesetStatus.get_status_lbl(status)
1292 if status and allowed_to_change_status else None),
1300 if status and allowed_to_change_status else None),
1293 status_change_type=(status
1301 status_change_type=(status
1294 if status and allowed_to_change_status else None),
1302 if status and allowed_to_change_status else None),
1295 comment_type=comment_type,
1303 comment_type=comment_type,
1296 resolves_comment_id=resolves_comment_id,
1304 resolves_comment_id=resolves_comment_id,
1297 auth_user=self._rhodecode_user
1305 auth_user=self._rhodecode_user
1298 )
1306 )
1299
1307
1300 if allowed_to_change_status:
1308 if allowed_to_change_status:
1301 # calculate old status before we change it
1309 # calculate old status before we change it
1302 old_calculated_status = pull_request.calculated_review_status()
1310 old_calculated_status = pull_request.calculated_review_status()
1303
1311
1304 # get status if set !
1312 # get status if set !
1305 if status:
1313 if status:
1306 ChangesetStatusModel().set_status(
1314 ChangesetStatusModel().set_status(
1307 self.db_repo.repo_id,
1315 self.db_repo.repo_id,
1308 status,
1316 status,
1309 self._rhodecode_user.user_id,
1317 self._rhodecode_user.user_id,
1310 comment,
1318 comment,
1311 pull_request=pull_request
1319 pull_request=pull_request
1312 )
1320 )
1313
1321
1314 Session().flush()
1322 Session().flush()
1315 # this is somehow required to get access to some relationship
1323 # this is somehow required to get access to some relationship
1316 # loaded on comment
1324 # loaded on comment
1317 Session().refresh(comment)
1325 Session().refresh(comment)
1318
1326
1319 events.trigger(
1327 events.trigger(
1320 events.PullRequestCommentEvent(pull_request, comment))
1328 events.PullRequestCommentEvent(pull_request, comment))
1321
1329
1322 # we now calculate the status of pull request, and based on that
1330 # we now calculate the status of pull request, and based on that
1323 # calculation we set the commits status
1331 # calculation we set the commits status
1324 calculated_status = pull_request.calculated_review_status()
1332 calculated_status = pull_request.calculated_review_status()
1325 if old_calculated_status != calculated_status:
1333 if old_calculated_status != calculated_status:
1326 PullRequestModel()._trigger_pull_request_hook(
1334 PullRequestModel()._trigger_pull_request_hook(
1327 pull_request, self._rhodecode_user, 'review_status_change')
1335 pull_request, self._rhodecode_user, 'review_status_change')
1328
1336
1329 Session().commit()
1337 Session().commit()
1330
1338
1331 data = {
1339 data = {
1332 'target_id': h.safeid(h.safe_unicode(
1340 'target_id': h.safeid(h.safe_unicode(
1333 self.request.POST.get('f_path'))),
1341 self.request.POST.get('f_path'))),
1334 }
1342 }
1335 if comment:
1343 if comment:
1336 c.co = comment
1344 c.co = comment
1337 rendered_comment = render(
1345 rendered_comment = render(
1338 'rhodecode:templates/changeset/changeset_comment_block.mako',
1346 'rhodecode:templates/changeset/changeset_comment_block.mako',
1339 self._get_template_context(c), self.request)
1347 self._get_template_context(c), self.request)
1340
1348
1341 data.update(comment.get_dict())
1349 data.update(comment.get_dict())
1342 data.update({'rendered_text': rendered_comment})
1350 data.update({'rendered_text': rendered_comment})
1343
1351
1344 return data
1352 return data
1345
1353
1346 @LoginRequired()
1354 @LoginRequired()
1347 @NotAnonymous()
1355 @NotAnonymous()
1348 @HasRepoPermissionAnyDecorator(
1356 @HasRepoPermissionAnyDecorator(
1349 'repository.read', 'repository.write', 'repository.admin')
1357 'repository.read', 'repository.write', 'repository.admin')
1350 @CSRFRequired()
1358 @CSRFRequired()
1351 @view_config(
1359 @view_config(
1352 route_name='pullrequest_comment_delete', request_method='POST',
1360 route_name='pullrequest_comment_delete', request_method='POST',
1353 renderer='json_ext')
1361 renderer='json_ext')
1354 def pull_request_comment_delete(self):
1362 def pull_request_comment_delete(self):
1355 pull_request = PullRequest.get_or_404(
1363 pull_request = PullRequest.get_or_404(
1356 self.request.matchdict['pull_request_id'])
1364 self.request.matchdict['pull_request_id'])
1357
1365
1358 comment = ChangesetComment.get_or_404(
1366 comment = ChangesetComment.get_or_404(
1359 self.request.matchdict['comment_id'])
1367 self.request.matchdict['comment_id'])
1360 comment_id = comment.comment_id
1368 comment_id = comment.comment_id
1361
1369
1362 if pull_request.is_closed():
1370 if pull_request.is_closed():
1363 log.debug('comment: forbidden because pull request is closed')
1371 log.debug('comment: forbidden because pull request is closed')
1364 raise HTTPForbidden()
1372 raise HTTPForbidden()
1365
1373
1366 if not comment:
1374 if not comment:
1367 log.debug('Comment with id:%s not found, skipping', comment_id)
1375 log.debug('Comment with id:%s not found, skipping', comment_id)
1368 # comment already deleted in another call probably
1376 # comment already deleted in another call probably
1369 return True
1377 return True
1370
1378
1371 if comment.pull_request.is_closed():
1379 if comment.pull_request.is_closed():
1372 # don't allow deleting comments on closed pull request
1380 # don't allow deleting comments on closed pull request
1373 raise HTTPForbidden()
1381 raise HTTPForbidden()
1374
1382
1375 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1383 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1376 super_admin = h.HasPermissionAny('hg.admin')()
1384 super_admin = h.HasPermissionAny('hg.admin')()
1377 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1385 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1378 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1386 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1379 comment_repo_admin = is_repo_admin and is_repo_comment
1387 comment_repo_admin = is_repo_admin and is_repo_comment
1380
1388
1381 if super_admin or comment_owner or comment_repo_admin:
1389 if super_admin or comment_owner or comment_repo_admin:
1382 old_calculated_status = comment.pull_request.calculated_review_status()
1390 old_calculated_status = comment.pull_request.calculated_review_status()
1383 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1391 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1384 Session().commit()
1392 Session().commit()
1385 calculated_status = comment.pull_request.calculated_review_status()
1393 calculated_status = comment.pull_request.calculated_review_status()
1386 if old_calculated_status != calculated_status:
1394 if old_calculated_status != calculated_status:
1387 PullRequestModel()._trigger_pull_request_hook(
1395 PullRequestModel()._trigger_pull_request_hook(
1388 comment.pull_request, self._rhodecode_user, 'review_status_change')
1396 comment.pull_request, self._rhodecode_user, 'review_status_change')
1389 return True
1397 return True
1390 else:
1398 else:
1391 log.warning('No permissions for user %s to delete comment_id: %s',
1399 log.warning('No permissions for user %s to delete comment_id: %s',
1392 self._rhodecode_db_user, comment_id)
1400 self._rhodecode_db_user, comment_id)
1393 raise HTTPNotFound()
1401 raise HTTPNotFound()
@@ -1,1228 +1,1237 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Set of diffing helpers, previously part of vcs
23 Set of diffing helpers, previously part of vcs
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import bz2
28 import bz2
29
29
30 import collections
30 import collections
31 import difflib
31 import difflib
32 import logging
32 import logging
33 import cPickle as pickle
33 import cPickle as pickle
34 from itertools import tee, imap
34 from itertools import tee, imap
35
35
36 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.exceptions import VCSError
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42 # define max context, a file with more than this numbers of lines is unusable
42 # define max context, a file with more than this numbers of lines is unusable
43 # in browser anyway
43 # in browser anyway
44 MAX_CONTEXT = 1024 * 1014
44 MAX_CONTEXT = 20 * 1024
45 DEFAULT_CONTEXT = 3
46
47
48 def get_diff_context(request):
49 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
50
51
52 def get_diff_whitespace_flag(request):
53 return request.GET.get('ignorews', '') == '1'
45
54
46
55
47 class OPS(object):
56 class OPS(object):
48 ADD = 'A'
57 ADD = 'A'
49 MOD = 'M'
58 MOD = 'M'
50 DEL = 'D'
59 DEL = 'D'
51
60
52
61
53 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
62 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
54 """
63 """
55 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
64 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
56
65
57 :param ignore_whitespace: ignore whitespaces in diff
66 :param ignore_whitespace: ignore whitespaces in diff
58 """
67 """
59 # make sure we pass in default context
68 # make sure we pass in default context
60 context = context or 3
69 context = context or 3
61 # protect against IntOverflow when passing HUGE context
70 # protect against IntOverflow when passing HUGE context
62 if context > MAX_CONTEXT:
71 if context > MAX_CONTEXT:
63 context = MAX_CONTEXT
72 context = MAX_CONTEXT
64
73
65 submodules = filter(lambda o: isinstance(o, SubModuleNode),
74 submodules = filter(lambda o: isinstance(o, SubModuleNode),
66 [filenode_new, filenode_old])
75 [filenode_new, filenode_old])
67 if submodules:
76 if submodules:
68 return ''
77 return ''
69
78
70 for filenode in (filenode_old, filenode_new):
79 for filenode in (filenode_old, filenode_new):
71 if not isinstance(filenode, FileNode):
80 if not isinstance(filenode, FileNode):
72 raise VCSError(
81 raise VCSError(
73 "Given object should be FileNode object, not %s"
82 "Given object should be FileNode object, not %s"
74 % filenode.__class__)
83 % filenode.__class__)
75
84
76 repo = filenode_new.commit.repository
85 repo = filenode_new.commit.repository
77 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
86 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
78 new_commit = filenode_new.commit
87 new_commit = filenode_new.commit
79
88
80 vcs_gitdiff = repo.get_diff(
89 vcs_gitdiff = repo.get_diff(
81 old_commit, new_commit, filenode_new.path,
90 old_commit, new_commit, filenode_new.path,
82 ignore_whitespace, context, path1=filenode_old.path)
91 ignore_whitespace, context, path1=filenode_old.path)
83 return vcs_gitdiff
92 return vcs_gitdiff
84
93
85 NEW_FILENODE = 1
94 NEW_FILENODE = 1
86 DEL_FILENODE = 2
95 DEL_FILENODE = 2
87 MOD_FILENODE = 3
96 MOD_FILENODE = 3
88 RENAMED_FILENODE = 4
97 RENAMED_FILENODE = 4
89 COPIED_FILENODE = 5
98 COPIED_FILENODE = 5
90 CHMOD_FILENODE = 6
99 CHMOD_FILENODE = 6
91 BIN_FILENODE = 7
100 BIN_FILENODE = 7
92
101
93
102
94 class LimitedDiffContainer(object):
103 class LimitedDiffContainer(object):
95
104
96 def __init__(self, diff_limit, cur_diff_size, diff):
105 def __init__(self, diff_limit, cur_diff_size, diff):
97 self.diff = diff
106 self.diff = diff
98 self.diff_limit = diff_limit
107 self.diff_limit = diff_limit
99 self.cur_diff_size = cur_diff_size
108 self.cur_diff_size = cur_diff_size
100
109
101 def __getitem__(self, key):
110 def __getitem__(self, key):
102 return self.diff.__getitem__(key)
111 return self.diff.__getitem__(key)
103
112
104 def __iter__(self):
113 def __iter__(self):
105 for l in self.diff:
114 for l in self.diff:
106 yield l
115 yield l
107
116
108
117
109 class Action(object):
118 class Action(object):
110 """
119 """
111 Contains constants for the action value of the lines in a parsed diff.
120 Contains constants for the action value of the lines in a parsed diff.
112 """
121 """
113
122
114 ADD = 'add'
123 ADD = 'add'
115 DELETE = 'del'
124 DELETE = 'del'
116 UNMODIFIED = 'unmod'
125 UNMODIFIED = 'unmod'
117
126
118 CONTEXT = 'context'
127 CONTEXT = 'context'
119 OLD_NO_NL = 'old-no-nl'
128 OLD_NO_NL = 'old-no-nl'
120 NEW_NO_NL = 'new-no-nl'
129 NEW_NO_NL = 'new-no-nl'
121
130
122
131
123 class DiffProcessor(object):
132 class DiffProcessor(object):
124 """
133 """
125 Give it a unified or git diff and it returns a list of the files that were
134 Give it a unified or git diff and it returns a list of the files that were
126 mentioned in the diff together with a dict of meta information that
135 mentioned in the diff together with a dict of meta information that
127 can be used to render it in a HTML template.
136 can be used to render it in a HTML template.
128
137
129 .. note:: Unicode handling
138 .. note:: Unicode handling
130
139
131 The original diffs are a byte sequence and can contain filenames
140 The original diffs are a byte sequence and can contain filenames
132 in mixed encodings. This class generally returns `unicode` objects
141 in mixed encodings. This class generally returns `unicode` objects
133 since the result is intended for presentation to the user.
142 since the result is intended for presentation to the user.
134
143
135 """
144 """
136 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
145 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
137 _newline_marker = re.compile(r'^\\ No newline at end of file')
146 _newline_marker = re.compile(r'^\\ No newline at end of file')
138
147
139 # used for inline highlighter word split
148 # used for inline highlighter word split
140 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
149 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
141
150
142 # collapse ranges of commits over given number
151 # collapse ranges of commits over given number
143 _collapse_commits_over = 5
152 _collapse_commits_over = 5
144
153
145 def __init__(self, diff, format='gitdiff', diff_limit=None,
154 def __init__(self, diff, format='gitdiff', diff_limit=None,
146 file_limit=None, show_full_diff=True):
155 file_limit=None, show_full_diff=True):
147 """
156 """
148 :param diff: A `Diff` object representing a diff from a vcs backend
157 :param diff: A `Diff` object representing a diff from a vcs backend
149 :param format: format of diff passed, `udiff` or `gitdiff`
158 :param format: format of diff passed, `udiff` or `gitdiff`
150 :param diff_limit: define the size of diff that is considered "big"
159 :param diff_limit: define the size of diff that is considered "big"
151 based on that parameter cut off will be triggered, set to None
160 based on that parameter cut off will be triggered, set to None
152 to show full diff
161 to show full diff
153 """
162 """
154 self._diff = diff
163 self._diff = diff
155 self._format = format
164 self._format = format
156 self.adds = 0
165 self.adds = 0
157 self.removes = 0
166 self.removes = 0
158 # calculate diff size
167 # calculate diff size
159 self.diff_limit = diff_limit
168 self.diff_limit = diff_limit
160 self.file_limit = file_limit
169 self.file_limit = file_limit
161 self.show_full_diff = show_full_diff
170 self.show_full_diff = show_full_diff
162 self.cur_diff_size = 0
171 self.cur_diff_size = 0
163 self.parsed = False
172 self.parsed = False
164 self.parsed_diff = []
173 self.parsed_diff = []
165
174
166 log.debug('Initialized DiffProcessor with %s mode', format)
175 log.debug('Initialized DiffProcessor with %s mode', format)
167 if format == 'gitdiff':
176 if format == 'gitdiff':
168 self.differ = self._highlight_line_difflib
177 self.differ = self._highlight_line_difflib
169 self._parser = self._parse_gitdiff
178 self._parser = self._parse_gitdiff
170 else:
179 else:
171 self.differ = self._highlight_line_udiff
180 self.differ = self._highlight_line_udiff
172 self._parser = self._new_parse_gitdiff
181 self._parser = self._new_parse_gitdiff
173
182
174 def _copy_iterator(self):
183 def _copy_iterator(self):
175 """
184 """
176 make a fresh copy of generator, we should not iterate thru
185 make a fresh copy of generator, we should not iterate thru
177 an original as it's needed for repeating operations on
186 an original as it's needed for repeating operations on
178 this instance of DiffProcessor
187 this instance of DiffProcessor
179 """
188 """
180 self.__udiff, iterator_copy = tee(self.__udiff)
189 self.__udiff, iterator_copy = tee(self.__udiff)
181 return iterator_copy
190 return iterator_copy
182
191
183 def _escaper(self, string):
192 def _escaper(self, string):
184 """
193 """
185 Escaper for diff escapes special chars and checks the diff limit
194 Escaper for diff escapes special chars and checks the diff limit
186
195
187 :param string:
196 :param string:
188 """
197 """
189 self.cur_diff_size += len(string)
198 self.cur_diff_size += len(string)
190
199
191 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
200 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
192 raise DiffLimitExceeded('Diff Limit Exceeded')
201 raise DiffLimitExceeded('Diff Limit Exceeded')
193
202
194 return string \
203 return string \
195 .replace('&', '&amp;')\
204 .replace('&', '&amp;')\
196 .replace('<', '&lt;')\
205 .replace('<', '&lt;')\
197 .replace('>', '&gt;')
206 .replace('>', '&gt;')
198
207
199 def _line_counter(self, l):
208 def _line_counter(self, l):
200 """
209 """
201 Checks each line and bumps total adds/removes for this diff
210 Checks each line and bumps total adds/removes for this diff
202
211
203 :param l:
212 :param l:
204 """
213 """
205 if l.startswith('+') and not l.startswith('+++'):
214 if l.startswith('+') and not l.startswith('+++'):
206 self.adds += 1
215 self.adds += 1
207 elif l.startswith('-') and not l.startswith('---'):
216 elif l.startswith('-') and not l.startswith('---'):
208 self.removes += 1
217 self.removes += 1
209 return safe_unicode(l)
218 return safe_unicode(l)
210
219
211 def _highlight_line_difflib(self, line, next_):
220 def _highlight_line_difflib(self, line, next_):
212 """
221 """
213 Highlight inline changes in both lines.
222 Highlight inline changes in both lines.
214 """
223 """
215
224
216 if line['action'] == Action.DELETE:
225 if line['action'] == Action.DELETE:
217 old, new = line, next_
226 old, new = line, next_
218 else:
227 else:
219 old, new = next_, line
228 old, new = next_, line
220
229
221 oldwords = self._token_re.split(old['line'])
230 oldwords = self._token_re.split(old['line'])
222 newwords = self._token_re.split(new['line'])
231 newwords = self._token_re.split(new['line'])
223 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
232 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
224
233
225 oldfragments, newfragments = [], []
234 oldfragments, newfragments = [], []
226 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
235 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
227 oldfrag = ''.join(oldwords[i1:i2])
236 oldfrag = ''.join(oldwords[i1:i2])
228 newfrag = ''.join(newwords[j1:j2])
237 newfrag = ''.join(newwords[j1:j2])
229 if tag != 'equal':
238 if tag != 'equal':
230 if oldfrag:
239 if oldfrag:
231 oldfrag = '<del>%s</del>' % oldfrag
240 oldfrag = '<del>%s</del>' % oldfrag
232 if newfrag:
241 if newfrag:
233 newfrag = '<ins>%s</ins>' % newfrag
242 newfrag = '<ins>%s</ins>' % newfrag
234 oldfragments.append(oldfrag)
243 oldfragments.append(oldfrag)
235 newfragments.append(newfrag)
244 newfragments.append(newfrag)
236
245
237 old['line'] = "".join(oldfragments)
246 old['line'] = "".join(oldfragments)
238 new['line'] = "".join(newfragments)
247 new['line'] = "".join(newfragments)
239
248
240 def _highlight_line_udiff(self, line, next_):
249 def _highlight_line_udiff(self, line, next_):
241 """
250 """
242 Highlight inline changes in both lines.
251 Highlight inline changes in both lines.
243 """
252 """
244 start = 0
253 start = 0
245 limit = min(len(line['line']), len(next_['line']))
254 limit = min(len(line['line']), len(next_['line']))
246 while start < limit and line['line'][start] == next_['line'][start]:
255 while start < limit and line['line'][start] == next_['line'][start]:
247 start += 1
256 start += 1
248 end = -1
257 end = -1
249 limit -= start
258 limit -= start
250 while -end <= limit and line['line'][end] == next_['line'][end]:
259 while -end <= limit and line['line'][end] == next_['line'][end]:
251 end -= 1
260 end -= 1
252 end += 1
261 end += 1
253 if start or end:
262 if start or end:
254 def do(l):
263 def do(l):
255 last = end + len(l['line'])
264 last = end + len(l['line'])
256 if l['action'] == Action.ADD:
265 if l['action'] == Action.ADD:
257 tag = 'ins'
266 tag = 'ins'
258 else:
267 else:
259 tag = 'del'
268 tag = 'del'
260 l['line'] = '%s<%s>%s</%s>%s' % (
269 l['line'] = '%s<%s>%s</%s>%s' % (
261 l['line'][:start],
270 l['line'][:start],
262 tag,
271 tag,
263 l['line'][start:last],
272 l['line'][start:last],
264 tag,
273 tag,
265 l['line'][last:]
274 l['line'][last:]
266 )
275 )
267 do(line)
276 do(line)
268 do(next_)
277 do(next_)
269
278
270 def _clean_line(self, line, command):
279 def _clean_line(self, line, command):
271 if command in ['+', '-', ' ']:
280 if command in ['+', '-', ' ']:
272 # only modify the line if it's actually a diff thing
281 # only modify the line if it's actually a diff thing
273 line = line[1:]
282 line = line[1:]
274 return line
283 return line
275
284
276 def _parse_gitdiff(self, inline_diff=True):
285 def _parse_gitdiff(self, inline_diff=True):
277 _files = []
286 _files = []
278 diff_container = lambda arg: arg
287 diff_container = lambda arg: arg
279
288
280 for chunk in self._diff.chunks():
289 for chunk in self._diff.chunks():
281 head = chunk.header
290 head = chunk.header
282
291
283 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
292 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
284 raw_diff = chunk.raw
293 raw_diff = chunk.raw
285 limited_diff = False
294 limited_diff = False
286 exceeds_limit = False
295 exceeds_limit = False
287
296
288 op = None
297 op = None
289 stats = {
298 stats = {
290 'added': 0,
299 'added': 0,
291 'deleted': 0,
300 'deleted': 0,
292 'binary': False,
301 'binary': False,
293 'ops': {},
302 'ops': {},
294 }
303 }
295
304
296 if head['deleted_file_mode']:
305 if head['deleted_file_mode']:
297 op = OPS.DEL
306 op = OPS.DEL
298 stats['binary'] = True
307 stats['binary'] = True
299 stats['ops'][DEL_FILENODE] = 'deleted file'
308 stats['ops'][DEL_FILENODE] = 'deleted file'
300
309
301 elif head['new_file_mode']:
310 elif head['new_file_mode']:
302 op = OPS.ADD
311 op = OPS.ADD
303 stats['binary'] = True
312 stats['binary'] = True
304 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
313 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
305 else: # modify operation, can be copy, rename or chmod
314 else: # modify operation, can be copy, rename or chmod
306
315
307 # CHMOD
316 # CHMOD
308 if head['new_mode'] and head['old_mode']:
317 if head['new_mode'] and head['old_mode']:
309 op = OPS.MOD
318 op = OPS.MOD
310 stats['binary'] = True
319 stats['binary'] = True
311 stats['ops'][CHMOD_FILENODE] = (
320 stats['ops'][CHMOD_FILENODE] = (
312 'modified file chmod %s => %s' % (
321 'modified file chmod %s => %s' % (
313 head['old_mode'], head['new_mode']))
322 head['old_mode'], head['new_mode']))
314 # RENAME
323 # RENAME
315 if head['rename_from'] != head['rename_to']:
324 if head['rename_from'] != head['rename_to']:
316 op = OPS.MOD
325 op = OPS.MOD
317 stats['binary'] = True
326 stats['binary'] = True
318 stats['ops'][RENAMED_FILENODE] = (
327 stats['ops'][RENAMED_FILENODE] = (
319 'file renamed from %s to %s' % (
328 'file renamed from %s to %s' % (
320 head['rename_from'], head['rename_to']))
329 head['rename_from'], head['rename_to']))
321 # COPY
330 # COPY
322 if head.get('copy_from') and head.get('copy_to'):
331 if head.get('copy_from') and head.get('copy_to'):
323 op = OPS.MOD
332 op = OPS.MOD
324 stats['binary'] = True
333 stats['binary'] = True
325 stats['ops'][COPIED_FILENODE] = (
334 stats['ops'][COPIED_FILENODE] = (
326 'file copied from %s to %s' % (
335 'file copied from %s to %s' % (
327 head['copy_from'], head['copy_to']))
336 head['copy_from'], head['copy_to']))
328
337
329 # If our new parsed headers didn't match anything fallback to
338 # If our new parsed headers didn't match anything fallback to
330 # old style detection
339 # old style detection
331 if op is None:
340 if op is None:
332 if not head['a_file'] and head['b_file']:
341 if not head['a_file'] and head['b_file']:
333 op = OPS.ADD
342 op = OPS.ADD
334 stats['binary'] = True
343 stats['binary'] = True
335 stats['ops'][NEW_FILENODE] = 'new file'
344 stats['ops'][NEW_FILENODE] = 'new file'
336
345
337 elif head['a_file'] and not head['b_file']:
346 elif head['a_file'] and not head['b_file']:
338 op = OPS.DEL
347 op = OPS.DEL
339 stats['binary'] = True
348 stats['binary'] = True
340 stats['ops'][DEL_FILENODE] = 'deleted file'
349 stats['ops'][DEL_FILENODE] = 'deleted file'
341
350
342 # it's not ADD not DELETE
351 # it's not ADD not DELETE
343 if op is None:
352 if op is None:
344 op = OPS.MOD
353 op = OPS.MOD
345 stats['binary'] = True
354 stats['binary'] = True
346 stats['ops'][MOD_FILENODE] = 'modified file'
355 stats['ops'][MOD_FILENODE] = 'modified file'
347
356
348 # a real non-binary diff
357 # a real non-binary diff
349 if head['a_file'] or head['b_file']:
358 if head['a_file'] or head['b_file']:
350 try:
359 try:
351 raw_diff, chunks, _stats = self._parse_lines(diff)
360 raw_diff, chunks, _stats = self._parse_lines(diff)
352 stats['binary'] = False
361 stats['binary'] = False
353 stats['added'] = _stats[0]
362 stats['added'] = _stats[0]
354 stats['deleted'] = _stats[1]
363 stats['deleted'] = _stats[1]
355 # explicit mark that it's a modified file
364 # explicit mark that it's a modified file
356 if op == OPS.MOD:
365 if op == OPS.MOD:
357 stats['ops'][MOD_FILENODE] = 'modified file'
366 stats['ops'][MOD_FILENODE] = 'modified file'
358 exceeds_limit = len(raw_diff) > self.file_limit
367 exceeds_limit = len(raw_diff) > self.file_limit
359
368
360 # changed from _escaper function so we validate size of
369 # changed from _escaper function so we validate size of
361 # each file instead of the whole diff
370 # each file instead of the whole diff
362 # diff will hide big files but still show small ones
371 # diff will hide big files but still show small ones
363 # from my tests, big files are fairly safe to be parsed
372 # from my tests, big files are fairly safe to be parsed
364 # but the browser is the bottleneck
373 # but the browser is the bottleneck
365 if not self.show_full_diff and exceeds_limit:
374 if not self.show_full_diff and exceeds_limit:
366 raise DiffLimitExceeded('File Limit Exceeded')
375 raise DiffLimitExceeded('File Limit Exceeded')
367
376
368 except DiffLimitExceeded:
377 except DiffLimitExceeded:
369 diff_container = lambda _diff: \
378 diff_container = lambda _diff: \
370 LimitedDiffContainer(
379 LimitedDiffContainer(
371 self.diff_limit, self.cur_diff_size, _diff)
380 self.diff_limit, self.cur_diff_size, _diff)
372
381
373 exceeds_limit = len(raw_diff) > self.file_limit
382 exceeds_limit = len(raw_diff) > self.file_limit
374 limited_diff = True
383 limited_diff = True
375 chunks = []
384 chunks = []
376
385
377 else: # GIT format binary patch, or possibly empty diff
386 else: # GIT format binary patch, or possibly empty diff
378 if head['bin_patch']:
387 if head['bin_patch']:
379 # we have operation already extracted, but we mark simply
388 # we have operation already extracted, but we mark simply
380 # it's a diff we wont show for binary files
389 # it's a diff we wont show for binary files
381 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
390 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
382 chunks = []
391 chunks = []
383
392
384 if chunks and not self.show_full_diff and op == OPS.DEL:
393 if chunks and not self.show_full_diff and op == OPS.DEL:
385 # if not full diff mode show deleted file contents
394 # if not full diff mode show deleted file contents
386 # TODO: anderson: if the view is not too big, there is no way
395 # TODO: anderson: if the view is not too big, there is no way
387 # to see the content of the file
396 # to see the content of the file
388 chunks = []
397 chunks = []
389
398
390 chunks.insert(0, [{
399 chunks.insert(0, [{
391 'old_lineno': '',
400 'old_lineno': '',
392 'new_lineno': '',
401 'new_lineno': '',
393 'action': Action.CONTEXT,
402 'action': Action.CONTEXT,
394 'line': msg,
403 'line': msg,
395 } for _op, msg in stats['ops'].iteritems()
404 } for _op, msg in stats['ops'].iteritems()
396 if _op not in [MOD_FILENODE]])
405 if _op not in [MOD_FILENODE]])
397
406
398 _files.append({
407 _files.append({
399 'filename': safe_unicode(head['b_path']),
408 'filename': safe_unicode(head['b_path']),
400 'old_revision': head['a_blob_id'],
409 'old_revision': head['a_blob_id'],
401 'new_revision': head['b_blob_id'],
410 'new_revision': head['b_blob_id'],
402 'chunks': chunks,
411 'chunks': chunks,
403 'raw_diff': safe_unicode(raw_diff),
412 'raw_diff': safe_unicode(raw_diff),
404 'operation': op,
413 'operation': op,
405 'stats': stats,
414 'stats': stats,
406 'exceeds_limit': exceeds_limit,
415 'exceeds_limit': exceeds_limit,
407 'is_limited_diff': limited_diff,
416 'is_limited_diff': limited_diff,
408 })
417 })
409
418
410 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
419 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
411 OPS.DEL: 2}.get(info['operation'])
420 OPS.DEL: 2}.get(info['operation'])
412
421
413 if not inline_diff:
422 if not inline_diff:
414 return diff_container(sorted(_files, key=sorter))
423 return diff_container(sorted(_files, key=sorter))
415
424
416 # highlight inline changes
425 # highlight inline changes
417 for diff_data in _files:
426 for diff_data in _files:
418 for chunk in diff_data['chunks']:
427 for chunk in diff_data['chunks']:
419 lineiter = iter(chunk)
428 lineiter = iter(chunk)
420 try:
429 try:
421 while 1:
430 while 1:
422 line = lineiter.next()
431 line = lineiter.next()
423 if line['action'] not in (
432 if line['action'] not in (
424 Action.UNMODIFIED, Action.CONTEXT):
433 Action.UNMODIFIED, Action.CONTEXT):
425 nextline = lineiter.next()
434 nextline = lineiter.next()
426 if nextline['action'] in ['unmod', 'context'] or \
435 if nextline['action'] in ['unmod', 'context'] or \
427 nextline['action'] == line['action']:
436 nextline['action'] == line['action']:
428 continue
437 continue
429 self.differ(line, nextline)
438 self.differ(line, nextline)
430 except StopIteration:
439 except StopIteration:
431 pass
440 pass
432
441
433 return diff_container(sorted(_files, key=sorter))
442 return diff_container(sorted(_files, key=sorter))
434
443
435 def _check_large_diff(self):
444 def _check_large_diff(self):
436 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
445 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
437 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
446 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
438 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
447 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
439
448
440 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
449 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
441 def _new_parse_gitdiff(self, inline_diff=True):
450 def _new_parse_gitdiff(self, inline_diff=True):
442 _files = []
451 _files = []
443
452
444 # this can be overriden later to a LimitedDiffContainer type
453 # this can be overriden later to a LimitedDiffContainer type
445 diff_container = lambda arg: arg
454 diff_container = lambda arg: arg
446
455
447 for chunk in self._diff.chunks():
456 for chunk in self._diff.chunks():
448 head = chunk.header
457 head = chunk.header
449 log.debug('parsing diff %r', head)
458 log.debug('parsing diff %r', head)
450
459
451 raw_diff = chunk.raw
460 raw_diff = chunk.raw
452 limited_diff = False
461 limited_diff = False
453 exceeds_limit = False
462 exceeds_limit = False
454
463
455 op = None
464 op = None
456 stats = {
465 stats = {
457 'added': 0,
466 'added': 0,
458 'deleted': 0,
467 'deleted': 0,
459 'binary': False,
468 'binary': False,
460 'old_mode': None,
469 'old_mode': None,
461 'new_mode': None,
470 'new_mode': None,
462 'ops': {},
471 'ops': {},
463 }
472 }
464 if head['old_mode']:
473 if head['old_mode']:
465 stats['old_mode'] = head['old_mode']
474 stats['old_mode'] = head['old_mode']
466 if head['new_mode']:
475 if head['new_mode']:
467 stats['new_mode'] = head['new_mode']
476 stats['new_mode'] = head['new_mode']
468 if head['b_mode']:
477 if head['b_mode']:
469 stats['new_mode'] = head['b_mode']
478 stats['new_mode'] = head['b_mode']
470
479
471 # delete file
480 # delete file
472 if head['deleted_file_mode']:
481 if head['deleted_file_mode']:
473 op = OPS.DEL
482 op = OPS.DEL
474 stats['binary'] = True
483 stats['binary'] = True
475 stats['ops'][DEL_FILENODE] = 'deleted file'
484 stats['ops'][DEL_FILENODE] = 'deleted file'
476
485
477 # new file
486 # new file
478 elif head['new_file_mode']:
487 elif head['new_file_mode']:
479 op = OPS.ADD
488 op = OPS.ADD
480 stats['binary'] = True
489 stats['binary'] = True
481 stats['old_mode'] = None
490 stats['old_mode'] = None
482 stats['new_mode'] = head['new_file_mode']
491 stats['new_mode'] = head['new_file_mode']
483 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
492 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
484
493
485 # modify operation, can be copy, rename or chmod
494 # modify operation, can be copy, rename or chmod
486 else:
495 else:
487 # CHMOD
496 # CHMOD
488 if head['new_mode'] and head['old_mode']:
497 if head['new_mode'] and head['old_mode']:
489 op = OPS.MOD
498 op = OPS.MOD
490 stats['binary'] = True
499 stats['binary'] = True
491 stats['ops'][CHMOD_FILENODE] = (
500 stats['ops'][CHMOD_FILENODE] = (
492 'modified file chmod %s => %s' % (
501 'modified file chmod %s => %s' % (
493 head['old_mode'], head['new_mode']))
502 head['old_mode'], head['new_mode']))
494
503
495 # RENAME
504 # RENAME
496 if head['rename_from'] != head['rename_to']:
505 if head['rename_from'] != head['rename_to']:
497 op = OPS.MOD
506 op = OPS.MOD
498 stats['binary'] = True
507 stats['binary'] = True
499 stats['renamed'] = (head['rename_from'], head['rename_to'])
508 stats['renamed'] = (head['rename_from'], head['rename_to'])
500 stats['ops'][RENAMED_FILENODE] = (
509 stats['ops'][RENAMED_FILENODE] = (
501 'file renamed from %s to %s' % (
510 'file renamed from %s to %s' % (
502 head['rename_from'], head['rename_to']))
511 head['rename_from'], head['rename_to']))
503 # COPY
512 # COPY
504 if head.get('copy_from') and head.get('copy_to'):
513 if head.get('copy_from') and head.get('copy_to'):
505 op = OPS.MOD
514 op = OPS.MOD
506 stats['binary'] = True
515 stats['binary'] = True
507 stats['copied'] = (head['copy_from'], head['copy_to'])
516 stats['copied'] = (head['copy_from'], head['copy_to'])
508 stats['ops'][COPIED_FILENODE] = (
517 stats['ops'][COPIED_FILENODE] = (
509 'file copied from %s to %s' % (
518 'file copied from %s to %s' % (
510 head['copy_from'], head['copy_to']))
519 head['copy_from'], head['copy_to']))
511
520
512 # If our new parsed headers didn't match anything fallback to
521 # If our new parsed headers didn't match anything fallback to
513 # old style detection
522 # old style detection
514 if op is None:
523 if op is None:
515 if not head['a_file'] and head['b_file']:
524 if not head['a_file'] and head['b_file']:
516 op = OPS.ADD
525 op = OPS.ADD
517 stats['binary'] = True
526 stats['binary'] = True
518 stats['new_file'] = True
527 stats['new_file'] = True
519 stats['ops'][NEW_FILENODE] = 'new file'
528 stats['ops'][NEW_FILENODE] = 'new file'
520
529
521 elif head['a_file'] and not head['b_file']:
530 elif head['a_file'] and not head['b_file']:
522 op = OPS.DEL
531 op = OPS.DEL
523 stats['binary'] = True
532 stats['binary'] = True
524 stats['ops'][DEL_FILENODE] = 'deleted file'
533 stats['ops'][DEL_FILENODE] = 'deleted file'
525
534
526 # it's not ADD not DELETE
535 # it's not ADD not DELETE
527 if op is None:
536 if op is None:
528 op = OPS.MOD
537 op = OPS.MOD
529 stats['binary'] = True
538 stats['binary'] = True
530 stats['ops'][MOD_FILENODE] = 'modified file'
539 stats['ops'][MOD_FILENODE] = 'modified file'
531
540
532 # a real non-binary diff
541 # a real non-binary diff
533 if head['a_file'] or head['b_file']:
542 if head['a_file'] or head['b_file']:
534 # simulate splitlines, so we keep the line end part
543 # simulate splitlines, so we keep the line end part
535 diff = self.diff_splitter(chunk.diff)
544 diff = self.diff_splitter(chunk.diff)
536
545
537 # append each file to the diff size
546 # append each file to the diff size
538 raw_chunk_size = len(raw_diff)
547 raw_chunk_size = len(raw_diff)
539
548
540 exceeds_limit = raw_chunk_size > self.file_limit
549 exceeds_limit = raw_chunk_size > self.file_limit
541 self.cur_diff_size += raw_chunk_size
550 self.cur_diff_size += raw_chunk_size
542
551
543 try:
552 try:
544 # Check each file instead of the whole diff.
553 # Check each file instead of the whole diff.
545 # Diff will hide big files but still show small ones.
554 # Diff will hide big files but still show small ones.
546 # From the tests big files are fairly safe to be parsed
555 # From the tests big files are fairly safe to be parsed
547 # but the browser is the bottleneck.
556 # but the browser is the bottleneck.
548 if not self.show_full_diff and exceeds_limit:
557 if not self.show_full_diff and exceeds_limit:
549 log.debug('File `%s` exceeds current file_limit of %s',
558 log.debug('File `%s` exceeds current file_limit of %s',
550 safe_unicode(head['b_path']), self.file_limit)
559 safe_unicode(head['b_path']), self.file_limit)
551 raise DiffLimitExceeded(
560 raise DiffLimitExceeded(
552 'File Limit %s Exceeded', self.file_limit)
561 'File Limit %s Exceeded', self.file_limit)
553
562
554 self._check_large_diff()
563 self._check_large_diff()
555
564
556 raw_diff, chunks, _stats = self._new_parse_lines(diff)
565 raw_diff, chunks, _stats = self._new_parse_lines(diff)
557 stats['binary'] = False
566 stats['binary'] = False
558 stats['added'] = _stats[0]
567 stats['added'] = _stats[0]
559 stats['deleted'] = _stats[1]
568 stats['deleted'] = _stats[1]
560 # explicit mark that it's a modified file
569 # explicit mark that it's a modified file
561 if op == OPS.MOD:
570 if op == OPS.MOD:
562 stats['ops'][MOD_FILENODE] = 'modified file'
571 stats['ops'][MOD_FILENODE] = 'modified file'
563
572
564 except DiffLimitExceeded:
573 except DiffLimitExceeded:
565 diff_container = lambda _diff: \
574 diff_container = lambda _diff: \
566 LimitedDiffContainer(
575 LimitedDiffContainer(
567 self.diff_limit, self.cur_diff_size, _diff)
576 self.diff_limit, self.cur_diff_size, _diff)
568
577
569 limited_diff = True
578 limited_diff = True
570 chunks = []
579 chunks = []
571
580
572 else: # GIT format binary patch, or possibly empty diff
581 else: # GIT format binary patch, or possibly empty diff
573 if head['bin_patch']:
582 if head['bin_patch']:
574 # we have operation already extracted, but we mark simply
583 # we have operation already extracted, but we mark simply
575 # it's a diff we wont show for binary files
584 # it's a diff we wont show for binary files
576 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
585 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
577 chunks = []
586 chunks = []
578
587
579 # Hide content of deleted node by setting empty chunks
588 # Hide content of deleted node by setting empty chunks
580 if chunks and not self.show_full_diff and op == OPS.DEL:
589 if chunks and not self.show_full_diff and op == OPS.DEL:
581 # if not full diff mode show deleted file contents
590 # if not full diff mode show deleted file contents
582 # TODO: anderson: if the view is not too big, there is no way
591 # TODO: anderson: if the view is not too big, there is no way
583 # to see the content of the file
592 # to see the content of the file
584 chunks = []
593 chunks = []
585
594
586 chunks.insert(
595 chunks.insert(
587 0, [{'old_lineno': '',
596 0, [{'old_lineno': '',
588 'new_lineno': '',
597 'new_lineno': '',
589 'action': Action.CONTEXT,
598 'action': Action.CONTEXT,
590 'line': msg,
599 'line': msg,
591 } for _op, msg in stats['ops'].iteritems()
600 } for _op, msg in stats['ops'].iteritems()
592 if _op not in [MOD_FILENODE]])
601 if _op not in [MOD_FILENODE]])
593
602
594 original_filename = safe_unicode(head['a_path'])
603 original_filename = safe_unicode(head['a_path'])
595 _files.append({
604 _files.append({
596 'original_filename': original_filename,
605 'original_filename': original_filename,
597 'filename': safe_unicode(head['b_path']),
606 'filename': safe_unicode(head['b_path']),
598 'old_revision': head['a_blob_id'],
607 'old_revision': head['a_blob_id'],
599 'new_revision': head['b_blob_id'],
608 'new_revision': head['b_blob_id'],
600 'chunks': chunks,
609 'chunks': chunks,
601 'raw_diff': safe_unicode(raw_diff),
610 'raw_diff': safe_unicode(raw_diff),
602 'operation': op,
611 'operation': op,
603 'stats': stats,
612 'stats': stats,
604 'exceeds_limit': exceeds_limit,
613 'exceeds_limit': exceeds_limit,
605 'is_limited_diff': limited_diff,
614 'is_limited_diff': limited_diff,
606 })
615 })
607
616
608 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
617 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
609 OPS.DEL: 2}.get(info['operation'])
618 OPS.DEL: 2}.get(info['operation'])
610
619
611 return diff_container(sorted(_files, key=sorter))
620 return diff_container(sorted(_files, key=sorter))
612
621
613 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
622 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
614 def _parse_lines(self, diff_iter):
623 def _parse_lines(self, diff_iter):
615 """
624 """
616 Parse the diff an return data for the template.
625 Parse the diff an return data for the template.
617 """
626 """
618
627
619 stats = [0, 0]
628 stats = [0, 0]
620 chunks = []
629 chunks = []
621 raw_diff = []
630 raw_diff = []
622
631
623 try:
632 try:
624 line = diff_iter.next()
633 line = diff_iter.next()
625
634
626 while line:
635 while line:
627 raw_diff.append(line)
636 raw_diff.append(line)
628 lines = []
637 lines = []
629 chunks.append(lines)
638 chunks.append(lines)
630
639
631 match = self._chunk_re.match(line)
640 match = self._chunk_re.match(line)
632
641
633 if not match:
642 if not match:
634 break
643 break
635
644
636 gr = match.groups()
645 gr = match.groups()
637 (old_line, old_end,
646 (old_line, old_end,
638 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
647 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
639 old_line -= 1
648 old_line -= 1
640 new_line -= 1
649 new_line -= 1
641
650
642 context = len(gr) == 5
651 context = len(gr) == 5
643 old_end += old_line
652 old_end += old_line
644 new_end += new_line
653 new_end += new_line
645
654
646 if context:
655 if context:
647 # skip context only if it's first line
656 # skip context only if it's first line
648 if int(gr[0]) > 1:
657 if int(gr[0]) > 1:
649 lines.append({
658 lines.append({
650 'old_lineno': '...',
659 'old_lineno': '...',
651 'new_lineno': '...',
660 'new_lineno': '...',
652 'action': Action.CONTEXT,
661 'action': Action.CONTEXT,
653 'line': line,
662 'line': line,
654 })
663 })
655
664
656 line = diff_iter.next()
665 line = diff_iter.next()
657
666
658 while old_line < old_end or new_line < new_end:
667 while old_line < old_end or new_line < new_end:
659 command = ' '
668 command = ' '
660 if line:
669 if line:
661 command = line[0]
670 command = line[0]
662
671
663 affects_old = affects_new = False
672 affects_old = affects_new = False
664
673
665 # ignore those if we don't expect them
674 # ignore those if we don't expect them
666 if command in '#@':
675 if command in '#@':
667 continue
676 continue
668 elif command == '+':
677 elif command == '+':
669 affects_new = True
678 affects_new = True
670 action = Action.ADD
679 action = Action.ADD
671 stats[0] += 1
680 stats[0] += 1
672 elif command == '-':
681 elif command == '-':
673 affects_old = True
682 affects_old = True
674 action = Action.DELETE
683 action = Action.DELETE
675 stats[1] += 1
684 stats[1] += 1
676 else:
685 else:
677 affects_old = affects_new = True
686 affects_old = affects_new = True
678 action = Action.UNMODIFIED
687 action = Action.UNMODIFIED
679
688
680 if not self._newline_marker.match(line):
689 if not self._newline_marker.match(line):
681 old_line += affects_old
690 old_line += affects_old
682 new_line += affects_new
691 new_line += affects_new
683 lines.append({
692 lines.append({
684 'old_lineno': affects_old and old_line or '',
693 'old_lineno': affects_old and old_line or '',
685 'new_lineno': affects_new and new_line or '',
694 'new_lineno': affects_new and new_line or '',
686 'action': action,
695 'action': action,
687 'line': self._clean_line(line, command)
696 'line': self._clean_line(line, command)
688 })
697 })
689 raw_diff.append(line)
698 raw_diff.append(line)
690
699
691 line = diff_iter.next()
700 line = diff_iter.next()
692
701
693 if self._newline_marker.match(line):
702 if self._newline_marker.match(line):
694 # we need to append to lines, since this is not
703 # we need to append to lines, since this is not
695 # counted in the line specs of diff
704 # counted in the line specs of diff
696 lines.append({
705 lines.append({
697 'old_lineno': '...',
706 'old_lineno': '...',
698 'new_lineno': '...',
707 'new_lineno': '...',
699 'action': Action.CONTEXT,
708 'action': Action.CONTEXT,
700 'line': self._clean_line(line, command)
709 'line': self._clean_line(line, command)
701 })
710 })
702
711
703 except StopIteration:
712 except StopIteration:
704 pass
713 pass
705 return ''.join(raw_diff), chunks, stats
714 return ''.join(raw_diff), chunks, stats
706
715
707 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
716 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
708 def _new_parse_lines(self, diff_iter):
717 def _new_parse_lines(self, diff_iter):
709 """
718 """
710 Parse the diff an return data for the template.
719 Parse the diff an return data for the template.
711 """
720 """
712
721
713 stats = [0, 0]
722 stats = [0, 0]
714 chunks = []
723 chunks = []
715 raw_diff = []
724 raw_diff = []
716
725
717 try:
726 try:
718 line = diff_iter.next()
727 line = diff_iter.next()
719
728
720 while line:
729 while line:
721 raw_diff.append(line)
730 raw_diff.append(line)
722 # match header e.g @@ -0,0 +1 @@\n'
731 # match header e.g @@ -0,0 +1 @@\n'
723 match = self._chunk_re.match(line)
732 match = self._chunk_re.match(line)
724
733
725 if not match:
734 if not match:
726 break
735 break
727
736
728 gr = match.groups()
737 gr = match.groups()
729 (old_line, old_end,
738 (old_line, old_end,
730 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
739 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
731
740
732 lines = []
741 lines = []
733 hunk = {
742 hunk = {
734 'section_header': gr[-1],
743 'section_header': gr[-1],
735 'source_start': old_line,
744 'source_start': old_line,
736 'source_length': old_end,
745 'source_length': old_end,
737 'target_start': new_line,
746 'target_start': new_line,
738 'target_length': new_end,
747 'target_length': new_end,
739 'lines': lines,
748 'lines': lines,
740 }
749 }
741 chunks.append(hunk)
750 chunks.append(hunk)
742
751
743 old_line -= 1
752 old_line -= 1
744 new_line -= 1
753 new_line -= 1
745
754
746 context = len(gr) == 5
755 context = len(gr) == 5
747 old_end += old_line
756 old_end += old_line
748 new_end += new_line
757 new_end += new_line
749
758
750 line = diff_iter.next()
759 line = diff_iter.next()
751
760
752 while old_line < old_end or new_line < new_end:
761 while old_line < old_end or new_line < new_end:
753 command = ' '
762 command = ' '
754 if line:
763 if line:
755 command = line[0]
764 command = line[0]
756
765
757 affects_old = affects_new = False
766 affects_old = affects_new = False
758
767
759 # ignore those if we don't expect them
768 # ignore those if we don't expect them
760 if command in '#@':
769 if command in '#@':
761 continue
770 continue
762 elif command == '+':
771 elif command == '+':
763 affects_new = True
772 affects_new = True
764 action = Action.ADD
773 action = Action.ADD
765 stats[0] += 1
774 stats[0] += 1
766 elif command == '-':
775 elif command == '-':
767 affects_old = True
776 affects_old = True
768 action = Action.DELETE
777 action = Action.DELETE
769 stats[1] += 1
778 stats[1] += 1
770 else:
779 else:
771 affects_old = affects_new = True
780 affects_old = affects_new = True
772 action = Action.UNMODIFIED
781 action = Action.UNMODIFIED
773
782
774 if not self._newline_marker.match(line):
783 if not self._newline_marker.match(line):
775 old_line += affects_old
784 old_line += affects_old
776 new_line += affects_new
785 new_line += affects_new
777 lines.append({
786 lines.append({
778 'old_lineno': affects_old and old_line or '',
787 'old_lineno': affects_old and old_line or '',
779 'new_lineno': affects_new and new_line or '',
788 'new_lineno': affects_new and new_line or '',
780 'action': action,
789 'action': action,
781 'line': self._clean_line(line, command)
790 'line': self._clean_line(line, command)
782 })
791 })
783 raw_diff.append(line)
792 raw_diff.append(line)
784
793
785 line = diff_iter.next()
794 line = diff_iter.next()
786
795
787 if self._newline_marker.match(line):
796 if self._newline_marker.match(line):
788 # we need to append to lines, since this is not
797 # we need to append to lines, since this is not
789 # counted in the line specs of diff
798 # counted in the line specs of diff
790 if affects_old:
799 if affects_old:
791 action = Action.OLD_NO_NL
800 action = Action.OLD_NO_NL
792 elif affects_new:
801 elif affects_new:
793 action = Action.NEW_NO_NL
802 action = Action.NEW_NO_NL
794 else:
803 else:
795 raise Exception('invalid context for no newline')
804 raise Exception('invalid context for no newline')
796
805
797 lines.append({
806 lines.append({
798 'old_lineno': None,
807 'old_lineno': None,
799 'new_lineno': None,
808 'new_lineno': None,
800 'action': action,
809 'action': action,
801 'line': self._clean_line(line, command)
810 'line': self._clean_line(line, command)
802 })
811 })
803
812
804 except StopIteration:
813 except StopIteration:
805 pass
814 pass
806
815
807 return ''.join(raw_diff), chunks, stats
816 return ''.join(raw_diff), chunks, stats
808
817
809 def _safe_id(self, idstring):
818 def _safe_id(self, idstring):
810 """Make a string safe for including in an id attribute.
819 """Make a string safe for including in an id attribute.
811
820
812 The HTML spec says that id attributes 'must begin with
821 The HTML spec says that id attributes 'must begin with
813 a letter ([A-Za-z]) and may be followed by any number
822 a letter ([A-Za-z]) and may be followed by any number
814 of letters, digits ([0-9]), hyphens ("-"), underscores
823 of letters, digits ([0-9]), hyphens ("-"), underscores
815 ("_"), colons (":"), and periods (".")'. These regexps
824 ("_"), colons (":"), and periods (".")'. These regexps
816 are slightly over-zealous, in that they remove colons
825 are slightly over-zealous, in that they remove colons
817 and periods unnecessarily.
826 and periods unnecessarily.
818
827
819 Whitespace is transformed into underscores, and then
828 Whitespace is transformed into underscores, and then
820 anything which is not a hyphen or a character that
829 anything which is not a hyphen or a character that
821 matches \w (alphanumerics and underscore) is removed.
830 matches \w (alphanumerics and underscore) is removed.
822
831
823 """
832 """
824 # Transform all whitespace to underscore
833 # Transform all whitespace to underscore
825 idstring = re.sub(r'\s', "_", '%s' % idstring)
834 idstring = re.sub(r'\s', "_", '%s' % idstring)
826 # Remove everything that is not a hyphen or a member of \w
835 # Remove everything that is not a hyphen or a member of \w
827 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
836 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
828 return idstring
837 return idstring
829
838
830 @classmethod
839 @classmethod
831 def diff_splitter(cls, string):
840 def diff_splitter(cls, string):
832 """
841 """
833 Diff split that emulates .splitlines() but works only on \n
842 Diff split that emulates .splitlines() but works only on \n
834 """
843 """
835 if not string:
844 if not string:
836 return
845 return
837 elif string == '\n':
846 elif string == '\n':
838 yield u'\n'
847 yield u'\n'
839 else:
848 else:
840
849
841 has_newline = string.endswith('\n')
850 has_newline = string.endswith('\n')
842 elements = string.split('\n')
851 elements = string.split('\n')
843 if has_newline:
852 if has_newline:
844 # skip last element as it's empty string from newlines
853 # skip last element as it's empty string from newlines
845 elements = elements[:-1]
854 elements = elements[:-1]
846
855
847 len_elements = len(elements)
856 len_elements = len(elements)
848
857
849 for cnt, line in enumerate(elements, start=1):
858 for cnt, line in enumerate(elements, start=1):
850 last_line = cnt == len_elements
859 last_line = cnt == len_elements
851 if last_line and not has_newline:
860 if last_line and not has_newline:
852 yield safe_unicode(line)
861 yield safe_unicode(line)
853 else:
862 else:
854 yield safe_unicode(line) + '\n'
863 yield safe_unicode(line) + '\n'
855
864
856 def prepare(self, inline_diff=True):
865 def prepare(self, inline_diff=True):
857 """
866 """
858 Prepare the passed udiff for HTML rendering.
867 Prepare the passed udiff for HTML rendering.
859
868
860 :return: A list of dicts with diff information.
869 :return: A list of dicts with diff information.
861 """
870 """
862 parsed = self._parser(inline_diff=inline_diff)
871 parsed = self._parser(inline_diff=inline_diff)
863 self.parsed = True
872 self.parsed = True
864 self.parsed_diff = parsed
873 self.parsed_diff = parsed
865 return parsed
874 return parsed
866
875
867 def as_raw(self, diff_lines=None):
876 def as_raw(self, diff_lines=None):
868 """
877 """
869 Returns raw diff as a byte string
878 Returns raw diff as a byte string
870 """
879 """
871 return self._diff.raw
880 return self._diff.raw
872
881
873 def as_html(self, table_class='code-difftable', line_class='line',
882 def as_html(self, table_class='code-difftable', line_class='line',
874 old_lineno_class='lineno old', new_lineno_class='lineno new',
883 old_lineno_class='lineno old', new_lineno_class='lineno new',
875 code_class='code', enable_comments=False, parsed_lines=None):
884 code_class='code', enable_comments=False, parsed_lines=None):
876 """
885 """
877 Return given diff as html table with customized css classes
886 Return given diff as html table with customized css classes
878 """
887 """
879 # TODO(marcink): not sure how to pass in translator
888 # TODO(marcink): not sure how to pass in translator
880 # here in an efficient way, leave the _ for proper gettext extraction
889 # here in an efficient way, leave the _ for proper gettext extraction
881 _ = lambda s: s
890 _ = lambda s: s
882
891
883 def _link_to_if(condition, label, url):
892 def _link_to_if(condition, label, url):
884 """
893 """
885 Generates a link if condition is meet or just the label if not.
894 Generates a link if condition is meet or just the label if not.
886 """
895 """
887
896
888 if condition:
897 if condition:
889 return '''<a href="%(url)s" class="tooltip"
898 return '''<a href="%(url)s" class="tooltip"
890 title="%(title)s">%(label)s</a>''' % {
899 title="%(title)s">%(label)s</a>''' % {
891 'title': _('Click to select line'),
900 'title': _('Click to select line'),
892 'url': url,
901 'url': url,
893 'label': label
902 'label': label
894 }
903 }
895 else:
904 else:
896 return label
905 return label
897 if not self.parsed:
906 if not self.parsed:
898 self.prepare()
907 self.prepare()
899
908
900 diff_lines = self.parsed_diff
909 diff_lines = self.parsed_diff
901 if parsed_lines:
910 if parsed_lines:
902 diff_lines = parsed_lines
911 diff_lines = parsed_lines
903
912
904 _html_empty = True
913 _html_empty = True
905 _html = []
914 _html = []
906 _html.append('''<table class="%(table_class)s">\n''' % {
915 _html.append('''<table class="%(table_class)s">\n''' % {
907 'table_class': table_class
916 'table_class': table_class
908 })
917 })
909
918
910 for diff in diff_lines:
919 for diff in diff_lines:
911 for line in diff['chunks']:
920 for line in diff['chunks']:
912 _html_empty = False
921 _html_empty = False
913 for change in line:
922 for change in line:
914 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
923 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
915 'lc': line_class,
924 'lc': line_class,
916 'action': change['action']
925 'action': change['action']
917 })
926 })
918 anchor_old_id = ''
927 anchor_old_id = ''
919 anchor_new_id = ''
928 anchor_new_id = ''
920 anchor_old = "%(filename)s_o%(oldline_no)s" % {
929 anchor_old = "%(filename)s_o%(oldline_no)s" % {
921 'filename': self._safe_id(diff['filename']),
930 'filename': self._safe_id(diff['filename']),
922 'oldline_no': change['old_lineno']
931 'oldline_no': change['old_lineno']
923 }
932 }
924 anchor_new = "%(filename)s_n%(oldline_no)s" % {
933 anchor_new = "%(filename)s_n%(oldline_no)s" % {
925 'filename': self._safe_id(diff['filename']),
934 'filename': self._safe_id(diff['filename']),
926 'oldline_no': change['new_lineno']
935 'oldline_no': change['new_lineno']
927 }
936 }
928 cond_old = (change['old_lineno'] != '...' and
937 cond_old = (change['old_lineno'] != '...' and
929 change['old_lineno'])
938 change['old_lineno'])
930 cond_new = (change['new_lineno'] != '...' and
939 cond_new = (change['new_lineno'] != '...' and
931 change['new_lineno'])
940 change['new_lineno'])
932 if cond_old:
941 if cond_old:
933 anchor_old_id = 'id="%s"' % anchor_old
942 anchor_old_id = 'id="%s"' % anchor_old
934 if cond_new:
943 if cond_new:
935 anchor_new_id = 'id="%s"' % anchor_new
944 anchor_new_id = 'id="%s"' % anchor_new
936
945
937 if change['action'] != Action.CONTEXT:
946 if change['action'] != Action.CONTEXT:
938 anchor_link = True
947 anchor_link = True
939 else:
948 else:
940 anchor_link = False
949 anchor_link = False
941
950
942 ###########################################################
951 ###########################################################
943 # COMMENT ICONS
952 # COMMENT ICONS
944 ###########################################################
953 ###########################################################
945 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
954 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
946
955
947 if enable_comments and change['action'] != Action.CONTEXT:
956 if enable_comments and change['action'] != Action.CONTEXT:
948 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
957 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
949
958
950 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
959 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
951
960
952 ###########################################################
961 ###########################################################
953 # OLD LINE NUMBER
962 # OLD LINE NUMBER
954 ###########################################################
963 ###########################################################
955 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
964 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
956 'a_id': anchor_old_id,
965 'a_id': anchor_old_id,
957 'olc': old_lineno_class
966 'olc': old_lineno_class
958 })
967 })
959
968
960 _html.append('''%(link)s''' % {
969 _html.append('''%(link)s''' % {
961 'link': _link_to_if(anchor_link, change['old_lineno'],
970 'link': _link_to_if(anchor_link, change['old_lineno'],
962 '#%s' % anchor_old)
971 '#%s' % anchor_old)
963 })
972 })
964 _html.append('''</td>\n''')
973 _html.append('''</td>\n''')
965 ###########################################################
974 ###########################################################
966 # NEW LINE NUMBER
975 # NEW LINE NUMBER
967 ###########################################################
976 ###########################################################
968
977
969 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
978 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
970 'a_id': anchor_new_id,
979 'a_id': anchor_new_id,
971 'nlc': new_lineno_class
980 'nlc': new_lineno_class
972 })
981 })
973
982
974 _html.append('''%(link)s''' % {
983 _html.append('''%(link)s''' % {
975 'link': _link_to_if(anchor_link, change['new_lineno'],
984 'link': _link_to_if(anchor_link, change['new_lineno'],
976 '#%s' % anchor_new)
985 '#%s' % anchor_new)
977 })
986 })
978 _html.append('''</td>\n''')
987 _html.append('''</td>\n''')
979 ###########################################################
988 ###########################################################
980 # CODE
989 # CODE
981 ###########################################################
990 ###########################################################
982 code_classes = [code_class]
991 code_classes = [code_class]
983 if (not enable_comments or
992 if (not enable_comments or
984 change['action'] == Action.CONTEXT):
993 change['action'] == Action.CONTEXT):
985 code_classes.append('no-comment')
994 code_classes.append('no-comment')
986 _html.append('\t<td class="%s">' % ' '.join(code_classes))
995 _html.append('\t<td class="%s">' % ' '.join(code_classes))
987 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
996 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
988 'code': change['line']
997 'code': change['line']
989 })
998 })
990
999
991 _html.append('''\t</td>''')
1000 _html.append('''\t</td>''')
992 _html.append('''\n</tr>\n''')
1001 _html.append('''\n</tr>\n''')
993 _html.append('''</table>''')
1002 _html.append('''</table>''')
994 if _html_empty:
1003 if _html_empty:
995 return None
1004 return None
996 return ''.join(_html)
1005 return ''.join(_html)
997
1006
998 def stat(self):
1007 def stat(self):
999 """
1008 """
1000 Returns tuple of added, and removed lines for this instance
1009 Returns tuple of added, and removed lines for this instance
1001 """
1010 """
1002 return self.adds, self.removes
1011 return self.adds, self.removes
1003
1012
1004 def get_context_of_line(
1013 def get_context_of_line(
1005 self, path, diff_line=None, context_before=3, context_after=3):
1014 self, path, diff_line=None, context_before=3, context_after=3):
1006 """
1015 """
1007 Returns the context lines for the specified diff line.
1016 Returns the context lines for the specified diff line.
1008
1017
1009 :type diff_line: :class:`DiffLineNumber`
1018 :type diff_line: :class:`DiffLineNumber`
1010 """
1019 """
1011 assert self.parsed, "DiffProcessor is not initialized."
1020 assert self.parsed, "DiffProcessor is not initialized."
1012
1021
1013 if None not in diff_line:
1022 if None not in diff_line:
1014 raise ValueError(
1023 raise ValueError(
1015 "Cannot specify both line numbers: {}".format(diff_line))
1024 "Cannot specify both line numbers: {}".format(diff_line))
1016
1025
1017 file_diff = self._get_file_diff(path)
1026 file_diff = self._get_file_diff(path)
1018 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1027 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1019
1028
1020 first_line_to_include = max(idx - context_before, 0)
1029 first_line_to_include = max(idx - context_before, 0)
1021 first_line_after_context = idx + context_after + 1
1030 first_line_after_context = idx + context_after + 1
1022 context_lines = chunk[first_line_to_include:first_line_after_context]
1031 context_lines = chunk[first_line_to_include:first_line_after_context]
1023
1032
1024 line_contents = [
1033 line_contents = [
1025 _context_line(line) for line in context_lines
1034 _context_line(line) for line in context_lines
1026 if _is_diff_content(line)]
1035 if _is_diff_content(line)]
1027 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1036 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1028 # Once they are fixed, we can drop this line here.
1037 # Once they are fixed, we can drop this line here.
1029 if line_contents:
1038 if line_contents:
1030 line_contents[-1] = (
1039 line_contents[-1] = (
1031 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1040 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1032 return line_contents
1041 return line_contents
1033
1042
1034 def find_context(self, path, context, offset=0):
1043 def find_context(self, path, context, offset=0):
1035 """
1044 """
1036 Finds the given `context` inside of the diff.
1045 Finds the given `context` inside of the diff.
1037
1046
1038 Use the parameter `offset` to specify which offset the target line has
1047 Use the parameter `offset` to specify which offset the target line has
1039 inside of the given `context`. This way the correct diff line will be
1048 inside of the given `context`. This way the correct diff line will be
1040 returned.
1049 returned.
1041
1050
1042 :param offset: Shall be used to specify the offset of the main line
1051 :param offset: Shall be used to specify the offset of the main line
1043 within the given `context`.
1052 within the given `context`.
1044 """
1053 """
1045 if offset < 0 or offset >= len(context):
1054 if offset < 0 or offset >= len(context):
1046 raise ValueError(
1055 raise ValueError(
1047 "Only positive values up to the length of the context "
1056 "Only positive values up to the length of the context "
1048 "minus one are allowed.")
1057 "minus one are allowed.")
1049
1058
1050 matches = []
1059 matches = []
1051 file_diff = self._get_file_diff(path)
1060 file_diff = self._get_file_diff(path)
1052
1061
1053 for chunk in file_diff['chunks']:
1062 for chunk in file_diff['chunks']:
1054 context_iter = iter(context)
1063 context_iter = iter(context)
1055 for line_idx, line in enumerate(chunk):
1064 for line_idx, line in enumerate(chunk):
1056 try:
1065 try:
1057 if _context_line(line) == context_iter.next():
1066 if _context_line(line) == context_iter.next():
1058 continue
1067 continue
1059 except StopIteration:
1068 except StopIteration:
1060 matches.append((line_idx, chunk))
1069 matches.append((line_idx, chunk))
1061 context_iter = iter(context)
1070 context_iter = iter(context)
1062
1071
1063 # Increment position and triger StopIteration
1072 # Increment position and triger StopIteration
1064 # if we had a match at the end
1073 # if we had a match at the end
1065 line_idx += 1
1074 line_idx += 1
1066 try:
1075 try:
1067 context_iter.next()
1076 context_iter.next()
1068 except StopIteration:
1077 except StopIteration:
1069 matches.append((line_idx, chunk))
1078 matches.append((line_idx, chunk))
1070
1079
1071 effective_offset = len(context) - offset
1080 effective_offset = len(context) - offset
1072 found_at_diff_lines = [
1081 found_at_diff_lines = [
1073 _line_to_diff_line_number(chunk[idx - effective_offset])
1082 _line_to_diff_line_number(chunk[idx - effective_offset])
1074 for idx, chunk in matches]
1083 for idx, chunk in matches]
1075
1084
1076 return found_at_diff_lines
1085 return found_at_diff_lines
1077
1086
1078 def _get_file_diff(self, path):
1087 def _get_file_diff(self, path):
1079 for file_diff in self.parsed_diff:
1088 for file_diff in self.parsed_diff:
1080 if file_diff['filename'] == path:
1089 if file_diff['filename'] == path:
1081 break
1090 break
1082 else:
1091 else:
1083 raise FileNotInDiffException("File {} not in diff".format(path))
1092 raise FileNotInDiffException("File {} not in diff".format(path))
1084 return file_diff
1093 return file_diff
1085
1094
1086 def _find_chunk_line_index(self, file_diff, diff_line):
1095 def _find_chunk_line_index(self, file_diff, diff_line):
1087 for chunk in file_diff['chunks']:
1096 for chunk in file_diff['chunks']:
1088 for idx, line in enumerate(chunk):
1097 for idx, line in enumerate(chunk):
1089 if line['old_lineno'] == diff_line.old:
1098 if line['old_lineno'] == diff_line.old:
1090 return chunk, idx
1099 return chunk, idx
1091 if line['new_lineno'] == diff_line.new:
1100 if line['new_lineno'] == diff_line.new:
1092 return chunk, idx
1101 return chunk, idx
1093 raise LineNotInDiffException(
1102 raise LineNotInDiffException(
1094 "The line {} is not part of the diff.".format(diff_line))
1103 "The line {} is not part of the diff.".format(diff_line))
1095
1104
1096
1105
1097 def _is_diff_content(line):
1106 def _is_diff_content(line):
1098 return line['action'] in (
1107 return line['action'] in (
1099 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1108 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1100
1109
1101
1110
1102 def _context_line(line):
1111 def _context_line(line):
1103 return (line['action'], line['line'])
1112 return (line['action'], line['line'])
1104
1113
1105
1114
1106 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1115 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1107
1116
1108
1117
1109 def _line_to_diff_line_number(line):
1118 def _line_to_diff_line_number(line):
1110 new_line_no = line['new_lineno'] or None
1119 new_line_no = line['new_lineno'] or None
1111 old_line_no = line['old_lineno'] or None
1120 old_line_no = line['old_lineno'] or None
1112 return DiffLineNumber(old=old_line_no, new=new_line_no)
1121 return DiffLineNumber(old=old_line_no, new=new_line_no)
1113
1122
1114
1123
1115 class FileNotInDiffException(Exception):
1124 class FileNotInDiffException(Exception):
1116 """
1125 """
1117 Raised when the context for a missing file is requested.
1126 Raised when the context for a missing file is requested.
1118
1127
1119 If you request the context for a line in a file which is not part of the
1128 If you request the context for a line in a file which is not part of the
1120 given diff, then this exception is raised.
1129 given diff, then this exception is raised.
1121 """
1130 """
1122
1131
1123
1132
1124 class LineNotInDiffException(Exception):
1133 class LineNotInDiffException(Exception):
1125 """
1134 """
1126 Raised when the context for a missing line is requested.
1135 Raised when the context for a missing line is requested.
1127
1136
1128 If you request the context for a line in a file and this line is not
1137 If you request the context for a line in a file and this line is not
1129 part of the given diff, then this exception is raised.
1138 part of the given diff, then this exception is raised.
1130 """
1139 """
1131
1140
1132
1141
1133 class DiffLimitExceeded(Exception):
1142 class DiffLimitExceeded(Exception):
1134 pass
1143 pass
1135
1144
1136
1145
1137 # NOTE(marcink): if diffs.mako change, probably this
1146 # NOTE(marcink): if diffs.mako change, probably this
1138 # needs a bump to next version
1147 # needs a bump to next version
1139 CURRENT_DIFF_VERSION = 'v3'
1148 CURRENT_DIFF_VERSION = 'v3'
1140
1149
1141
1150
1142 def _cleanup_cache_file(cached_diff_file):
1151 def _cleanup_cache_file(cached_diff_file):
1143 # cleanup file to not store it "damaged"
1152 # cleanup file to not store it "damaged"
1144 try:
1153 try:
1145 os.remove(cached_diff_file)
1154 os.remove(cached_diff_file)
1146 except Exception:
1155 except Exception:
1147 log.exception('Failed to cleanup path %s', cached_diff_file)
1156 log.exception('Failed to cleanup path %s', cached_diff_file)
1148
1157
1149
1158
1150 def cache_diff(cached_diff_file, diff, commits):
1159 def cache_diff(cached_diff_file, diff, commits):
1151
1160
1152 struct = {
1161 struct = {
1153 'version': CURRENT_DIFF_VERSION,
1162 'version': CURRENT_DIFF_VERSION,
1154 'diff': diff,
1163 'diff': diff,
1155 'commits': commits
1164 'commits': commits
1156 }
1165 }
1157
1166
1158 try:
1167 try:
1159 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1168 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1160 pickle.dump(struct, f)
1169 pickle.dump(struct, f)
1161 log.debug('Saved diff cache under %s', cached_diff_file)
1170 log.debug('Saved diff cache under %s', cached_diff_file)
1162 except Exception:
1171 except Exception:
1163 log.warn('Failed to save cache', exc_info=True)
1172 log.warn('Failed to save cache', exc_info=True)
1164 _cleanup_cache_file(cached_diff_file)
1173 _cleanup_cache_file(cached_diff_file)
1165
1174
1166
1175
1167 def load_cached_diff(cached_diff_file):
1176 def load_cached_diff(cached_diff_file):
1168
1177
1169 default_struct = {
1178 default_struct = {
1170 'version': CURRENT_DIFF_VERSION,
1179 'version': CURRENT_DIFF_VERSION,
1171 'diff': None,
1180 'diff': None,
1172 'commits': None
1181 'commits': None
1173 }
1182 }
1174
1183
1175 has_cache = os.path.isfile(cached_diff_file)
1184 has_cache = os.path.isfile(cached_diff_file)
1176 if not has_cache:
1185 if not has_cache:
1177 return default_struct
1186 return default_struct
1178
1187
1179 data = None
1188 data = None
1180 try:
1189 try:
1181 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1190 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1182 data = pickle.load(f)
1191 data = pickle.load(f)
1183 log.debug('Loaded diff cache from %s', cached_diff_file)
1192 log.debug('Loaded diff cache from %s', cached_diff_file)
1184 except Exception:
1193 except Exception:
1185 log.warn('Failed to read diff cache file', exc_info=True)
1194 log.warn('Failed to read diff cache file', exc_info=True)
1186
1195
1187 if not data:
1196 if not data:
1188 data = default_struct
1197 data = default_struct
1189
1198
1190 if not isinstance(data, dict):
1199 if not isinstance(data, dict):
1191 # old version of data ?
1200 # old version of data ?
1192 data = default_struct
1201 data = default_struct
1193
1202
1194 # check version
1203 # check version
1195 if data.get('version') != CURRENT_DIFF_VERSION:
1204 if data.get('version') != CURRENT_DIFF_VERSION:
1196 # purge cache
1205 # purge cache
1197 _cleanup_cache_file(cached_diff_file)
1206 _cleanup_cache_file(cached_diff_file)
1198 return default_struct
1207 return default_struct
1199
1208
1200 return data
1209 return data
1201
1210
1202
1211
1203 def generate_diff_cache_key(*args):
1212 def generate_diff_cache_key(*args):
1204 """
1213 """
1205 Helper to generate a cache key using arguments
1214 Helper to generate a cache key using arguments
1206 """
1215 """
1207 def arg_mapper(input_param):
1216 def arg_mapper(input_param):
1208 input_param = safe_str(input_param)
1217 input_param = safe_str(input_param)
1209 # we cannot allow '/' in arguments since it would allow
1218 # we cannot allow '/' in arguments since it would allow
1210 # subdirectory usage
1219 # subdirectory usage
1211 input_param.replace('/', '_')
1220 input_param.replace('/', '_')
1212 return input_param or None # prevent empty string arguments
1221 return input_param or None # prevent empty string arguments
1213
1222
1214 return '_'.join([
1223 return '_'.join([
1215 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1224 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1216
1225
1217
1226
1218 def diff_cache_exist(cache_storage, *args):
1227 def diff_cache_exist(cache_storage, *args):
1219 """
1228 """
1220 Based on all generated arguments check and return a cache path
1229 Based on all generated arguments check and return a cache path
1221 """
1230 """
1222 cache_key = generate_diff_cache_key(*args)
1231 cache_key = generate_diff_cache_key(*args)
1223 cache_file_path = os.path.join(cache_storage, cache_key)
1232 cache_file_path = os.path.join(cache_storage, cache_key)
1224 # prevent path traversal attacks using some param that have e.g '../../'
1233 # prevent path traversal attacks using some param that have e.g '../../'
1225 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1234 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1226 raise ValueError('Final path must be within {}'.format(cache_storage))
1235 raise ValueError('Final path must be within {}'.format(cache_storage))
1227
1236
1228 return cache_file_path
1237 return cache_file_path
@@ -1,1731 +1,1739 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext#, _
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
66 # request update.
66 # request update.
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
70
70
71
71
72 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
73
73
74 cls = PullRequest
74 cls = PullRequest
75
75
76 DIFF_CONTEXT = 3
76 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77
77
78 MERGE_STATUS_MESSAGES = {
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
87 'This pull request could not be merged because push to target'
88 ' failed.'),
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
112 'to sub repositories.'),
113 }
113 }
114
114
115 UPDATE_STATUS_MESSAGES = {
115 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
116 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
117 'Pull request update successful.'),
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 'Pull request update failed because of an unknown error.'),
119 'Pull request update failed because of an unknown error.'),
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 'No update needed because the source and target have not changed.'),
121 'No update needed because the source and target have not changed.'),
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 'Pull request cannot be updated because the reference type is '
123 'Pull request cannot be updated because the reference type is '
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 'This pull request cannot be updated because the target '
126 'This pull request cannot be updated because the target '
127 'reference is missing.'),
127 'reference is missing.'),
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 'This pull request cannot be updated because the source '
129 'This pull request cannot be updated because the source '
130 'reference is missing.'),
130 'reference is missing.'),
131 }
131 }
132
132
133 def __get_pull_request(self, pull_request):
133 def __get_pull_request(self, pull_request):
134 return self._get_instance((
134 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
135 PullRequest, PullRequestVersion), pull_request)
136
136
137 def _check_perms(self, perms, pull_request, user, api=False):
137 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
138 if not api:
139 return h.HasRepoPermissionAny(*perms)(
139 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
140 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
141 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
142 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
143 user=user, repo_name=pull_request.target_repo.repo_name)
144
144
145 def check_user_read(self, pull_request, user, api=False):
145 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
147 return self._check_perms(_perms, pull_request, user, api)
148
148
149 def check_user_merge(self, pull_request, user, api=False):
149 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
151 return self._check_perms(_perms, pull_request, user, api)
152
152
153 def check_user_update(self, pull_request, user, api=False):
153 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
154 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
155 return self.check_user_merge(pull_request, user, api) or owner
156
156
157 def check_user_delete(self, pull_request, user):
157 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
158 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
159 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
160 return self._check_perms(_perms, pull_request, user) or owner
161
161
162 def check_user_change_status(self, pull_request, user, api=False):
162 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
163 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
164 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
165 return self.check_user_update(pull_request, user, api) or reviewer
166
166
167 def check_user_comment(self, pull_request, user):
167 def check_user_comment(self, pull_request, user):
168 owner = user.user_id == pull_request.user_id
168 owner = user.user_id == pull_request.user_id
169 return self.check_user_read(pull_request, user) or owner
169 return self.check_user_read(pull_request, user) or owner
170
170
171 def get(self, pull_request):
171 def get(self, pull_request):
172 return self.__get_pull_request(pull_request)
172 return self.__get_pull_request(pull_request)
173
173
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 opened_by=None, order_by=None,
175 opened_by=None, order_by=None,
176 order_dir='desc'):
176 order_dir='desc'):
177 repo = None
177 repo = None
178 if repo_name:
178 if repo_name:
179 repo = self._get_repo(repo_name)
179 repo = self._get_repo(repo_name)
180
180
181 q = PullRequest.query()
181 q = PullRequest.query()
182
182
183 # source or target
183 # source or target
184 if repo and source:
184 if repo and source:
185 q = q.filter(PullRequest.source_repo == repo)
185 q = q.filter(PullRequest.source_repo == repo)
186 elif repo:
186 elif repo:
187 q = q.filter(PullRequest.target_repo == repo)
187 q = q.filter(PullRequest.target_repo == repo)
188
188
189 # closed,opened
189 # closed,opened
190 if statuses:
190 if statuses:
191 q = q.filter(PullRequest.status.in_(statuses))
191 q = q.filter(PullRequest.status.in_(statuses))
192
192
193 # opened by filter
193 # opened by filter
194 if opened_by:
194 if opened_by:
195 q = q.filter(PullRequest.user_id.in_(opened_by))
195 q = q.filter(PullRequest.user_id.in_(opened_by))
196
196
197 if order_by:
197 if order_by:
198 order_map = {
198 order_map = {
199 'name_raw': PullRequest.pull_request_id,
199 'name_raw': PullRequest.pull_request_id,
200 'title': PullRequest.title,
200 'title': PullRequest.title,
201 'updated_on_raw': PullRequest.updated_on,
201 'updated_on_raw': PullRequest.updated_on,
202 'target_repo': PullRequest.target_repo_id
202 'target_repo': PullRequest.target_repo_id
203 }
203 }
204 if order_dir == 'asc':
204 if order_dir == 'asc':
205 q = q.order_by(order_map[order_by].asc())
205 q = q.order_by(order_map[order_by].asc())
206 else:
206 else:
207 q = q.order_by(order_map[order_by].desc())
207 q = q.order_by(order_map[order_by].desc())
208
208
209 return q
209 return q
210
210
211 def count_all(self, repo_name, source=False, statuses=None,
211 def count_all(self, repo_name, source=False, statuses=None,
212 opened_by=None):
212 opened_by=None):
213 """
213 """
214 Count the number of pull requests for a specific repository.
214 Count the number of pull requests for a specific repository.
215
215
216 :param repo_name: target or source repo
216 :param repo_name: target or source repo
217 :param source: boolean flag to specify if repo_name refers to source
217 :param source: boolean flag to specify if repo_name refers to source
218 :param statuses: list of pull request statuses
218 :param statuses: list of pull request statuses
219 :param opened_by: author user of the pull request
219 :param opened_by: author user of the pull request
220 :returns: int number of pull requests
220 :returns: int number of pull requests
221 """
221 """
222 q = self._prepare_get_all_query(
222 q = self._prepare_get_all_query(
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
223 repo_name, source=source, statuses=statuses, opened_by=opened_by)
224
224
225 return q.count()
225 return q.count()
226
226
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 offset=0, length=None, order_by=None, order_dir='desc'):
228 offset=0, length=None, order_by=None, order_dir='desc'):
229 """
229 """
230 Get all pull requests for a specific repository.
230 Get all pull requests for a specific repository.
231
231
232 :param repo_name: target or source repo
232 :param repo_name: target or source repo
233 :param source: boolean flag to specify if repo_name refers to source
233 :param source: boolean flag to specify if repo_name refers to source
234 :param statuses: list of pull request statuses
234 :param statuses: list of pull request statuses
235 :param opened_by: author user of the pull request
235 :param opened_by: author user of the pull request
236 :param offset: pagination offset
236 :param offset: pagination offset
237 :param length: length of returned list
237 :param length: length of returned list
238 :param order_by: order of the returned list
238 :param order_by: order of the returned list
239 :param order_dir: 'asc' or 'desc' ordering direction
239 :param order_dir: 'asc' or 'desc' ordering direction
240 :returns: list of pull requests
240 :returns: list of pull requests
241 """
241 """
242 q = self._prepare_get_all_query(
242 q = self._prepare_get_all_query(
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 order_by=order_by, order_dir=order_dir)
244 order_by=order_by, order_dir=order_dir)
245
245
246 if length:
246 if length:
247 pull_requests = q.limit(length).offset(offset).all()
247 pull_requests = q.limit(length).offset(offset).all()
248 else:
248 else:
249 pull_requests = q.all()
249 pull_requests = q.all()
250
250
251 return pull_requests
251 return pull_requests
252
252
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 opened_by=None):
254 opened_by=None):
255 """
255 """
256 Count the number of pull requests for a specific repository that are
256 Count the number of pull requests for a specific repository that are
257 awaiting review.
257 awaiting review.
258
258
259 :param repo_name: target or source repo
259 :param repo_name: target or source repo
260 :param source: boolean flag to specify if repo_name refers to source
260 :param source: boolean flag to specify if repo_name refers to source
261 :param statuses: list of pull request statuses
261 :param statuses: list of pull request statuses
262 :param opened_by: author user of the pull request
262 :param opened_by: author user of the pull request
263 :returns: int number of pull requests
263 :returns: int number of pull requests
264 """
264 """
265 pull_requests = self.get_awaiting_review(
265 pull_requests = self.get_awaiting_review(
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267
267
268 return len(pull_requests)
268 return len(pull_requests)
269
269
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 opened_by=None, offset=0, length=None,
271 opened_by=None, offset=0, length=None,
272 order_by=None, order_dir='desc'):
272 order_by=None, order_dir='desc'):
273 """
273 """
274 Get all pull requests for a specific repository that are awaiting
274 Get all pull requests for a specific repository that are awaiting
275 review.
275 review.
276
276
277 :param repo_name: target or source repo
277 :param repo_name: target or source repo
278 :param source: boolean flag to specify if repo_name refers to source
278 :param source: boolean flag to specify if repo_name refers to source
279 :param statuses: list of pull request statuses
279 :param statuses: list of pull request statuses
280 :param opened_by: author user of the pull request
280 :param opened_by: author user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _filtered_pull_requests = []
291 _filtered_pull_requests = []
292 for pr in pull_requests:
292 for pr in pull_requests:
293 status = pr.calculated_review_status()
293 status = pr.calculated_review_status()
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 _filtered_pull_requests.append(pr)
296 _filtered_pull_requests.append(pr)
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 opened_by=None, user_id=None):
303 opened_by=None, user_id=None):
304 """
304 """
305 Count the number of pull requests for a specific repository that are
305 Count the number of pull requests for a specific repository that are
306 awaiting review from a specific user.
306 awaiting review from a specific user.
307
307
308 :param repo_name: target or source repo
308 :param repo_name: target or source repo
309 :param source: boolean flag to specify if repo_name refers to source
309 :param source: boolean flag to specify if repo_name refers to source
310 :param statuses: list of pull request statuses
310 :param statuses: list of pull request statuses
311 :param opened_by: author user of the pull request
311 :param opened_by: author user of the pull request
312 :param user_id: reviewer user of the pull request
312 :param user_id: reviewer user of the pull request
313 :returns: int number of pull requests
313 :returns: int number of pull requests
314 """
314 """
315 pull_requests = self.get_awaiting_my_review(
315 pull_requests = self.get_awaiting_my_review(
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 user_id=user_id)
317 user_id=user_id)
318
318
319 return len(pull_requests)
319 return len(pull_requests)
320
320
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 opened_by=None, user_id=None, offset=0,
322 opened_by=None, user_id=None, offset=0,
323 length=None, order_by=None, order_dir='desc'):
323 length=None, order_by=None, order_dir='desc'):
324 """
324 """
325 Get all pull requests for a specific repository that are awaiting
325 Get all pull requests for a specific repository that are awaiting
326 review from a specific user.
326 review from a specific user.
327
327
328 :param repo_name: target or source repo
328 :param repo_name: target or source repo
329 :param source: boolean flag to specify if repo_name refers to source
329 :param source: boolean flag to specify if repo_name refers to source
330 :param statuses: list of pull request statuses
330 :param statuses: list of pull request statuses
331 :param opened_by: author user of the pull request
331 :param opened_by: author user of the pull request
332 :param user_id: reviewer user of the pull request
332 :param user_id: reviewer user of the pull request
333 :param offset: pagination offset
333 :param offset: pagination offset
334 :param length: length of returned list
334 :param length: length of returned list
335 :param order_by: order of the returned list
335 :param order_by: order of the returned list
336 :param order_dir: 'asc' or 'desc' ordering direction
336 :param order_dir: 'asc' or 'desc' ordering direction
337 :returns: list of pull requests
337 :returns: list of pull requests
338 """
338 """
339 pull_requests = self.get_all(
339 pull_requests = self.get_all(
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 order_by=order_by, order_dir=order_dir)
341 order_by=order_by, order_dir=order_dir)
342
342
343 _my = PullRequestModel().get_not_reviewed(user_id)
343 _my = PullRequestModel().get_not_reviewed(user_id)
344 my_participation = []
344 my_participation = []
345 for pr in pull_requests:
345 for pr in pull_requests:
346 if pr in _my:
346 if pr in _my:
347 my_participation.append(pr)
347 my_participation.append(pr)
348 _filtered_pull_requests = my_participation
348 _filtered_pull_requests = my_participation
349 if length:
349 if length:
350 return _filtered_pull_requests[offset:offset+length]
350 return _filtered_pull_requests[offset:offset+length]
351 else:
351 else:
352 return _filtered_pull_requests
352 return _filtered_pull_requests
353
353
354 def get_not_reviewed(self, user_id):
354 def get_not_reviewed(self, user_id):
355 return [
355 return [
356 x.pull_request for x in PullRequestReviewers.query().filter(
356 x.pull_request for x in PullRequestReviewers.query().filter(
357 PullRequestReviewers.user_id == user_id).all()
357 PullRequestReviewers.user_id == user_id).all()
358 ]
358 ]
359
359
360 def _prepare_participating_query(self, user_id=None, statuses=None,
360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 order_by=None, order_dir='desc'):
361 order_by=None, order_dir='desc'):
362 q = PullRequest.query()
362 q = PullRequest.query()
363 if user_id:
363 if user_id:
364 reviewers_subquery = Session().query(
364 reviewers_subquery = Session().query(
365 PullRequestReviewers.pull_request_id).filter(
365 PullRequestReviewers.pull_request_id).filter(
366 PullRequestReviewers.user_id == user_id).subquery()
366 PullRequestReviewers.user_id == user_id).subquery()
367 user_filter = or_(
367 user_filter = or_(
368 PullRequest.user_id == user_id,
368 PullRequest.user_id == user_id,
369 PullRequest.pull_request_id.in_(reviewers_subquery)
369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 )
370 )
371 q = PullRequest.query().filter(user_filter)
371 q = PullRequest.query().filter(user_filter)
372
372
373 # closed,opened
373 # closed,opened
374 if statuses:
374 if statuses:
375 q = q.filter(PullRequest.status.in_(statuses))
375 q = q.filter(PullRequest.status.in_(statuses))
376
376
377 if order_by:
377 if order_by:
378 order_map = {
378 order_map = {
379 'name_raw': PullRequest.pull_request_id,
379 'name_raw': PullRequest.pull_request_id,
380 'title': PullRequest.title,
380 'title': PullRequest.title,
381 'updated_on_raw': PullRequest.updated_on,
381 'updated_on_raw': PullRequest.updated_on,
382 'target_repo': PullRequest.target_repo_id
382 'target_repo': PullRequest.target_repo_id
383 }
383 }
384 if order_dir == 'asc':
384 if order_dir == 'asc':
385 q = q.order_by(order_map[order_by].asc())
385 q = q.order_by(order_map[order_by].asc())
386 else:
386 else:
387 q = q.order_by(order_map[order_by].desc())
387 q = q.order_by(order_map[order_by].desc())
388
388
389 return q
389 return q
390
390
391 def count_im_participating_in(self, user_id=None, statuses=None):
391 def count_im_participating_in(self, user_id=None, statuses=None):
392 q = self._prepare_participating_query(user_id, statuses=statuses)
392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 return q.count()
393 return q.count()
394
394
395 def get_im_participating_in(
395 def get_im_participating_in(
396 self, user_id=None, statuses=None, offset=0,
396 self, user_id=None, statuses=None, offset=0,
397 length=None, order_by=None, order_dir='desc'):
397 length=None, order_by=None, order_dir='desc'):
398 """
398 """
399 Get all Pull requests that i'm participating in, or i have opened
399 Get all Pull requests that i'm participating in, or i have opened
400 """
400 """
401
401
402 q = self._prepare_participating_query(
402 q = self._prepare_participating_query(
403 user_id, statuses=statuses, order_by=order_by,
403 user_id, statuses=statuses, order_by=order_by,
404 order_dir=order_dir)
404 order_dir=order_dir)
405
405
406 if length:
406 if length:
407 pull_requests = q.limit(length).offset(offset).all()
407 pull_requests = q.limit(length).offset(offset).all()
408 else:
408 else:
409 pull_requests = q.all()
409 pull_requests = q.all()
410
410
411 return pull_requests
411 return pull_requests
412
412
413 def get_versions(self, pull_request):
413 def get_versions(self, pull_request):
414 """
414 """
415 returns version of pull request sorted by ID descending
415 returns version of pull request sorted by ID descending
416 """
416 """
417 return PullRequestVersion.query()\
417 return PullRequestVersion.query()\
418 .filter(PullRequestVersion.pull_request == pull_request)\
418 .filter(PullRequestVersion.pull_request == pull_request)\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 .all()
420 .all()
421
421
422 def get_pr_version(self, pull_request_id, version=None):
422 def get_pr_version(self, pull_request_id, version=None):
423 at_version = None
423 at_version = None
424
424
425 if version and version == 'latest':
425 if version and version == 'latest':
426 pull_request_ver = PullRequest.get(pull_request_id)
426 pull_request_ver = PullRequest.get(pull_request_id)
427 pull_request_obj = pull_request_ver
427 pull_request_obj = pull_request_ver
428 _org_pull_request_obj = pull_request_obj
428 _org_pull_request_obj = pull_request_obj
429 at_version = 'latest'
429 at_version = 'latest'
430 elif version:
430 elif version:
431 pull_request_ver = PullRequestVersion.get_or_404(version)
431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 pull_request_obj = pull_request_ver
432 pull_request_obj = pull_request_ver
433 _org_pull_request_obj = pull_request_ver.pull_request
433 _org_pull_request_obj = pull_request_ver.pull_request
434 at_version = pull_request_ver.pull_request_version_id
434 at_version = pull_request_ver.pull_request_version_id
435 else:
435 else:
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 pull_request_id)
437 pull_request_id)
438
438
439 pull_request_display_obj = PullRequest.get_pr_display_object(
439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 pull_request_obj, _org_pull_request_obj)
440 pull_request_obj, _org_pull_request_obj)
441
441
442 return _org_pull_request_obj, pull_request_obj, \
442 return _org_pull_request_obj, pull_request_obj, \
443 pull_request_display_obj, at_version
443 pull_request_display_obj, at_version
444
444
445 def create(self, created_by, source_repo, source_ref, target_repo,
445 def create(self, created_by, source_repo, source_ref, target_repo,
446 target_ref, revisions, reviewers, title, description=None,
446 target_ref, revisions, reviewers, title, description=None,
447 description_renderer=None,
447 description_renderer=None,
448 reviewer_data=None, translator=None, auth_user=None):
448 reviewer_data=None, translator=None, auth_user=None):
449 translator = translator or get_current_request().translate
449 translator = translator or get_current_request().translate
450
450
451 created_by_user = self._get_user(created_by)
451 created_by_user = self._get_user(created_by)
452 auth_user = auth_user or created_by_user.AuthUser()
452 auth_user = auth_user or created_by_user.AuthUser()
453 source_repo = self._get_repo(source_repo)
453 source_repo = self._get_repo(source_repo)
454 target_repo = self._get_repo(target_repo)
454 target_repo = self._get_repo(target_repo)
455
455
456 pull_request = PullRequest()
456 pull_request = PullRequest()
457 pull_request.source_repo = source_repo
457 pull_request.source_repo = source_repo
458 pull_request.source_ref = source_ref
458 pull_request.source_ref = source_ref
459 pull_request.target_repo = target_repo
459 pull_request.target_repo = target_repo
460 pull_request.target_ref = target_ref
460 pull_request.target_ref = target_ref
461 pull_request.revisions = revisions
461 pull_request.revisions = revisions
462 pull_request.title = title
462 pull_request.title = title
463 pull_request.description = description
463 pull_request.description = description
464 pull_request.description_renderer = description_renderer
464 pull_request.description_renderer = description_renderer
465 pull_request.author = created_by_user
465 pull_request.author = created_by_user
466 pull_request.reviewer_data = reviewer_data
466 pull_request.reviewer_data = reviewer_data
467
467
468 Session().add(pull_request)
468 Session().add(pull_request)
469 Session().flush()
469 Session().flush()
470
470
471 reviewer_ids = set()
471 reviewer_ids = set()
472 # members / reviewers
472 # members / reviewers
473 for reviewer_object in reviewers:
473 for reviewer_object in reviewers:
474 user_id, reasons, mandatory, rules = reviewer_object
474 user_id, reasons, mandatory, rules = reviewer_object
475 user = self._get_user(user_id)
475 user = self._get_user(user_id)
476
476
477 # skip duplicates
477 # skip duplicates
478 if user.user_id in reviewer_ids:
478 if user.user_id in reviewer_ids:
479 continue
479 continue
480
480
481 reviewer_ids.add(user.user_id)
481 reviewer_ids.add(user.user_id)
482
482
483 reviewer = PullRequestReviewers()
483 reviewer = PullRequestReviewers()
484 reviewer.user = user
484 reviewer.user = user
485 reviewer.pull_request = pull_request
485 reviewer.pull_request = pull_request
486 reviewer.reasons = reasons
486 reviewer.reasons = reasons
487 reviewer.mandatory = mandatory
487 reviewer.mandatory = mandatory
488
488
489 # NOTE(marcink): pick only first rule for now
489 # NOTE(marcink): pick only first rule for now
490 rule_id = list(rules)[0] if rules else None
490 rule_id = list(rules)[0] if rules else None
491 rule = RepoReviewRule.get(rule_id) if rule_id else None
491 rule = RepoReviewRule.get(rule_id) if rule_id else None
492 if rule:
492 if rule:
493 review_group = rule.user_group_vote_rule(user_id)
493 review_group = rule.user_group_vote_rule(user_id)
494 # we check if this particular reviewer is member of a voting group
494 # we check if this particular reviewer is member of a voting group
495 if review_group:
495 if review_group:
496 # NOTE(marcink):
496 # NOTE(marcink):
497 # can be that user is member of more but we pick the first same,
497 # can be that user is member of more but we pick the first same,
498 # same as default reviewers algo
498 # same as default reviewers algo
499 review_group = review_group[0]
499 review_group = review_group[0]
500
500
501 rule_data = {
501 rule_data = {
502 'rule_name':
502 'rule_name':
503 rule.review_rule_name,
503 rule.review_rule_name,
504 'rule_user_group_entry_id':
504 'rule_user_group_entry_id':
505 review_group.repo_review_rule_users_group_id,
505 review_group.repo_review_rule_users_group_id,
506 'rule_user_group_name':
506 'rule_user_group_name':
507 review_group.users_group.users_group_name,
507 review_group.users_group.users_group_name,
508 'rule_user_group_members':
508 'rule_user_group_members':
509 [x.user.username for x in review_group.users_group.members],
509 [x.user.username for x in review_group.users_group.members],
510 'rule_user_group_members_id':
510 'rule_user_group_members_id':
511 [x.user.user_id for x in review_group.users_group.members],
511 [x.user.user_id for x in review_group.users_group.members],
512 }
512 }
513 # e.g {'vote_rule': -1, 'mandatory': True}
513 # e.g {'vote_rule': -1, 'mandatory': True}
514 rule_data.update(review_group.rule_data())
514 rule_data.update(review_group.rule_data())
515
515
516 reviewer.rule_data = rule_data
516 reviewer.rule_data = rule_data
517
517
518 Session().add(reviewer)
518 Session().add(reviewer)
519 Session().flush()
519 Session().flush()
520
520
521 # Set approval status to "Under Review" for all commits which are
521 # Set approval status to "Under Review" for all commits which are
522 # part of this pull request.
522 # part of this pull request.
523 ChangesetStatusModel().set_status(
523 ChangesetStatusModel().set_status(
524 repo=target_repo,
524 repo=target_repo,
525 status=ChangesetStatus.STATUS_UNDER_REVIEW,
525 status=ChangesetStatus.STATUS_UNDER_REVIEW,
526 user=created_by_user,
526 user=created_by_user,
527 pull_request=pull_request
527 pull_request=pull_request
528 )
528 )
529 # we commit early at this point. This has to do with a fact
529 # we commit early at this point. This has to do with a fact
530 # that before queries do some row-locking. And because of that
530 # that before queries do some row-locking. And because of that
531 # we need to commit and finish transation before below validate call
531 # we need to commit and finish transation before below validate call
532 # that for large repos could be long resulting in long row locks
532 # that for large repos could be long resulting in long row locks
533 Session().commit()
533 Session().commit()
534
534
535 # prepare workspace, and run initial merge simulation
535 # prepare workspace, and run initial merge simulation
536 MergeCheck.validate(
536 MergeCheck.validate(
537 pull_request, auth_user=auth_user, translator=translator)
537 pull_request, auth_user=auth_user, translator=translator)
538
538
539 self.notify_reviewers(pull_request, reviewer_ids)
539 self.notify_reviewers(pull_request, reviewer_ids)
540 self._trigger_pull_request_hook(
540 self._trigger_pull_request_hook(
541 pull_request, created_by_user, 'create')
541 pull_request, created_by_user, 'create')
542
542
543 creation_data = pull_request.get_api_data(with_merge_state=False)
543 creation_data = pull_request.get_api_data(with_merge_state=False)
544 self._log_audit_action(
544 self._log_audit_action(
545 'repo.pull_request.create', {'data': creation_data},
545 'repo.pull_request.create', {'data': creation_data},
546 auth_user, pull_request)
546 auth_user, pull_request)
547
547
548 return pull_request
548 return pull_request
549
549
550 def _trigger_pull_request_hook(self, pull_request, user, action):
550 def _trigger_pull_request_hook(self, pull_request, user, action):
551 pull_request = self.__get_pull_request(pull_request)
551 pull_request = self.__get_pull_request(pull_request)
552 target_scm = pull_request.target_repo.scm_instance()
552 target_scm = pull_request.target_repo.scm_instance()
553 if action == 'create':
553 if action == 'create':
554 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
554 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
555 elif action == 'merge':
555 elif action == 'merge':
556 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
556 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
557 elif action == 'close':
557 elif action == 'close':
558 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
558 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
559 elif action == 'review_status_change':
559 elif action == 'review_status_change':
560 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
560 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
561 elif action == 'update':
561 elif action == 'update':
562 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
562 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
563 else:
563 else:
564 return
564 return
565
565
566 trigger_hook(
566 trigger_hook(
567 username=user.username,
567 username=user.username,
568 repo_name=pull_request.target_repo.repo_name,
568 repo_name=pull_request.target_repo.repo_name,
569 repo_alias=target_scm.alias,
569 repo_alias=target_scm.alias,
570 pull_request=pull_request)
570 pull_request=pull_request)
571
571
572 def _get_commit_ids(self, pull_request):
572 def _get_commit_ids(self, pull_request):
573 """
573 """
574 Return the commit ids of the merged pull request.
574 Return the commit ids of the merged pull request.
575
575
576 This method is not dealing correctly yet with the lack of autoupdates
576 This method is not dealing correctly yet with the lack of autoupdates
577 nor with the implicit target updates.
577 nor with the implicit target updates.
578 For example: if a commit in the source repo is already in the target it
578 For example: if a commit in the source repo is already in the target it
579 will be reported anyways.
579 will be reported anyways.
580 """
580 """
581 merge_rev = pull_request.merge_rev
581 merge_rev = pull_request.merge_rev
582 if merge_rev is None:
582 if merge_rev is None:
583 raise ValueError('This pull request was not merged yet')
583 raise ValueError('This pull request was not merged yet')
584
584
585 commit_ids = list(pull_request.revisions)
585 commit_ids = list(pull_request.revisions)
586 if merge_rev not in commit_ids:
586 if merge_rev not in commit_ids:
587 commit_ids.append(merge_rev)
587 commit_ids.append(merge_rev)
588
588
589 return commit_ids
589 return commit_ids
590
590
591 def merge_repo(self, pull_request, user, extras):
591 def merge_repo(self, pull_request, user, extras):
592 log.debug("Merging pull request %s", pull_request.pull_request_id)
592 log.debug("Merging pull request %s", pull_request.pull_request_id)
593 extras['user_agent'] = 'internal-merge'
593 extras['user_agent'] = 'internal-merge'
594 merge_state = self._merge_pull_request(pull_request, user, extras)
594 merge_state = self._merge_pull_request(pull_request, user, extras)
595 if merge_state.executed:
595 if merge_state.executed:
596 log.debug(
596 log.debug(
597 "Merge was successful, updating the pull request comments.")
597 "Merge was successful, updating the pull request comments.")
598 self._comment_and_close_pr(pull_request, user, merge_state)
598 self._comment_and_close_pr(pull_request, user, merge_state)
599
599
600 self._log_audit_action(
600 self._log_audit_action(
601 'repo.pull_request.merge',
601 'repo.pull_request.merge',
602 {'merge_state': merge_state.__dict__},
602 {'merge_state': merge_state.__dict__},
603 user, pull_request)
603 user, pull_request)
604
604
605 else:
605 else:
606 log.warn("Merge failed, not updating the pull request.")
606 log.warn("Merge failed, not updating the pull request.")
607 return merge_state
607 return merge_state
608
608
609 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
609 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
610 target_vcs = pull_request.target_repo.scm_instance()
610 target_vcs = pull_request.target_repo.scm_instance()
611 source_vcs = pull_request.source_repo.scm_instance()
611 source_vcs = pull_request.source_repo.scm_instance()
612
612
613 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
613 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
614 pr_id=pull_request.pull_request_id,
614 pr_id=pull_request.pull_request_id,
615 pr_title=pull_request.title,
615 pr_title=pull_request.title,
616 source_repo=source_vcs.name,
616 source_repo=source_vcs.name,
617 source_ref_name=pull_request.source_ref_parts.name,
617 source_ref_name=pull_request.source_ref_parts.name,
618 target_repo=target_vcs.name,
618 target_repo=target_vcs.name,
619 target_ref_name=pull_request.target_ref_parts.name,
619 target_ref_name=pull_request.target_ref_parts.name,
620 )
620 )
621
621
622 workspace_id = self._workspace_id(pull_request)
622 workspace_id = self._workspace_id(pull_request)
623 repo_id = pull_request.target_repo.repo_id
623 repo_id = pull_request.target_repo.repo_id
624 use_rebase = self._use_rebase_for_merging(pull_request)
624 use_rebase = self._use_rebase_for_merging(pull_request)
625 close_branch = self._close_branch_before_merging(pull_request)
625 close_branch = self._close_branch_before_merging(pull_request)
626
626
627 target_ref = self._refresh_reference(
627 target_ref = self._refresh_reference(
628 pull_request.target_ref_parts, target_vcs)
628 pull_request.target_ref_parts, target_vcs)
629
629
630 callback_daemon, extras = prepare_callback_daemon(
630 callback_daemon, extras = prepare_callback_daemon(
631 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
631 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
632 host=vcs_settings.HOOKS_HOST,
632 host=vcs_settings.HOOKS_HOST,
633 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
633 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
634
634
635 with callback_daemon:
635 with callback_daemon:
636 # TODO: johbo: Implement a clean way to run a config_override
636 # TODO: johbo: Implement a clean way to run a config_override
637 # for a single call.
637 # for a single call.
638 target_vcs.config.set(
638 target_vcs.config.set(
639 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
639 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
640
640
641 user_name = user.short_contact
641 user_name = user.short_contact
642 merge_state = target_vcs.merge(
642 merge_state = target_vcs.merge(
643 repo_id, workspace_id, target_ref, source_vcs,
643 repo_id, workspace_id, target_ref, source_vcs,
644 pull_request.source_ref_parts,
644 pull_request.source_ref_parts,
645 user_name=user_name, user_email=user.email,
645 user_name=user_name, user_email=user.email,
646 message=message, use_rebase=use_rebase,
646 message=message, use_rebase=use_rebase,
647 close_branch=close_branch)
647 close_branch=close_branch)
648 return merge_state
648 return merge_state
649
649
650 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
650 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
651 pull_request.merge_rev = merge_state.merge_ref.commit_id
651 pull_request.merge_rev = merge_state.merge_ref.commit_id
652 pull_request.updated_on = datetime.datetime.now()
652 pull_request.updated_on = datetime.datetime.now()
653 close_msg = close_msg or 'Pull request merged and closed'
653 close_msg = close_msg or 'Pull request merged and closed'
654
654
655 CommentsModel().create(
655 CommentsModel().create(
656 text=safe_unicode(close_msg),
656 text=safe_unicode(close_msg),
657 repo=pull_request.target_repo.repo_id,
657 repo=pull_request.target_repo.repo_id,
658 user=user.user_id,
658 user=user.user_id,
659 pull_request=pull_request.pull_request_id,
659 pull_request=pull_request.pull_request_id,
660 f_path=None,
660 f_path=None,
661 line_no=None,
661 line_no=None,
662 closing_pr=True
662 closing_pr=True
663 )
663 )
664
664
665 Session().add(pull_request)
665 Session().add(pull_request)
666 Session().flush()
666 Session().flush()
667 # TODO: paris: replace invalidation with less radical solution
667 # TODO: paris: replace invalidation with less radical solution
668 ScmModel().mark_for_invalidation(
668 ScmModel().mark_for_invalidation(
669 pull_request.target_repo.repo_name)
669 pull_request.target_repo.repo_name)
670 self._trigger_pull_request_hook(pull_request, user, 'merge')
670 self._trigger_pull_request_hook(pull_request, user, 'merge')
671
671
672 def has_valid_update_type(self, pull_request):
672 def has_valid_update_type(self, pull_request):
673 source_ref_type = pull_request.source_ref_parts.type
673 source_ref_type = pull_request.source_ref_parts.type
674 return source_ref_type in ['book', 'branch', 'tag']
674 return source_ref_type in ['book', 'branch', 'tag']
675
675
676 def update_commits(self, pull_request):
676 def update_commits(self, pull_request):
677 """
677 """
678 Get the updated list of commits for the pull request
678 Get the updated list of commits for the pull request
679 and return the new pull request version and the list
679 and return the new pull request version and the list
680 of commits processed by this update action
680 of commits processed by this update action
681 """
681 """
682 pull_request = self.__get_pull_request(pull_request)
682 pull_request = self.__get_pull_request(pull_request)
683 source_ref_type = pull_request.source_ref_parts.type
683 source_ref_type = pull_request.source_ref_parts.type
684 source_ref_name = pull_request.source_ref_parts.name
684 source_ref_name = pull_request.source_ref_parts.name
685 source_ref_id = pull_request.source_ref_parts.commit_id
685 source_ref_id = pull_request.source_ref_parts.commit_id
686
686
687 target_ref_type = pull_request.target_ref_parts.type
687 target_ref_type = pull_request.target_ref_parts.type
688 target_ref_name = pull_request.target_ref_parts.name
688 target_ref_name = pull_request.target_ref_parts.name
689 target_ref_id = pull_request.target_ref_parts.commit_id
689 target_ref_id = pull_request.target_ref_parts.commit_id
690
690
691 if not self.has_valid_update_type(pull_request):
691 if not self.has_valid_update_type(pull_request):
692 log.debug(
692 log.debug(
693 "Skipping update of pull request %s due to ref type: %s",
693 "Skipping update of pull request %s due to ref type: %s",
694 pull_request, source_ref_type)
694 pull_request, source_ref_type)
695 return UpdateResponse(
695 return UpdateResponse(
696 executed=False,
696 executed=False,
697 reason=UpdateFailureReason.WRONG_REF_TYPE,
697 reason=UpdateFailureReason.WRONG_REF_TYPE,
698 old=pull_request, new=None, changes=None,
698 old=pull_request, new=None, changes=None,
699 source_changed=False, target_changed=False)
699 source_changed=False, target_changed=False)
700
700
701 # source repo
701 # source repo
702 source_repo = pull_request.source_repo.scm_instance()
702 source_repo = pull_request.source_repo.scm_instance()
703 try:
703 try:
704 source_commit = source_repo.get_commit(commit_id=source_ref_name)
704 source_commit = source_repo.get_commit(commit_id=source_ref_name)
705 except CommitDoesNotExistError:
705 except CommitDoesNotExistError:
706 return UpdateResponse(
706 return UpdateResponse(
707 executed=False,
707 executed=False,
708 reason=UpdateFailureReason.MISSING_SOURCE_REF,
708 reason=UpdateFailureReason.MISSING_SOURCE_REF,
709 old=pull_request, new=None, changes=None,
709 old=pull_request, new=None, changes=None,
710 source_changed=False, target_changed=False)
710 source_changed=False, target_changed=False)
711
711
712 source_changed = source_ref_id != source_commit.raw_id
712 source_changed = source_ref_id != source_commit.raw_id
713
713
714 # target repo
714 # target repo
715 target_repo = pull_request.target_repo.scm_instance()
715 target_repo = pull_request.target_repo.scm_instance()
716 try:
716 try:
717 target_commit = target_repo.get_commit(commit_id=target_ref_name)
717 target_commit = target_repo.get_commit(commit_id=target_ref_name)
718 except CommitDoesNotExistError:
718 except CommitDoesNotExistError:
719 return UpdateResponse(
719 return UpdateResponse(
720 executed=False,
720 executed=False,
721 reason=UpdateFailureReason.MISSING_TARGET_REF,
721 reason=UpdateFailureReason.MISSING_TARGET_REF,
722 old=pull_request, new=None, changes=None,
722 old=pull_request, new=None, changes=None,
723 source_changed=False, target_changed=False)
723 source_changed=False, target_changed=False)
724 target_changed = target_ref_id != target_commit.raw_id
724 target_changed = target_ref_id != target_commit.raw_id
725
725
726 if not (source_changed or target_changed):
726 if not (source_changed or target_changed):
727 log.debug("Nothing changed in pull request %s", pull_request)
727 log.debug("Nothing changed in pull request %s", pull_request)
728 return UpdateResponse(
728 return UpdateResponse(
729 executed=False,
729 executed=False,
730 reason=UpdateFailureReason.NO_CHANGE,
730 reason=UpdateFailureReason.NO_CHANGE,
731 old=pull_request, new=None, changes=None,
731 old=pull_request, new=None, changes=None,
732 source_changed=target_changed, target_changed=source_changed)
732 source_changed=target_changed, target_changed=source_changed)
733
733
734 change_in_found = 'target repo' if target_changed else 'source repo'
734 change_in_found = 'target repo' if target_changed else 'source repo'
735 log.debug('Updating pull request because of change in %s detected',
735 log.debug('Updating pull request because of change in %s detected',
736 change_in_found)
736 change_in_found)
737
737
738 # Finally there is a need for an update, in case of source change
738 # Finally there is a need for an update, in case of source change
739 # we create a new version, else just an update
739 # we create a new version, else just an update
740 if source_changed:
740 if source_changed:
741 pull_request_version = self._create_version_from_snapshot(pull_request)
741 pull_request_version = self._create_version_from_snapshot(pull_request)
742 self._link_comments_to_version(pull_request_version)
742 self._link_comments_to_version(pull_request_version)
743 else:
743 else:
744 try:
744 try:
745 ver = pull_request.versions[-1]
745 ver = pull_request.versions[-1]
746 except IndexError:
746 except IndexError:
747 ver = None
747 ver = None
748
748
749 pull_request.pull_request_version_id = \
749 pull_request.pull_request_version_id = \
750 ver.pull_request_version_id if ver else None
750 ver.pull_request_version_id if ver else None
751 pull_request_version = pull_request
751 pull_request_version = pull_request
752
752
753 try:
753 try:
754 if target_ref_type in ('tag', 'branch', 'book'):
754 if target_ref_type in ('tag', 'branch', 'book'):
755 target_commit = target_repo.get_commit(target_ref_name)
755 target_commit = target_repo.get_commit(target_ref_name)
756 else:
756 else:
757 target_commit = target_repo.get_commit(target_ref_id)
757 target_commit = target_repo.get_commit(target_ref_id)
758 except CommitDoesNotExistError:
758 except CommitDoesNotExistError:
759 return UpdateResponse(
759 return UpdateResponse(
760 executed=False,
760 executed=False,
761 reason=UpdateFailureReason.MISSING_TARGET_REF,
761 reason=UpdateFailureReason.MISSING_TARGET_REF,
762 old=pull_request, new=None, changes=None,
762 old=pull_request, new=None, changes=None,
763 source_changed=source_changed, target_changed=target_changed)
763 source_changed=source_changed, target_changed=target_changed)
764
764
765 # re-compute commit ids
765 # re-compute commit ids
766 old_commit_ids = pull_request.revisions
766 old_commit_ids = pull_request.revisions
767 pre_load = ["author", "branch", "date", "message"]
767 pre_load = ["author", "branch", "date", "message"]
768 commit_ranges = target_repo.compare(
768 commit_ranges = target_repo.compare(
769 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
769 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
770 pre_load=pre_load)
770 pre_load=pre_load)
771
771
772 ancestor = target_repo.get_common_ancestor(
772 ancestor = target_repo.get_common_ancestor(
773 target_commit.raw_id, source_commit.raw_id, source_repo)
773 target_commit.raw_id, source_commit.raw_id, source_repo)
774
774
775 pull_request.source_ref = '%s:%s:%s' % (
775 pull_request.source_ref = '%s:%s:%s' % (
776 source_ref_type, source_ref_name, source_commit.raw_id)
776 source_ref_type, source_ref_name, source_commit.raw_id)
777 pull_request.target_ref = '%s:%s:%s' % (
777 pull_request.target_ref = '%s:%s:%s' % (
778 target_ref_type, target_ref_name, ancestor)
778 target_ref_type, target_ref_name, ancestor)
779
779
780 pull_request.revisions = [
780 pull_request.revisions = [
781 commit.raw_id for commit in reversed(commit_ranges)]
781 commit.raw_id for commit in reversed(commit_ranges)]
782 pull_request.updated_on = datetime.datetime.now()
782 pull_request.updated_on = datetime.datetime.now()
783 Session().add(pull_request)
783 Session().add(pull_request)
784 new_commit_ids = pull_request.revisions
784 new_commit_ids = pull_request.revisions
785
785
786 old_diff_data, new_diff_data = self._generate_update_diffs(
786 old_diff_data, new_diff_data = self._generate_update_diffs(
787 pull_request, pull_request_version)
787 pull_request, pull_request_version)
788
788
789 # calculate commit and file changes
789 # calculate commit and file changes
790 changes = self._calculate_commit_id_changes(
790 changes = self._calculate_commit_id_changes(
791 old_commit_ids, new_commit_ids)
791 old_commit_ids, new_commit_ids)
792 file_changes = self._calculate_file_changes(
792 file_changes = self._calculate_file_changes(
793 old_diff_data, new_diff_data)
793 old_diff_data, new_diff_data)
794
794
795 # set comments as outdated if DIFFS changed
795 # set comments as outdated if DIFFS changed
796 CommentsModel().outdate_comments(
796 CommentsModel().outdate_comments(
797 pull_request, old_diff_data=old_diff_data,
797 pull_request, old_diff_data=old_diff_data,
798 new_diff_data=new_diff_data)
798 new_diff_data=new_diff_data)
799
799
800 commit_changes = (changes.added or changes.removed)
800 commit_changes = (changes.added or changes.removed)
801 file_node_changes = (
801 file_node_changes = (
802 file_changes.added or file_changes.modified or file_changes.removed)
802 file_changes.added or file_changes.modified or file_changes.removed)
803 pr_has_changes = commit_changes or file_node_changes
803 pr_has_changes = commit_changes or file_node_changes
804
804
805 # Add an automatic comment to the pull request, in case
805 # Add an automatic comment to the pull request, in case
806 # anything has changed
806 # anything has changed
807 if pr_has_changes:
807 if pr_has_changes:
808 update_comment = CommentsModel().create(
808 update_comment = CommentsModel().create(
809 text=self._render_update_message(changes, file_changes),
809 text=self._render_update_message(changes, file_changes),
810 repo=pull_request.target_repo,
810 repo=pull_request.target_repo,
811 user=pull_request.author,
811 user=pull_request.author,
812 pull_request=pull_request,
812 pull_request=pull_request,
813 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
813 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
814
814
815 # Update status to "Under Review" for added commits
815 # Update status to "Under Review" for added commits
816 for commit_id in changes.added:
816 for commit_id in changes.added:
817 ChangesetStatusModel().set_status(
817 ChangesetStatusModel().set_status(
818 repo=pull_request.source_repo,
818 repo=pull_request.source_repo,
819 status=ChangesetStatus.STATUS_UNDER_REVIEW,
819 status=ChangesetStatus.STATUS_UNDER_REVIEW,
820 comment=update_comment,
820 comment=update_comment,
821 user=pull_request.author,
821 user=pull_request.author,
822 pull_request=pull_request,
822 pull_request=pull_request,
823 revision=commit_id)
823 revision=commit_id)
824
824
825 log.debug(
825 log.debug(
826 'Updated pull request %s, added_ids: %s, common_ids: %s, '
826 'Updated pull request %s, added_ids: %s, common_ids: %s, '
827 'removed_ids: %s', pull_request.pull_request_id,
827 'removed_ids: %s', pull_request.pull_request_id,
828 changes.added, changes.common, changes.removed)
828 changes.added, changes.common, changes.removed)
829 log.debug(
829 log.debug(
830 'Updated pull request with the following file changes: %s',
830 'Updated pull request with the following file changes: %s',
831 file_changes)
831 file_changes)
832
832
833 log.info(
833 log.info(
834 "Updated pull request %s from commit %s to commit %s, "
834 "Updated pull request %s from commit %s to commit %s, "
835 "stored new version %s of this pull request.",
835 "stored new version %s of this pull request.",
836 pull_request.pull_request_id, source_ref_id,
836 pull_request.pull_request_id, source_ref_id,
837 pull_request.source_ref_parts.commit_id,
837 pull_request.source_ref_parts.commit_id,
838 pull_request_version.pull_request_version_id)
838 pull_request_version.pull_request_version_id)
839 Session().commit()
839 Session().commit()
840 self._trigger_pull_request_hook(
840 self._trigger_pull_request_hook(
841 pull_request, pull_request.author, 'update')
841 pull_request, pull_request.author, 'update')
842
842
843 return UpdateResponse(
843 return UpdateResponse(
844 executed=True, reason=UpdateFailureReason.NONE,
844 executed=True, reason=UpdateFailureReason.NONE,
845 old=pull_request, new=pull_request_version, changes=changes,
845 old=pull_request, new=pull_request_version, changes=changes,
846 source_changed=source_changed, target_changed=target_changed)
846 source_changed=source_changed, target_changed=target_changed)
847
847
848 def _create_version_from_snapshot(self, pull_request):
848 def _create_version_from_snapshot(self, pull_request):
849 version = PullRequestVersion()
849 version = PullRequestVersion()
850 version.title = pull_request.title
850 version.title = pull_request.title
851 version.description = pull_request.description
851 version.description = pull_request.description
852 version.status = pull_request.status
852 version.status = pull_request.status
853 version.created_on = datetime.datetime.now()
853 version.created_on = datetime.datetime.now()
854 version.updated_on = pull_request.updated_on
854 version.updated_on = pull_request.updated_on
855 version.user_id = pull_request.user_id
855 version.user_id = pull_request.user_id
856 version.source_repo = pull_request.source_repo
856 version.source_repo = pull_request.source_repo
857 version.source_ref = pull_request.source_ref
857 version.source_ref = pull_request.source_ref
858 version.target_repo = pull_request.target_repo
858 version.target_repo = pull_request.target_repo
859 version.target_ref = pull_request.target_ref
859 version.target_ref = pull_request.target_ref
860
860
861 version._last_merge_source_rev = pull_request._last_merge_source_rev
861 version._last_merge_source_rev = pull_request._last_merge_source_rev
862 version._last_merge_target_rev = pull_request._last_merge_target_rev
862 version._last_merge_target_rev = pull_request._last_merge_target_rev
863 version.last_merge_status = pull_request.last_merge_status
863 version.last_merge_status = pull_request.last_merge_status
864 version.shadow_merge_ref = pull_request.shadow_merge_ref
864 version.shadow_merge_ref = pull_request.shadow_merge_ref
865 version.merge_rev = pull_request.merge_rev
865 version.merge_rev = pull_request.merge_rev
866 version.reviewer_data = pull_request.reviewer_data
866 version.reviewer_data = pull_request.reviewer_data
867
867
868 version.revisions = pull_request.revisions
868 version.revisions = pull_request.revisions
869 version.pull_request = pull_request
869 version.pull_request = pull_request
870 Session().add(version)
870 Session().add(version)
871 Session().flush()
871 Session().flush()
872
872
873 return version
873 return version
874
874
875 def _generate_update_diffs(self, pull_request, pull_request_version):
875 def _generate_update_diffs(self, pull_request, pull_request_version):
876
876
877 diff_context = (
877 diff_context = (
878 self.DIFF_CONTEXT +
878 self.DIFF_CONTEXT +
879 CommentsModel.needed_extra_diff_context())
879 CommentsModel.needed_extra_diff_context())
880
880 hide_whitespace_changes = False
881 source_repo = pull_request_version.source_repo
881 source_repo = pull_request_version.source_repo
882 source_ref_id = pull_request_version.source_ref_parts.commit_id
882 source_ref_id = pull_request_version.source_ref_parts.commit_id
883 target_ref_id = pull_request_version.target_ref_parts.commit_id
883 target_ref_id = pull_request_version.target_ref_parts.commit_id
884 old_diff = self._get_diff_from_pr_or_version(
884 old_diff = self._get_diff_from_pr_or_version(
885 source_repo, source_ref_id, target_ref_id, context=diff_context)
885 source_repo, source_ref_id, target_ref_id,
886 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
886
887
887 source_repo = pull_request.source_repo
888 source_repo = pull_request.source_repo
888 source_ref_id = pull_request.source_ref_parts.commit_id
889 source_ref_id = pull_request.source_ref_parts.commit_id
889 target_ref_id = pull_request.target_ref_parts.commit_id
890 target_ref_id = pull_request.target_ref_parts.commit_id
890
891
891 new_diff = self._get_diff_from_pr_or_version(
892 new_diff = self._get_diff_from_pr_or_version(
892 source_repo, source_ref_id, target_ref_id, context=diff_context)
893 source_repo, source_ref_id, target_ref_id,
894 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
893
895
894 old_diff_data = diffs.DiffProcessor(old_diff)
896 old_diff_data = diffs.DiffProcessor(old_diff)
895 old_diff_data.prepare()
897 old_diff_data.prepare()
896 new_diff_data = diffs.DiffProcessor(new_diff)
898 new_diff_data = diffs.DiffProcessor(new_diff)
897 new_diff_data.prepare()
899 new_diff_data.prepare()
898
900
899 return old_diff_data, new_diff_data
901 return old_diff_data, new_diff_data
900
902
901 def _link_comments_to_version(self, pull_request_version):
903 def _link_comments_to_version(self, pull_request_version):
902 """
904 """
903 Link all unlinked comments of this pull request to the given version.
905 Link all unlinked comments of this pull request to the given version.
904
906
905 :param pull_request_version: The `PullRequestVersion` to which
907 :param pull_request_version: The `PullRequestVersion` to which
906 the comments shall be linked.
908 the comments shall be linked.
907
909
908 """
910 """
909 pull_request = pull_request_version.pull_request
911 pull_request = pull_request_version.pull_request
910 comments = ChangesetComment.query()\
912 comments = ChangesetComment.query()\
911 .filter(
913 .filter(
912 # TODO: johbo: Should we query for the repo at all here?
914 # TODO: johbo: Should we query for the repo at all here?
913 # Pending decision on how comments of PRs are to be related
915 # Pending decision on how comments of PRs are to be related
914 # to either the source repo, the target repo or no repo at all.
916 # to either the source repo, the target repo or no repo at all.
915 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
917 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
916 ChangesetComment.pull_request == pull_request,
918 ChangesetComment.pull_request == pull_request,
917 ChangesetComment.pull_request_version == None)\
919 ChangesetComment.pull_request_version == None)\
918 .order_by(ChangesetComment.comment_id.asc())
920 .order_by(ChangesetComment.comment_id.asc())
919
921
920 # TODO: johbo: Find out why this breaks if it is done in a bulk
922 # TODO: johbo: Find out why this breaks if it is done in a bulk
921 # operation.
923 # operation.
922 for comment in comments:
924 for comment in comments:
923 comment.pull_request_version_id = (
925 comment.pull_request_version_id = (
924 pull_request_version.pull_request_version_id)
926 pull_request_version.pull_request_version_id)
925 Session().add(comment)
927 Session().add(comment)
926
928
927 def _calculate_commit_id_changes(self, old_ids, new_ids):
929 def _calculate_commit_id_changes(self, old_ids, new_ids):
928 added = [x for x in new_ids if x not in old_ids]
930 added = [x for x in new_ids if x not in old_ids]
929 common = [x for x in new_ids if x in old_ids]
931 common = [x for x in new_ids if x in old_ids]
930 removed = [x for x in old_ids if x not in new_ids]
932 removed = [x for x in old_ids if x not in new_ids]
931 total = new_ids
933 total = new_ids
932 return ChangeTuple(added, common, removed, total)
934 return ChangeTuple(added, common, removed, total)
933
935
934 def _calculate_file_changes(self, old_diff_data, new_diff_data):
936 def _calculate_file_changes(self, old_diff_data, new_diff_data):
935
937
936 old_files = OrderedDict()
938 old_files = OrderedDict()
937 for diff_data in old_diff_data.parsed_diff:
939 for diff_data in old_diff_data.parsed_diff:
938 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
940 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
939
941
940 added_files = []
942 added_files = []
941 modified_files = []
943 modified_files = []
942 removed_files = []
944 removed_files = []
943 for diff_data in new_diff_data.parsed_diff:
945 for diff_data in new_diff_data.parsed_diff:
944 new_filename = diff_data['filename']
946 new_filename = diff_data['filename']
945 new_hash = md5_safe(diff_data['raw_diff'])
947 new_hash = md5_safe(diff_data['raw_diff'])
946
948
947 old_hash = old_files.get(new_filename)
949 old_hash = old_files.get(new_filename)
948 if not old_hash:
950 if not old_hash:
949 # file is not present in old diff, means it's added
951 # file is not present in old diff, means it's added
950 added_files.append(new_filename)
952 added_files.append(new_filename)
951 else:
953 else:
952 if new_hash != old_hash:
954 if new_hash != old_hash:
953 modified_files.append(new_filename)
955 modified_files.append(new_filename)
954 # now remove a file from old, since we have seen it already
956 # now remove a file from old, since we have seen it already
955 del old_files[new_filename]
957 del old_files[new_filename]
956
958
957 # removed files is when there are present in old, but not in NEW,
959 # removed files is when there are present in old, but not in NEW,
958 # since we remove old files that are present in new diff, left-overs
960 # since we remove old files that are present in new diff, left-overs
959 # if any should be the removed files
961 # if any should be the removed files
960 removed_files.extend(old_files.keys())
962 removed_files.extend(old_files.keys())
961
963
962 return FileChangeTuple(added_files, modified_files, removed_files)
964 return FileChangeTuple(added_files, modified_files, removed_files)
963
965
964 def _render_update_message(self, changes, file_changes):
966 def _render_update_message(self, changes, file_changes):
965 """
967 """
966 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
968 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
967 so it's always looking the same disregarding on which default
969 so it's always looking the same disregarding on which default
968 renderer system is using.
970 renderer system is using.
969
971
970 :param changes: changes named tuple
972 :param changes: changes named tuple
971 :param file_changes: file changes named tuple
973 :param file_changes: file changes named tuple
972
974
973 """
975 """
974 new_status = ChangesetStatus.get_status_lbl(
976 new_status = ChangesetStatus.get_status_lbl(
975 ChangesetStatus.STATUS_UNDER_REVIEW)
977 ChangesetStatus.STATUS_UNDER_REVIEW)
976
978
977 changed_files = (
979 changed_files = (
978 file_changes.added + file_changes.modified + file_changes.removed)
980 file_changes.added + file_changes.modified + file_changes.removed)
979
981
980 params = {
982 params = {
981 'under_review_label': new_status,
983 'under_review_label': new_status,
982 'added_commits': changes.added,
984 'added_commits': changes.added,
983 'removed_commits': changes.removed,
985 'removed_commits': changes.removed,
984 'changed_files': changed_files,
986 'changed_files': changed_files,
985 'added_files': file_changes.added,
987 'added_files': file_changes.added,
986 'modified_files': file_changes.modified,
988 'modified_files': file_changes.modified,
987 'removed_files': file_changes.removed,
989 'removed_files': file_changes.removed,
988 }
990 }
989 renderer = RstTemplateRenderer()
991 renderer = RstTemplateRenderer()
990 return renderer.render('pull_request_update.mako', **params)
992 return renderer.render('pull_request_update.mako', **params)
991
993
992 def edit(self, pull_request, title, description, description_renderer, user):
994 def edit(self, pull_request, title, description, description_renderer, user):
993 pull_request = self.__get_pull_request(pull_request)
995 pull_request = self.__get_pull_request(pull_request)
994 old_data = pull_request.get_api_data(with_merge_state=False)
996 old_data = pull_request.get_api_data(with_merge_state=False)
995 if pull_request.is_closed():
997 if pull_request.is_closed():
996 raise ValueError('This pull request is closed')
998 raise ValueError('This pull request is closed')
997 if title:
999 if title:
998 pull_request.title = title
1000 pull_request.title = title
999 pull_request.description = description
1001 pull_request.description = description
1000 pull_request.updated_on = datetime.datetime.now()
1002 pull_request.updated_on = datetime.datetime.now()
1001 pull_request.description_renderer = description_renderer
1003 pull_request.description_renderer = description_renderer
1002 Session().add(pull_request)
1004 Session().add(pull_request)
1003 self._log_audit_action(
1005 self._log_audit_action(
1004 'repo.pull_request.edit', {'old_data': old_data},
1006 'repo.pull_request.edit', {'old_data': old_data},
1005 user, pull_request)
1007 user, pull_request)
1006
1008
1007 def update_reviewers(self, pull_request, reviewer_data, user):
1009 def update_reviewers(self, pull_request, reviewer_data, user):
1008 """
1010 """
1009 Update the reviewers in the pull request
1011 Update the reviewers in the pull request
1010
1012
1011 :param pull_request: the pr to update
1013 :param pull_request: the pr to update
1012 :param reviewer_data: list of tuples
1014 :param reviewer_data: list of tuples
1013 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1015 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1014 """
1016 """
1015 pull_request = self.__get_pull_request(pull_request)
1017 pull_request = self.__get_pull_request(pull_request)
1016 if pull_request.is_closed():
1018 if pull_request.is_closed():
1017 raise ValueError('This pull request is closed')
1019 raise ValueError('This pull request is closed')
1018
1020
1019 reviewers = {}
1021 reviewers = {}
1020 for user_id, reasons, mandatory, rules in reviewer_data:
1022 for user_id, reasons, mandatory, rules in reviewer_data:
1021 if isinstance(user_id, (int, basestring)):
1023 if isinstance(user_id, (int, basestring)):
1022 user_id = self._get_user(user_id).user_id
1024 user_id = self._get_user(user_id).user_id
1023 reviewers[user_id] = {
1025 reviewers[user_id] = {
1024 'reasons': reasons, 'mandatory': mandatory}
1026 'reasons': reasons, 'mandatory': mandatory}
1025
1027
1026 reviewers_ids = set(reviewers.keys())
1028 reviewers_ids = set(reviewers.keys())
1027 current_reviewers = PullRequestReviewers.query()\
1029 current_reviewers = PullRequestReviewers.query()\
1028 .filter(PullRequestReviewers.pull_request ==
1030 .filter(PullRequestReviewers.pull_request ==
1029 pull_request).all()
1031 pull_request).all()
1030 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1032 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1031
1033
1032 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1034 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1033 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1035 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1034
1036
1035 log.debug("Adding %s reviewers", ids_to_add)
1037 log.debug("Adding %s reviewers", ids_to_add)
1036 log.debug("Removing %s reviewers", ids_to_remove)
1038 log.debug("Removing %s reviewers", ids_to_remove)
1037 changed = False
1039 changed = False
1038 for uid in ids_to_add:
1040 for uid in ids_to_add:
1039 changed = True
1041 changed = True
1040 _usr = self._get_user(uid)
1042 _usr = self._get_user(uid)
1041 reviewer = PullRequestReviewers()
1043 reviewer = PullRequestReviewers()
1042 reviewer.user = _usr
1044 reviewer.user = _usr
1043 reviewer.pull_request = pull_request
1045 reviewer.pull_request = pull_request
1044 reviewer.reasons = reviewers[uid]['reasons']
1046 reviewer.reasons = reviewers[uid]['reasons']
1045 # NOTE(marcink): mandatory shouldn't be changed now
1047 # NOTE(marcink): mandatory shouldn't be changed now
1046 # reviewer.mandatory = reviewers[uid]['reasons']
1048 # reviewer.mandatory = reviewers[uid]['reasons']
1047 Session().add(reviewer)
1049 Session().add(reviewer)
1048 self._log_audit_action(
1050 self._log_audit_action(
1049 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1051 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1050 user, pull_request)
1052 user, pull_request)
1051
1053
1052 for uid in ids_to_remove:
1054 for uid in ids_to_remove:
1053 changed = True
1055 changed = True
1054 reviewers = PullRequestReviewers.query()\
1056 reviewers = PullRequestReviewers.query()\
1055 .filter(PullRequestReviewers.user_id == uid,
1057 .filter(PullRequestReviewers.user_id == uid,
1056 PullRequestReviewers.pull_request == pull_request)\
1058 PullRequestReviewers.pull_request == pull_request)\
1057 .all()
1059 .all()
1058 # use .all() in case we accidentally added the same person twice
1060 # use .all() in case we accidentally added the same person twice
1059 # this CAN happen due to the lack of DB checks
1061 # this CAN happen due to the lack of DB checks
1060 for obj in reviewers:
1062 for obj in reviewers:
1061 old_data = obj.get_dict()
1063 old_data = obj.get_dict()
1062 Session().delete(obj)
1064 Session().delete(obj)
1063 self._log_audit_action(
1065 self._log_audit_action(
1064 'repo.pull_request.reviewer.delete',
1066 'repo.pull_request.reviewer.delete',
1065 {'old_data': old_data}, user, pull_request)
1067 {'old_data': old_data}, user, pull_request)
1066
1068
1067 if changed:
1069 if changed:
1068 pull_request.updated_on = datetime.datetime.now()
1070 pull_request.updated_on = datetime.datetime.now()
1069 Session().add(pull_request)
1071 Session().add(pull_request)
1070
1072
1071 self.notify_reviewers(pull_request, ids_to_add)
1073 self.notify_reviewers(pull_request, ids_to_add)
1072 return ids_to_add, ids_to_remove
1074 return ids_to_add, ids_to_remove
1073
1075
1074 def get_url(self, pull_request, request=None, permalink=False):
1076 def get_url(self, pull_request, request=None, permalink=False):
1075 if not request:
1077 if not request:
1076 request = get_current_request()
1078 request = get_current_request()
1077
1079
1078 if permalink:
1080 if permalink:
1079 return request.route_url(
1081 return request.route_url(
1080 'pull_requests_global',
1082 'pull_requests_global',
1081 pull_request_id=pull_request.pull_request_id,)
1083 pull_request_id=pull_request.pull_request_id,)
1082 else:
1084 else:
1083 return request.route_url('pullrequest_show',
1085 return request.route_url('pullrequest_show',
1084 repo_name=safe_str(pull_request.target_repo.repo_name),
1086 repo_name=safe_str(pull_request.target_repo.repo_name),
1085 pull_request_id=pull_request.pull_request_id,)
1087 pull_request_id=pull_request.pull_request_id,)
1086
1088
1087 def get_shadow_clone_url(self, pull_request, request=None):
1089 def get_shadow_clone_url(self, pull_request, request=None):
1088 """
1090 """
1089 Returns qualified url pointing to the shadow repository. If this pull
1091 Returns qualified url pointing to the shadow repository. If this pull
1090 request is closed there is no shadow repository and ``None`` will be
1092 request is closed there is no shadow repository and ``None`` will be
1091 returned.
1093 returned.
1092 """
1094 """
1093 if pull_request.is_closed():
1095 if pull_request.is_closed():
1094 return None
1096 return None
1095 else:
1097 else:
1096 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1098 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1097 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1099 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1098
1100
1099 def notify_reviewers(self, pull_request, reviewers_ids):
1101 def notify_reviewers(self, pull_request, reviewers_ids):
1100 # notification to reviewers
1102 # notification to reviewers
1101 if not reviewers_ids:
1103 if not reviewers_ids:
1102 return
1104 return
1103
1105
1104 pull_request_obj = pull_request
1106 pull_request_obj = pull_request
1105 # get the current participants of this pull request
1107 # get the current participants of this pull request
1106 recipients = reviewers_ids
1108 recipients = reviewers_ids
1107 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1109 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1108
1110
1109 pr_source_repo = pull_request_obj.source_repo
1111 pr_source_repo = pull_request_obj.source_repo
1110 pr_target_repo = pull_request_obj.target_repo
1112 pr_target_repo = pull_request_obj.target_repo
1111
1113
1112 pr_url = h.route_url('pullrequest_show',
1114 pr_url = h.route_url('pullrequest_show',
1113 repo_name=pr_target_repo.repo_name,
1115 repo_name=pr_target_repo.repo_name,
1114 pull_request_id=pull_request_obj.pull_request_id,)
1116 pull_request_id=pull_request_obj.pull_request_id,)
1115
1117
1116 # set some variables for email notification
1118 # set some variables for email notification
1117 pr_target_repo_url = h.route_url(
1119 pr_target_repo_url = h.route_url(
1118 'repo_summary', repo_name=pr_target_repo.repo_name)
1120 'repo_summary', repo_name=pr_target_repo.repo_name)
1119
1121
1120 pr_source_repo_url = h.route_url(
1122 pr_source_repo_url = h.route_url(
1121 'repo_summary', repo_name=pr_source_repo.repo_name)
1123 'repo_summary', repo_name=pr_source_repo.repo_name)
1122
1124
1123 # pull request specifics
1125 # pull request specifics
1124 pull_request_commits = [
1126 pull_request_commits = [
1125 (x.raw_id, x.message)
1127 (x.raw_id, x.message)
1126 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1128 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1127
1129
1128 kwargs = {
1130 kwargs = {
1129 'user': pull_request.author,
1131 'user': pull_request.author,
1130 'pull_request': pull_request_obj,
1132 'pull_request': pull_request_obj,
1131 'pull_request_commits': pull_request_commits,
1133 'pull_request_commits': pull_request_commits,
1132
1134
1133 'pull_request_target_repo': pr_target_repo,
1135 'pull_request_target_repo': pr_target_repo,
1134 'pull_request_target_repo_url': pr_target_repo_url,
1136 'pull_request_target_repo_url': pr_target_repo_url,
1135
1137
1136 'pull_request_source_repo': pr_source_repo,
1138 'pull_request_source_repo': pr_source_repo,
1137 'pull_request_source_repo_url': pr_source_repo_url,
1139 'pull_request_source_repo_url': pr_source_repo_url,
1138
1140
1139 'pull_request_url': pr_url,
1141 'pull_request_url': pr_url,
1140 }
1142 }
1141
1143
1142 # pre-generate the subject for notification itself
1144 # pre-generate the subject for notification itself
1143 (subject,
1145 (subject,
1144 _h, _e, # we don't care about those
1146 _h, _e, # we don't care about those
1145 body_plaintext) = EmailNotificationModel().render_email(
1147 body_plaintext) = EmailNotificationModel().render_email(
1146 notification_type, **kwargs)
1148 notification_type, **kwargs)
1147
1149
1148 # create notification objects, and emails
1150 # create notification objects, and emails
1149 NotificationModel().create(
1151 NotificationModel().create(
1150 created_by=pull_request.author,
1152 created_by=pull_request.author,
1151 notification_subject=subject,
1153 notification_subject=subject,
1152 notification_body=body_plaintext,
1154 notification_body=body_plaintext,
1153 notification_type=notification_type,
1155 notification_type=notification_type,
1154 recipients=recipients,
1156 recipients=recipients,
1155 email_kwargs=kwargs,
1157 email_kwargs=kwargs,
1156 )
1158 )
1157
1159
1158 def delete(self, pull_request, user):
1160 def delete(self, pull_request, user):
1159 pull_request = self.__get_pull_request(pull_request)
1161 pull_request = self.__get_pull_request(pull_request)
1160 old_data = pull_request.get_api_data(with_merge_state=False)
1162 old_data = pull_request.get_api_data(with_merge_state=False)
1161 self._cleanup_merge_workspace(pull_request)
1163 self._cleanup_merge_workspace(pull_request)
1162 self._log_audit_action(
1164 self._log_audit_action(
1163 'repo.pull_request.delete', {'old_data': old_data},
1165 'repo.pull_request.delete', {'old_data': old_data},
1164 user, pull_request)
1166 user, pull_request)
1165 Session().delete(pull_request)
1167 Session().delete(pull_request)
1166
1168
1167 def close_pull_request(self, pull_request, user):
1169 def close_pull_request(self, pull_request, user):
1168 pull_request = self.__get_pull_request(pull_request)
1170 pull_request = self.__get_pull_request(pull_request)
1169 self._cleanup_merge_workspace(pull_request)
1171 self._cleanup_merge_workspace(pull_request)
1170 pull_request.status = PullRequest.STATUS_CLOSED
1172 pull_request.status = PullRequest.STATUS_CLOSED
1171 pull_request.updated_on = datetime.datetime.now()
1173 pull_request.updated_on = datetime.datetime.now()
1172 Session().add(pull_request)
1174 Session().add(pull_request)
1173 self._trigger_pull_request_hook(
1175 self._trigger_pull_request_hook(
1174 pull_request, pull_request.author, 'close')
1176 pull_request, pull_request.author, 'close')
1175
1177
1176 pr_data = pull_request.get_api_data(with_merge_state=False)
1178 pr_data = pull_request.get_api_data(with_merge_state=False)
1177 self._log_audit_action(
1179 self._log_audit_action(
1178 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1180 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1179
1181
1180 def close_pull_request_with_comment(
1182 def close_pull_request_with_comment(
1181 self, pull_request, user, repo, message=None, auth_user=None):
1183 self, pull_request, user, repo, message=None, auth_user=None):
1182
1184
1183 pull_request_review_status = pull_request.calculated_review_status()
1185 pull_request_review_status = pull_request.calculated_review_status()
1184
1186
1185 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1187 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1186 # approved only if we have voting consent
1188 # approved only if we have voting consent
1187 status = ChangesetStatus.STATUS_APPROVED
1189 status = ChangesetStatus.STATUS_APPROVED
1188 else:
1190 else:
1189 status = ChangesetStatus.STATUS_REJECTED
1191 status = ChangesetStatus.STATUS_REJECTED
1190 status_lbl = ChangesetStatus.get_status_lbl(status)
1192 status_lbl = ChangesetStatus.get_status_lbl(status)
1191
1193
1192 default_message = (
1194 default_message = (
1193 'Closing with status change {transition_icon} {status}.'
1195 'Closing with status change {transition_icon} {status}.'
1194 ).format(transition_icon='>', status=status_lbl)
1196 ).format(transition_icon='>', status=status_lbl)
1195 text = message or default_message
1197 text = message or default_message
1196
1198
1197 # create a comment, and link it to new status
1199 # create a comment, and link it to new status
1198 comment = CommentsModel().create(
1200 comment = CommentsModel().create(
1199 text=text,
1201 text=text,
1200 repo=repo.repo_id,
1202 repo=repo.repo_id,
1201 user=user.user_id,
1203 user=user.user_id,
1202 pull_request=pull_request.pull_request_id,
1204 pull_request=pull_request.pull_request_id,
1203 status_change=status_lbl,
1205 status_change=status_lbl,
1204 status_change_type=status,
1206 status_change_type=status,
1205 closing_pr=True,
1207 closing_pr=True,
1206 auth_user=auth_user,
1208 auth_user=auth_user,
1207 )
1209 )
1208
1210
1209 # calculate old status before we change it
1211 # calculate old status before we change it
1210 old_calculated_status = pull_request.calculated_review_status()
1212 old_calculated_status = pull_request.calculated_review_status()
1211 ChangesetStatusModel().set_status(
1213 ChangesetStatusModel().set_status(
1212 repo.repo_id,
1214 repo.repo_id,
1213 status,
1215 status,
1214 user.user_id,
1216 user.user_id,
1215 comment=comment,
1217 comment=comment,
1216 pull_request=pull_request.pull_request_id
1218 pull_request=pull_request.pull_request_id
1217 )
1219 )
1218
1220
1219 Session().flush()
1221 Session().flush()
1220 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1222 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1221 # we now calculate the status of pull request again, and based on that
1223 # we now calculate the status of pull request again, and based on that
1222 # calculation trigger status change. This might happen in cases
1224 # calculation trigger status change. This might happen in cases
1223 # that non-reviewer admin closes a pr, which means his vote doesn't
1225 # that non-reviewer admin closes a pr, which means his vote doesn't
1224 # change the status, while if he's a reviewer this might change it.
1226 # change the status, while if he's a reviewer this might change it.
1225 calculated_status = pull_request.calculated_review_status()
1227 calculated_status = pull_request.calculated_review_status()
1226 if old_calculated_status != calculated_status:
1228 if old_calculated_status != calculated_status:
1227 self._trigger_pull_request_hook(
1229 self._trigger_pull_request_hook(
1228 pull_request, user, 'review_status_change')
1230 pull_request, user, 'review_status_change')
1229
1231
1230 # finally close the PR
1232 # finally close the PR
1231 PullRequestModel().close_pull_request(
1233 PullRequestModel().close_pull_request(
1232 pull_request.pull_request_id, user)
1234 pull_request.pull_request_id, user)
1233
1235
1234 return comment, status
1236 return comment, status
1235
1237
1236 def merge_status(self, pull_request, translator=None,
1238 def merge_status(self, pull_request, translator=None,
1237 force_shadow_repo_refresh=False):
1239 force_shadow_repo_refresh=False):
1238 _ = translator or get_current_request().translate
1240 _ = translator or get_current_request().translate
1239
1241
1240 if not self._is_merge_enabled(pull_request):
1242 if not self._is_merge_enabled(pull_request):
1241 return False, _('Server-side pull request merging is disabled.')
1243 return False, _('Server-side pull request merging is disabled.')
1242 if pull_request.is_closed():
1244 if pull_request.is_closed():
1243 return False, _('This pull request is closed.')
1245 return False, _('This pull request is closed.')
1244 merge_possible, msg = self._check_repo_requirements(
1246 merge_possible, msg = self._check_repo_requirements(
1245 target=pull_request.target_repo, source=pull_request.source_repo,
1247 target=pull_request.target_repo, source=pull_request.source_repo,
1246 translator=_)
1248 translator=_)
1247 if not merge_possible:
1249 if not merge_possible:
1248 return merge_possible, msg
1250 return merge_possible, msg
1249
1251
1250 try:
1252 try:
1251 resp = self._try_merge(
1253 resp = self._try_merge(
1252 pull_request,
1254 pull_request,
1253 force_shadow_repo_refresh=force_shadow_repo_refresh)
1255 force_shadow_repo_refresh=force_shadow_repo_refresh)
1254 log.debug("Merge response: %s", resp)
1256 log.debug("Merge response: %s", resp)
1255 status = resp.possible, self.merge_status_message(
1257 status = resp.possible, self.merge_status_message(
1256 resp.failure_reason)
1258 resp.failure_reason)
1257 except NotImplementedError:
1259 except NotImplementedError:
1258 status = False, _('Pull request merging is not supported.')
1260 status = False, _('Pull request merging is not supported.')
1259
1261
1260 return status
1262 return status
1261
1263
1262 def _check_repo_requirements(self, target, source, translator):
1264 def _check_repo_requirements(self, target, source, translator):
1263 """
1265 """
1264 Check if `target` and `source` have compatible requirements.
1266 Check if `target` and `source` have compatible requirements.
1265
1267
1266 Currently this is just checking for largefiles.
1268 Currently this is just checking for largefiles.
1267 """
1269 """
1268 _ = translator
1270 _ = translator
1269 target_has_largefiles = self._has_largefiles(target)
1271 target_has_largefiles = self._has_largefiles(target)
1270 source_has_largefiles = self._has_largefiles(source)
1272 source_has_largefiles = self._has_largefiles(source)
1271 merge_possible = True
1273 merge_possible = True
1272 message = u''
1274 message = u''
1273
1275
1274 if target_has_largefiles != source_has_largefiles:
1276 if target_has_largefiles != source_has_largefiles:
1275 merge_possible = False
1277 merge_possible = False
1276 if source_has_largefiles:
1278 if source_has_largefiles:
1277 message = _(
1279 message = _(
1278 'Target repository large files support is disabled.')
1280 'Target repository large files support is disabled.')
1279 else:
1281 else:
1280 message = _(
1282 message = _(
1281 'Source repository large files support is disabled.')
1283 'Source repository large files support is disabled.')
1282
1284
1283 return merge_possible, message
1285 return merge_possible, message
1284
1286
1285 def _has_largefiles(self, repo):
1287 def _has_largefiles(self, repo):
1286 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1288 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1287 'extensions', 'largefiles')
1289 'extensions', 'largefiles')
1288 return largefiles_ui and largefiles_ui[0].active
1290 return largefiles_ui and largefiles_ui[0].active
1289
1291
1290 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1292 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1291 """
1293 """
1292 Try to merge the pull request and return the merge status.
1294 Try to merge the pull request and return the merge status.
1293 """
1295 """
1294 log.debug(
1296 log.debug(
1295 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1297 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1296 pull_request.pull_request_id, force_shadow_repo_refresh)
1298 pull_request.pull_request_id, force_shadow_repo_refresh)
1297 target_vcs = pull_request.target_repo.scm_instance()
1299 target_vcs = pull_request.target_repo.scm_instance()
1298
1300
1299 # Refresh the target reference.
1301 # Refresh the target reference.
1300 try:
1302 try:
1301 target_ref = self._refresh_reference(
1303 target_ref = self._refresh_reference(
1302 pull_request.target_ref_parts, target_vcs)
1304 pull_request.target_ref_parts, target_vcs)
1303 except CommitDoesNotExistError:
1305 except CommitDoesNotExistError:
1304 merge_state = MergeResponse(
1306 merge_state = MergeResponse(
1305 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1307 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1306 return merge_state
1308 return merge_state
1307
1309
1308 target_locked = pull_request.target_repo.locked
1310 target_locked = pull_request.target_repo.locked
1309 if target_locked and target_locked[0]:
1311 if target_locked and target_locked[0]:
1310 log.debug("The target repository is locked.")
1312 log.debug("The target repository is locked.")
1311 merge_state = MergeResponse(
1313 merge_state = MergeResponse(
1312 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1314 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1313 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1315 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1314 pull_request, target_ref):
1316 pull_request, target_ref):
1315 log.debug("Refreshing the merge status of the repository.")
1317 log.debug("Refreshing the merge status of the repository.")
1316 merge_state = self._refresh_merge_state(
1318 merge_state = self._refresh_merge_state(
1317 pull_request, target_vcs, target_ref)
1319 pull_request, target_vcs, target_ref)
1318 else:
1320 else:
1319 possible = pull_request.\
1321 possible = pull_request.\
1320 last_merge_status == MergeFailureReason.NONE
1322 last_merge_status == MergeFailureReason.NONE
1321 merge_state = MergeResponse(
1323 merge_state = MergeResponse(
1322 possible, False, None, pull_request.last_merge_status)
1324 possible, False, None, pull_request.last_merge_status)
1323
1325
1324 return merge_state
1326 return merge_state
1325
1327
1326 def _refresh_reference(self, reference, vcs_repository):
1328 def _refresh_reference(self, reference, vcs_repository):
1327 if reference.type in ('branch', 'book'):
1329 if reference.type in ('branch', 'book'):
1328 name_or_id = reference.name
1330 name_or_id = reference.name
1329 else:
1331 else:
1330 name_or_id = reference.commit_id
1332 name_or_id = reference.commit_id
1331 refreshed_commit = vcs_repository.get_commit(name_or_id)
1333 refreshed_commit = vcs_repository.get_commit(name_or_id)
1332 refreshed_reference = Reference(
1334 refreshed_reference = Reference(
1333 reference.type, reference.name, refreshed_commit.raw_id)
1335 reference.type, reference.name, refreshed_commit.raw_id)
1334 return refreshed_reference
1336 return refreshed_reference
1335
1337
1336 def _needs_merge_state_refresh(self, pull_request, target_reference):
1338 def _needs_merge_state_refresh(self, pull_request, target_reference):
1337 return not(
1339 return not(
1338 pull_request.revisions and
1340 pull_request.revisions and
1339 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1341 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1340 target_reference.commit_id == pull_request._last_merge_target_rev)
1342 target_reference.commit_id == pull_request._last_merge_target_rev)
1341
1343
1342 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1344 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1343 workspace_id = self._workspace_id(pull_request)
1345 workspace_id = self._workspace_id(pull_request)
1344 source_vcs = pull_request.source_repo.scm_instance()
1346 source_vcs = pull_request.source_repo.scm_instance()
1345 repo_id = pull_request.target_repo.repo_id
1347 repo_id = pull_request.target_repo.repo_id
1346 use_rebase = self._use_rebase_for_merging(pull_request)
1348 use_rebase = self._use_rebase_for_merging(pull_request)
1347 close_branch = self._close_branch_before_merging(pull_request)
1349 close_branch = self._close_branch_before_merging(pull_request)
1348 merge_state = target_vcs.merge(
1350 merge_state = target_vcs.merge(
1349 repo_id, workspace_id,
1351 repo_id, workspace_id,
1350 target_reference, source_vcs, pull_request.source_ref_parts,
1352 target_reference, source_vcs, pull_request.source_ref_parts,
1351 dry_run=True, use_rebase=use_rebase,
1353 dry_run=True, use_rebase=use_rebase,
1352 close_branch=close_branch)
1354 close_branch=close_branch)
1353
1355
1354 # Do not store the response if there was an unknown error.
1356 # Do not store the response if there was an unknown error.
1355 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1357 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1356 pull_request._last_merge_source_rev = \
1358 pull_request._last_merge_source_rev = \
1357 pull_request.source_ref_parts.commit_id
1359 pull_request.source_ref_parts.commit_id
1358 pull_request._last_merge_target_rev = target_reference.commit_id
1360 pull_request._last_merge_target_rev = target_reference.commit_id
1359 pull_request.last_merge_status = merge_state.failure_reason
1361 pull_request.last_merge_status = merge_state.failure_reason
1360 pull_request.shadow_merge_ref = merge_state.merge_ref
1362 pull_request.shadow_merge_ref = merge_state.merge_ref
1361 Session().add(pull_request)
1363 Session().add(pull_request)
1362 Session().commit()
1364 Session().commit()
1363
1365
1364 return merge_state
1366 return merge_state
1365
1367
1366 def _workspace_id(self, pull_request):
1368 def _workspace_id(self, pull_request):
1367 workspace_id = 'pr-%s' % pull_request.pull_request_id
1369 workspace_id = 'pr-%s' % pull_request.pull_request_id
1368 return workspace_id
1370 return workspace_id
1369
1371
1370 def merge_status_message(self, status_code):
1372 def merge_status_message(self, status_code):
1371 """
1373 """
1372 Return a human friendly error message for the given merge status code.
1374 Return a human friendly error message for the given merge status code.
1373 """
1375 """
1374 return self.MERGE_STATUS_MESSAGES[status_code]
1376 return self.MERGE_STATUS_MESSAGES[status_code]
1375
1377
1376 def generate_repo_data(self, repo, commit_id=None, branch=None,
1378 def generate_repo_data(self, repo, commit_id=None, branch=None,
1377 bookmark=None, translator=None):
1379 bookmark=None, translator=None):
1378 from rhodecode.model.repo import RepoModel
1380 from rhodecode.model.repo import RepoModel
1379
1381
1380 all_refs, selected_ref = \
1382 all_refs, selected_ref = \
1381 self._get_repo_pullrequest_sources(
1383 self._get_repo_pullrequest_sources(
1382 repo.scm_instance(), commit_id=commit_id,
1384 repo.scm_instance(), commit_id=commit_id,
1383 branch=branch, bookmark=bookmark, translator=translator)
1385 branch=branch, bookmark=bookmark, translator=translator)
1384
1386
1385 refs_select2 = []
1387 refs_select2 = []
1386 for element in all_refs:
1388 for element in all_refs:
1387 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1389 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1388 refs_select2.append({'text': element[1], 'children': children})
1390 refs_select2.append({'text': element[1], 'children': children})
1389
1391
1390 return {
1392 return {
1391 'user': {
1393 'user': {
1392 'user_id': repo.user.user_id,
1394 'user_id': repo.user.user_id,
1393 'username': repo.user.username,
1395 'username': repo.user.username,
1394 'firstname': repo.user.first_name,
1396 'firstname': repo.user.first_name,
1395 'lastname': repo.user.last_name,
1397 'lastname': repo.user.last_name,
1396 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1398 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1397 },
1399 },
1398 'name': repo.repo_name,
1400 'name': repo.repo_name,
1399 'link': RepoModel().get_url(repo),
1401 'link': RepoModel().get_url(repo),
1400 'description': h.chop_at_smart(repo.description_safe, '\n'),
1402 'description': h.chop_at_smart(repo.description_safe, '\n'),
1401 'refs': {
1403 'refs': {
1402 'all_refs': all_refs,
1404 'all_refs': all_refs,
1403 'selected_ref': selected_ref,
1405 'selected_ref': selected_ref,
1404 'select2_refs': refs_select2
1406 'select2_refs': refs_select2
1405 }
1407 }
1406 }
1408 }
1407
1409
1408 def generate_pullrequest_title(self, source, source_ref, target):
1410 def generate_pullrequest_title(self, source, source_ref, target):
1409 return u'{source}#{at_ref} to {target}'.format(
1411 return u'{source}#{at_ref} to {target}'.format(
1410 source=source,
1412 source=source,
1411 at_ref=source_ref,
1413 at_ref=source_ref,
1412 target=target,
1414 target=target,
1413 )
1415 )
1414
1416
1415 def _cleanup_merge_workspace(self, pull_request):
1417 def _cleanup_merge_workspace(self, pull_request):
1416 # Merging related cleanup
1418 # Merging related cleanup
1417 repo_id = pull_request.target_repo.repo_id
1419 repo_id = pull_request.target_repo.repo_id
1418 target_scm = pull_request.target_repo.scm_instance()
1420 target_scm = pull_request.target_repo.scm_instance()
1419 workspace_id = self._workspace_id(pull_request)
1421 workspace_id = self._workspace_id(pull_request)
1420
1422
1421 try:
1423 try:
1422 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1424 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1423 except NotImplementedError:
1425 except NotImplementedError:
1424 pass
1426 pass
1425
1427
1426 def _get_repo_pullrequest_sources(
1428 def _get_repo_pullrequest_sources(
1427 self, repo, commit_id=None, branch=None, bookmark=None,
1429 self, repo, commit_id=None, branch=None, bookmark=None,
1428 translator=None):
1430 translator=None):
1429 """
1431 """
1430 Return a structure with repo's interesting commits, suitable for
1432 Return a structure with repo's interesting commits, suitable for
1431 the selectors in pullrequest controller
1433 the selectors in pullrequest controller
1432
1434
1433 :param commit_id: a commit that must be in the list somehow
1435 :param commit_id: a commit that must be in the list somehow
1434 and selected by default
1436 and selected by default
1435 :param branch: a branch that must be in the list and selected
1437 :param branch: a branch that must be in the list and selected
1436 by default - even if closed
1438 by default - even if closed
1437 :param bookmark: a bookmark that must be in the list and selected
1439 :param bookmark: a bookmark that must be in the list and selected
1438 """
1440 """
1439 _ = translator or get_current_request().translate
1441 _ = translator or get_current_request().translate
1440
1442
1441 commit_id = safe_str(commit_id) if commit_id else None
1443 commit_id = safe_str(commit_id) if commit_id else None
1442 branch = safe_str(branch) if branch else None
1444 branch = safe_str(branch) if branch else None
1443 bookmark = safe_str(bookmark) if bookmark else None
1445 bookmark = safe_str(bookmark) if bookmark else None
1444
1446
1445 selected = None
1447 selected = None
1446
1448
1447 # order matters: first source that has commit_id in it will be selected
1449 # order matters: first source that has commit_id in it will be selected
1448 sources = []
1450 sources = []
1449 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1451 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1450 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1452 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1451
1453
1452 if commit_id:
1454 if commit_id:
1453 ref_commit = (h.short_id(commit_id), commit_id)
1455 ref_commit = (h.short_id(commit_id), commit_id)
1454 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1456 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1455
1457
1456 sources.append(
1458 sources.append(
1457 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1459 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1458 )
1460 )
1459
1461
1460 groups = []
1462 groups = []
1461 for group_key, ref_list, group_name, match in sources:
1463 for group_key, ref_list, group_name, match in sources:
1462 group_refs = []
1464 group_refs = []
1463 for ref_name, ref_id in ref_list:
1465 for ref_name, ref_id in ref_list:
1464 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1466 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1465 group_refs.append((ref_key, ref_name))
1467 group_refs.append((ref_key, ref_name))
1466
1468
1467 if not selected:
1469 if not selected:
1468 if set([commit_id, match]) & set([ref_id, ref_name]):
1470 if set([commit_id, match]) & set([ref_id, ref_name]):
1469 selected = ref_key
1471 selected = ref_key
1470
1472
1471 if group_refs:
1473 if group_refs:
1472 groups.append((group_refs, group_name))
1474 groups.append((group_refs, group_name))
1473
1475
1474 if not selected:
1476 if not selected:
1475 ref = commit_id or branch or bookmark
1477 ref = commit_id or branch or bookmark
1476 if ref:
1478 if ref:
1477 raise CommitDoesNotExistError(
1479 raise CommitDoesNotExistError(
1478 'No commit refs could be found matching: %s' % ref)
1480 'No commit refs could be found matching: %s' % ref)
1479 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1481 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1480 selected = 'branch:%s:%s' % (
1482 selected = 'branch:%s:%s' % (
1481 repo.DEFAULT_BRANCH_NAME,
1483 repo.DEFAULT_BRANCH_NAME,
1482 repo.branches[repo.DEFAULT_BRANCH_NAME]
1484 repo.branches[repo.DEFAULT_BRANCH_NAME]
1483 )
1485 )
1484 elif repo.commit_ids:
1486 elif repo.commit_ids:
1485 # make the user select in this case
1487 # make the user select in this case
1486 selected = None
1488 selected = None
1487 else:
1489 else:
1488 raise EmptyRepositoryError()
1490 raise EmptyRepositoryError()
1489 return groups, selected
1491 return groups, selected
1490
1492
1491 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1493 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1494 hide_whitespace_changes, diff_context):
1495
1492 return self._get_diff_from_pr_or_version(
1496 return self._get_diff_from_pr_or_version(
1493 source_repo, source_ref_id, target_ref_id, context=context)
1497 source_repo, source_ref_id, target_ref_id,
1498 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1494
1499
1495 def _get_diff_from_pr_or_version(
1500 def _get_diff_from_pr_or_version(
1496 self, source_repo, source_ref_id, target_ref_id, context):
1501 self, source_repo, source_ref_id, target_ref_id,
1502 hide_whitespace_changes, diff_context):
1503
1497 target_commit = source_repo.get_commit(
1504 target_commit = source_repo.get_commit(
1498 commit_id=safe_str(target_ref_id))
1505 commit_id=safe_str(target_ref_id))
1499 source_commit = source_repo.get_commit(
1506 source_commit = source_repo.get_commit(
1500 commit_id=safe_str(source_ref_id))
1507 commit_id=safe_str(source_ref_id))
1501 if isinstance(source_repo, Repository):
1508 if isinstance(source_repo, Repository):
1502 vcs_repo = source_repo.scm_instance()
1509 vcs_repo = source_repo.scm_instance()
1503 else:
1510 else:
1504 vcs_repo = source_repo
1511 vcs_repo = source_repo
1505
1512
1506 # TODO: johbo: In the context of an update, we cannot reach
1513 # TODO: johbo: In the context of an update, we cannot reach
1507 # the old commit anymore with our normal mechanisms. It needs
1514 # the old commit anymore with our normal mechanisms. It needs
1508 # some sort of special support in the vcs layer to avoid this
1515 # some sort of special support in the vcs layer to avoid this
1509 # workaround.
1516 # workaround.
1510 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1517 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1511 vcs_repo.alias == 'git'):
1518 vcs_repo.alias == 'git'):
1512 source_commit.raw_id = safe_str(source_ref_id)
1519 source_commit.raw_id = safe_str(source_ref_id)
1513
1520
1514 log.debug('calculating diff between '
1521 log.debug('calculating diff between '
1515 'source_ref:%s and target_ref:%s for repo `%s`',
1522 'source_ref:%s and target_ref:%s for repo `%s`',
1516 target_ref_id, source_ref_id,
1523 target_ref_id, source_ref_id,
1517 safe_unicode(vcs_repo.path))
1524 safe_unicode(vcs_repo.path))
1518
1525
1519 vcs_diff = vcs_repo.get_diff(
1526 vcs_diff = vcs_repo.get_diff(
1520 commit1=target_commit, commit2=source_commit, context=context)
1527 commit1=target_commit, commit2=source_commit,
1528 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1521 return vcs_diff
1529 return vcs_diff
1522
1530
1523 def _is_merge_enabled(self, pull_request):
1531 def _is_merge_enabled(self, pull_request):
1524 return self._get_general_setting(
1532 return self._get_general_setting(
1525 pull_request, 'rhodecode_pr_merge_enabled')
1533 pull_request, 'rhodecode_pr_merge_enabled')
1526
1534
1527 def _use_rebase_for_merging(self, pull_request):
1535 def _use_rebase_for_merging(self, pull_request):
1528 repo_type = pull_request.target_repo.repo_type
1536 repo_type = pull_request.target_repo.repo_type
1529 if repo_type == 'hg':
1537 if repo_type == 'hg':
1530 return self._get_general_setting(
1538 return self._get_general_setting(
1531 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1539 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1532 elif repo_type == 'git':
1540 elif repo_type == 'git':
1533 return self._get_general_setting(
1541 return self._get_general_setting(
1534 pull_request, 'rhodecode_git_use_rebase_for_merging')
1542 pull_request, 'rhodecode_git_use_rebase_for_merging')
1535
1543
1536 return False
1544 return False
1537
1545
1538 def _close_branch_before_merging(self, pull_request):
1546 def _close_branch_before_merging(self, pull_request):
1539 repo_type = pull_request.target_repo.repo_type
1547 repo_type = pull_request.target_repo.repo_type
1540 if repo_type == 'hg':
1548 if repo_type == 'hg':
1541 return self._get_general_setting(
1549 return self._get_general_setting(
1542 pull_request, 'rhodecode_hg_close_branch_before_merging')
1550 pull_request, 'rhodecode_hg_close_branch_before_merging')
1543 elif repo_type == 'git':
1551 elif repo_type == 'git':
1544 return self._get_general_setting(
1552 return self._get_general_setting(
1545 pull_request, 'rhodecode_git_close_branch_before_merging')
1553 pull_request, 'rhodecode_git_close_branch_before_merging')
1546
1554
1547 return False
1555 return False
1548
1556
1549 def _get_general_setting(self, pull_request, settings_key, default=False):
1557 def _get_general_setting(self, pull_request, settings_key, default=False):
1550 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1558 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1551 settings = settings_model.get_general_settings()
1559 settings = settings_model.get_general_settings()
1552 return settings.get(settings_key, default)
1560 return settings.get(settings_key, default)
1553
1561
1554 def _log_audit_action(self, action, action_data, user, pull_request):
1562 def _log_audit_action(self, action, action_data, user, pull_request):
1555 audit_logger.store(
1563 audit_logger.store(
1556 action=action,
1564 action=action,
1557 action_data=action_data,
1565 action_data=action_data,
1558 user=user,
1566 user=user,
1559 repo=pull_request.target_repo)
1567 repo=pull_request.target_repo)
1560
1568
1561 def get_reviewer_functions(self):
1569 def get_reviewer_functions(self):
1562 """
1570 """
1563 Fetches functions for validation and fetching default reviewers.
1571 Fetches functions for validation and fetching default reviewers.
1564 If available we use the EE package, else we fallback to CE
1572 If available we use the EE package, else we fallback to CE
1565 package functions
1573 package functions
1566 """
1574 """
1567 try:
1575 try:
1568 from rc_reviewers.utils import get_default_reviewers_data
1576 from rc_reviewers.utils import get_default_reviewers_data
1569 from rc_reviewers.utils import validate_default_reviewers
1577 from rc_reviewers.utils import validate_default_reviewers
1570 except ImportError:
1578 except ImportError:
1571 from rhodecode.apps.repository.utils import \
1579 from rhodecode.apps.repository.utils import \
1572 get_default_reviewers_data
1580 get_default_reviewers_data
1573 from rhodecode.apps.repository.utils import \
1581 from rhodecode.apps.repository.utils import \
1574 validate_default_reviewers
1582 validate_default_reviewers
1575
1583
1576 return get_default_reviewers_data, validate_default_reviewers
1584 return get_default_reviewers_data, validate_default_reviewers
1577
1585
1578
1586
1579 class MergeCheck(object):
1587 class MergeCheck(object):
1580 """
1588 """
1581 Perform Merge Checks and returns a check object which stores information
1589 Perform Merge Checks and returns a check object which stores information
1582 about merge errors, and merge conditions
1590 about merge errors, and merge conditions
1583 """
1591 """
1584 TODO_CHECK = 'todo'
1592 TODO_CHECK = 'todo'
1585 PERM_CHECK = 'perm'
1593 PERM_CHECK = 'perm'
1586 REVIEW_CHECK = 'review'
1594 REVIEW_CHECK = 'review'
1587 MERGE_CHECK = 'merge'
1595 MERGE_CHECK = 'merge'
1588
1596
1589 def __init__(self):
1597 def __init__(self):
1590 self.review_status = None
1598 self.review_status = None
1591 self.merge_possible = None
1599 self.merge_possible = None
1592 self.merge_msg = ''
1600 self.merge_msg = ''
1593 self.failed = None
1601 self.failed = None
1594 self.errors = []
1602 self.errors = []
1595 self.error_details = OrderedDict()
1603 self.error_details = OrderedDict()
1596
1604
1597 def push_error(self, error_type, message, error_key, details):
1605 def push_error(self, error_type, message, error_key, details):
1598 self.failed = True
1606 self.failed = True
1599 self.errors.append([error_type, message])
1607 self.errors.append([error_type, message])
1600 self.error_details[error_key] = dict(
1608 self.error_details[error_key] = dict(
1601 details=details,
1609 details=details,
1602 error_type=error_type,
1610 error_type=error_type,
1603 message=message
1611 message=message
1604 )
1612 )
1605
1613
1606 @classmethod
1614 @classmethod
1607 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1615 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1608 force_shadow_repo_refresh=False):
1616 force_shadow_repo_refresh=False):
1609 _ = translator
1617 _ = translator
1610 merge_check = cls()
1618 merge_check = cls()
1611
1619
1612 # permissions to merge
1620 # permissions to merge
1613 user_allowed_to_merge = PullRequestModel().check_user_merge(
1621 user_allowed_to_merge = PullRequestModel().check_user_merge(
1614 pull_request, auth_user)
1622 pull_request, auth_user)
1615 if not user_allowed_to_merge:
1623 if not user_allowed_to_merge:
1616 log.debug("MergeCheck: cannot merge, approval is pending.")
1624 log.debug("MergeCheck: cannot merge, approval is pending.")
1617
1625
1618 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1626 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1619 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1627 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1620 if fail_early:
1628 if fail_early:
1621 return merge_check
1629 return merge_check
1622
1630
1623 # permission to merge into the target branch
1631 # permission to merge into the target branch
1624 target_commit_id = pull_request.target_ref_parts.commit_id
1632 target_commit_id = pull_request.target_ref_parts.commit_id
1625 if pull_request.target_ref_parts.type == 'branch':
1633 if pull_request.target_ref_parts.type == 'branch':
1626 branch_name = pull_request.target_ref_parts.name
1634 branch_name = pull_request.target_ref_parts.name
1627 else:
1635 else:
1628 # for mercurial we can always figure out the branch from the commit
1636 # for mercurial we can always figure out the branch from the commit
1629 # in case of bookmark
1637 # in case of bookmark
1630 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1638 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1631 branch_name = target_commit.branch
1639 branch_name = target_commit.branch
1632
1640
1633 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1641 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1634 pull_request.target_repo.repo_name, branch_name)
1642 pull_request.target_repo.repo_name, branch_name)
1635 if branch_perm and branch_perm == 'branch.none':
1643 if branch_perm and branch_perm == 'branch.none':
1636 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1644 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1637 branch_name, rule)
1645 branch_name, rule)
1638 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1646 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1639 if fail_early:
1647 if fail_early:
1640 return merge_check
1648 return merge_check
1641
1649
1642 # review status, must be always present
1650 # review status, must be always present
1643 review_status = pull_request.calculated_review_status()
1651 review_status = pull_request.calculated_review_status()
1644 merge_check.review_status = review_status
1652 merge_check.review_status = review_status
1645
1653
1646 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1654 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1647 if not status_approved:
1655 if not status_approved:
1648 log.debug("MergeCheck: cannot merge, approval is pending.")
1656 log.debug("MergeCheck: cannot merge, approval is pending.")
1649
1657
1650 msg = _('Pull request reviewer approval is pending.')
1658 msg = _('Pull request reviewer approval is pending.')
1651
1659
1652 merge_check.push_error(
1660 merge_check.push_error(
1653 'warning', msg, cls.REVIEW_CHECK, review_status)
1661 'warning', msg, cls.REVIEW_CHECK, review_status)
1654
1662
1655 if fail_early:
1663 if fail_early:
1656 return merge_check
1664 return merge_check
1657
1665
1658 # left over TODOs
1666 # left over TODOs
1659 todos = CommentsModel().get_unresolved_todos(pull_request)
1667 todos = CommentsModel().get_unresolved_todos(pull_request)
1660 if todos:
1668 if todos:
1661 log.debug("MergeCheck: cannot merge, {} "
1669 log.debug("MergeCheck: cannot merge, {} "
1662 "unresolved todos left.".format(len(todos)))
1670 "unresolved todos left.".format(len(todos)))
1663
1671
1664 if len(todos) == 1:
1672 if len(todos) == 1:
1665 msg = _('Cannot merge, {} TODO still not resolved.').format(
1673 msg = _('Cannot merge, {} TODO still not resolved.').format(
1666 len(todos))
1674 len(todos))
1667 else:
1675 else:
1668 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1676 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1669 len(todos))
1677 len(todos))
1670
1678
1671 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1679 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1672
1680
1673 if fail_early:
1681 if fail_early:
1674 return merge_check
1682 return merge_check
1675
1683
1676 # merge possible, here is the filesystem simulation + shadow repo
1684 # merge possible, here is the filesystem simulation + shadow repo
1677 merge_status, msg = PullRequestModel().merge_status(
1685 merge_status, msg = PullRequestModel().merge_status(
1678 pull_request, translator=translator,
1686 pull_request, translator=translator,
1679 force_shadow_repo_refresh=force_shadow_repo_refresh)
1687 force_shadow_repo_refresh=force_shadow_repo_refresh)
1680 merge_check.merge_possible = merge_status
1688 merge_check.merge_possible = merge_status
1681 merge_check.merge_msg = msg
1689 merge_check.merge_msg = msg
1682 if not merge_status:
1690 if not merge_status:
1683 log.debug(
1691 log.debug(
1684 "MergeCheck: cannot merge, pull request merge not possible.")
1692 "MergeCheck: cannot merge, pull request merge not possible.")
1685 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1693 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1686
1694
1687 if fail_early:
1695 if fail_early:
1688 return merge_check
1696 return merge_check
1689
1697
1690 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1698 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1691 return merge_check
1699 return merge_check
1692
1700
1693 @classmethod
1701 @classmethod
1694 def get_merge_conditions(cls, pull_request, translator):
1702 def get_merge_conditions(cls, pull_request, translator):
1695 _ = translator
1703 _ = translator
1696 merge_details = {}
1704 merge_details = {}
1697
1705
1698 model = PullRequestModel()
1706 model = PullRequestModel()
1699 use_rebase = model._use_rebase_for_merging(pull_request)
1707 use_rebase = model._use_rebase_for_merging(pull_request)
1700
1708
1701 if use_rebase:
1709 if use_rebase:
1702 merge_details['merge_strategy'] = dict(
1710 merge_details['merge_strategy'] = dict(
1703 details={},
1711 details={},
1704 message=_('Merge strategy: rebase')
1712 message=_('Merge strategy: rebase')
1705 )
1713 )
1706 else:
1714 else:
1707 merge_details['merge_strategy'] = dict(
1715 merge_details['merge_strategy'] = dict(
1708 details={},
1716 details={},
1709 message=_('Merge strategy: explicit merge commit')
1717 message=_('Merge strategy: explicit merge commit')
1710 )
1718 )
1711
1719
1712 close_branch = model._close_branch_before_merging(pull_request)
1720 close_branch = model._close_branch_before_merging(pull_request)
1713 if close_branch:
1721 if close_branch:
1714 repo_type = pull_request.target_repo.repo_type
1722 repo_type = pull_request.target_repo.repo_type
1715 if repo_type == 'hg':
1723 if repo_type == 'hg':
1716 close_msg = _('Source branch will be closed after merge.')
1724 close_msg = _('Source branch will be closed after merge.')
1717 elif repo_type == 'git':
1725 elif repo_type == 'git':
1718 close_msg = _('Source branch will be deleted after merge.')
1726 close_msg = _('Source branch will be deleted after merge.')
1719
1727
1720 merge_details['close_branch'] = dict(
1728 merge_details['close_branch'] = dict(
1721 details={},
1729 details={},
1722 message=close_msg
1730 message=close_msg
1723 )
1731 )
1724
1732
1725 return merge_details
1733 return merge_details
1726
1734
1727 ChangeTuple = collections.namedtuple(
1735 ChangeTuple = collections.namedtuple(
1728 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1736 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1729
1737
1730 FileChangeTuple = collections.namedtuple(
1738 FileChangeTuple = collections.namedtuple(
1731 'FileChangeTuple', ['added', 'modified', 'removed'])
1739 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,354 +1,350 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.mako"/>
3 <%inherit file="/base/base.mako"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5
5
6 <%def name="title()">
6 <%def name="title()">
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 %if c.rhodecode_name:
8 %if c.rhodecode_name:
9 &middot; ${h.branding(c.rhodecode_name)}
9 &middot; ${h.branding(c.rhodecode_name)}
10 %endif
10 %endif
11 </%def>
11 </%def>
12
12
13 <%def name="menu_bar_nav()">
13 <%def name="menu_bar_nav()">
14 ${self.menu_items(active='repositories')}
14 ${self.menu_items(active='repositories')}
15 </%def>
15 </%def>
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='changelog')}
18 ${self.repo_menu(active='changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <script>
22 <script>
23 // TODO: marcink switch this to pyroutes
23 // TODO: marcink switch this to pyroutes
24 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
24 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 </script>
26 </script>
27 <div class="box">
27 <div class="box">
28 <div class="title">
28 <div class="title">
29 ${self.repo_page_title(c.rhodecode_db_repo)}
29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 </div>
30 </div>
31
31
32 <div id="changeset_compare_view_content" class="summary changeset">
32 <div id="changeset_compare_view_content" class="summary changeset">
33 <div class="summary-detail">
33 <div class="summary-detail">
34 <div class="summary-detail-header">
34 <div class="summary-detail-header">
35 <div class="breadcrumbs files_location">
35 <div class="breadcrumbs files_location">
36 <h4>
36 <h4>
37 ${_('Commit')}
37 ${_('Commit')}
38
38
39 <code>
39 <code>
40 ${h.show_id(c.commit)}
40 ${h.show_id(c.commit)}
41 </code>
41 </code>
42 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
42 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
43 % if hasattr(c.commit, 'phase'):
43 % if hasattr(c.commit, 'phase'):
44 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">${c.commit.phase}</span>
44 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">${c.commit.phase}</span>
45 % endif
45 % endif
46
46
47 ## obsolete commits
47 ## obsolete commits
48 % if hasattr(c.commit, 'obsolete'):
48 % if hasattr(c.commit, 'obsolete'):
49 % if c.commit.obsolete:
49 % if c.commit.obsolete:
50 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
50 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
51 % endif
51 % endif
52 % endif
52 % endif
53
53
54 ## hidden commits
54 ## hidden commits
55 % if hasattr(c.commit, 'hidden'):
55 % if hasattr(c.commit, 'hidden'):
56 % if c.commit.hidden:
56 % if c.commit.hidden:
57 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
57 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
58 % endif
58 % endif
59 % endif
59 % endif
60 </h4>
60 </h4>
61
61
62 </div>
62 </div>
63 <div class="pull-right">
63 <div class="pull-right">
64 <span id="parent_link">
64 <span id="parent_link">
65 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
65 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
66 </span>
66 </span>
67 |
67 |
68 <span id="child_link">
68 <span id="child_link">
69 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
69 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
70 </span>
70 </span>
71 </div>
71 </div>
72 </div>
72 </div>
73
73
74 <div class="fieldset">
74 <div class="fieldset">
75 <div class="left-label">
75 <div class="left-label">
76 ${_('Description')}:
76 ${_('Description')}:
77 </div>
77 </div>
78 <div class="right-content">
78 <div class="right-content">
79 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
79 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
80 <div id="message_expand" style="display:none;">
80 <div id="message_expand" style="display:none;">
81 ${_('Expand')}
81 ${_('Expand')}
82 </div>
82 </div>
83 </div>
83 </div>
84 </div>
84 </div>
85
85
86 %if c.statuses:
86 %if c.statuses:
87 <div class="fieldset">
87 <div class="fieldset">
88 <div class="left-label">
88 <div class="left-label">
89 ${_('Commit status')}:
89 ${_('Commit status')}:
90 </div>
90 </div>
91 <div class="right-content">
91 <div class="right-content">
92 <div class="changeset-status-ico">
92 <div class="changeset-status-ico">
93 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
93 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
94 </div>
94 </div>
95 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
95 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
96 </div>
96 </div>
97 </div>
97 </div>
98 %endif
98 %endif
99
99
100 <div class="fieldset">
100 <div class="fieldset">
101 <div class="left-label">
101 <div class="left-label">
102 ${_('References')}:
102 ${_('References')}:
103 </div>
103 </div>
104 <div class="right-content">
104 <div class="right-content">
105 <div class="tags">
105 <div class="tags">
106
106
107 %if c.commit.merge:
107 %if c.commit.merge:
108 <span class="mergetag tag">
108 <span class="mergetag tag">
109 <i class="icon-merge"></i>${_('merge')}
109 <i class="icon-merge"></i>${_('merge')}
110 </span>
110 </span>
111 %endif
111 %endif
112
112
113 %if h.is_hg(c.rhodecode_repo):
113 %if h.is_hg(c.rhodecode_repo):
114 %for book in c.commit.bookmarks:
114 %for book in c.commit.bookmarks:
115 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
115 <span class="booktag tag" title="${h.tooltip(_('Bookmark %s') % book)}">
116 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
116 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
117 </span>
117 </span>
118 %endfor
118 %endfor
119 %endif
119 %endif
120
120
121 %for tag in c.commit.tags:
121 %for tag in c.commit.tags:
122 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
122 <span class="tagtag tag" title="${h.tooltip(_('Tag %s') % tag)}">
123 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=tag))}"><i class="icon-tag"></i>${tag}</a>
123 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=tag))}"><i class="icon-tag"></i>${tag}</a>
124 </span>
124 </span>
125 %endfor
125 %endfor
126
126
127 %if c.commit.branch:
127 %if c.commit.branch:
128 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % c.commit.branch)}">
128 <span class="branchtag tag" title="${h.tooltip(_('Branch %s') % c.commit.branch)}">
129 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=c.commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
129 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(at=c.commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
130 </span>
130 </span>
131 %endif
131 %endif
132 </div>
132 </div>
133 </div>
133 </div>
134 </div>
134 </div>
135
135
136 <div class="fieldset">
136 <div class="fieldset">
137 <div class="left-label">
137 <div class="left-label">
138 ${_('Diff options')}:
138 ${_('Diff options')}:
139 </div>
139 </div>
140 <div class="right-content">
140 <div class="right-content">
141 <div class="diff-actions">
141 <div class="diff-actions">
142 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
142 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
143 ${_('Raw Diff')}
143 ${_('Raw Diff')}
144 </a>
144 </a>
145 |
145 |
146 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
146 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
147 ${_('Patch Diff')}
147 ${_('Patch Diff')}
148 </a>
148 </a>
149 |
149 |
150 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
150 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
151 ${_('Download Diff')}
151 ${_('Download Diff')}
152 </a>
152 </a>
153 |
154 ${c.ignorews_url(request)}
155 |
156 ${c.context_url(request)}
157 </div>
153 </div>
158 </div>
154 </div>
159 </div>
155 </div>
160
156
161 <div class="fieldset">
157 <div class="fieldset">
162 <div class="left-label">
158 <div class="left-label">
163 ${_('Comments')}:
159 ${_('Comments')}:
164 </div>
160 </div>
165 <div class="right-content">
161 <div class="right-content">
166 <div class="comments-number">
162 <div class="comments-number">
167 %if c.comments:
163 %if c.comments:
168 <a href="#comments">${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
164 <a href="#comments">${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
169 %else:
165 %else:
170 ${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
166 ${_ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
171 %endif
167 %endif
172 %if c.inline_cnt:
168 %if c.inline_cnt:
173 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
169 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
174 %else:
170 %else:
175 ${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
171 ${_ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
176 %endif
172 %endif
177 </div>
173 </div>
178 </div>
174 </div>
179 </div>
175 </div>
180
176
181 <div class="fieldset">
177 <div class="fieldset">
182 <div class="left-label">
178 <div class="left-label">
183 ${_('Unresolved TODOs')}:
179 ${_('Unresolved TODOs')}:
184 </div>
180 </div>
185 <div class="right-content">
181 <div class="right-content">
186 <div class="comments-number">
182 <div class="comments-number">
187 % if c.unresolved_comments:
183 % if c.unresolved_comments:
188 % for co in c.unresolved_comments:
184 % for co in c.unresolved_comments:
189 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))"> #${co.comment_id}</a>${'' if loop.last else ','}
185 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))"> #${co.comment_id}</a>${'' if loop.last else ','}
190 % endfor
186 % endfor
191 % else:
187 % else:
192 ${_('There are no unresolved TODOs')}
188 ${_('There are no unresolved TODOs')}
193 % endif
189 % endif
194 </div>
190 </div>
195 </div>
191 </div>
196 </div>
192 </div>
197
193
198 </div> <!-- end summary-detail -->
194 </div> <!-- end summary-detail -->
199
195
200 <div id="commit-stats" class="sidebar-right">
196 <div id="commit-stats" class="sidebar-right">
201 <div class="summary-detail-header">
197 <div class="summary-detail-header">
202 <h4 class="item">
198 <h4 class="item">
203 ${_('Author')}
199 ${_('Author')}
204 </h4>
200 </h4>
205 </div>
201 </div>
206 <div class="sidebar-right-content">
202 <div class="sidebar-right-content">
207 ${self.gravatar_with_user(c.commit.author)}
203 ${self.gravatar_with_user(c.commit.author)}
208 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
204 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
209 </div>
205 </div>
210 </div><!-- end sidebar -->
206 </div><!-- end sidebar -->
211 </div> <!-- end summary -->
207 </div> <!-- end summary -->
212 <div class="cs_files">
208 <div class="cs_files">
213 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
209 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
214 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id])}
210 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id])}
215 ${cbdiffs.render_diffset(
211 ${cbdiffs.render_diffset(
216 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
212 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
217 </div>
213 </div>
218
214
219 ## template for inline comment form
215 ## template for inline comment form
220 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
216 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
221
217
222 ## render comments
218 ## render comments
223 ${comment.generate_comments(c.comments)}
219 ${comment.generate_comments(c.comments)}
224
220
225 ## main comment form and it status
221 ## main comment form and it status
226 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
222 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
227 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
223 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
228 </div>
224 </div>
229
225
230 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
226 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
231 <script type="text/javascript">
227 <script type="text/javascript">
232
228
233 $(document).ready(function() {
229 $(document).ready(function() {
234
230
235 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
231 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
236 if($('#trimmed_message_box').height() === boxmax){
232 if($('#trimmed_message_box').height() === boxmax){
237 $('#message_expand').show();
233 $('#message_expand').show();
238 }
234 }
239
235
240 $('#message_expand').on('click', function(e){
236 $('#message_expand').on('click', function(e){
241 $('#trimmed_message_box').css('max-height', 'none');
237 $('#trimmed_message_box').css('max-height', 'none');
242 $(this).hide();
238 $(this).hide();
243 });
239 });
244
240
245 $('.show-inline-comments').on('click', function(e){
241 $('.show-inline-comments').on('click', function(e){
246 var boxid = $(this).attr('data-comment-id');
242 var boxid = $(this).attr('data-comment-id');
247 var button = $(this);
243 var button = $(this);
248
244
249 if(button.hasClass("comments-visible")) {
245 if(button.hasClass("comments-visible")) {
250 $('#{0} .inline-comments'.format(boxid)).each(function(index){
246 $('#{0} .inline-comments'.format(boxid)).each(function(index){
251 $(this).hide();
247 $(this).hide();
252 });
248 });
253 button.removeClass("comments-visible");
249 button.removeClass("comments-visible");
254 } else {
250 } else {
255 $('#{0} .inline-comments'.format(boxid)).each(function(index){
251 $('#{0} .inline-comments'.format(boxid)).each(function(index){
256 $(this).show();
252 $(this).show();
257 });
253 });
258 button.addClass("comments-visible");
254 button.addClass("comments-visible");
259 }
255 }
260 });
256 });
261
257
262
258
263 // next links
259 // next links
264 $('#child_link').on('click', function(e){
260 $('#child_link').on('click', function(e){
265 // fetch via ajax what is going to be the next link, if we have
261 // fetch via ajax what is going to be the next link, if we have
266 // >1 links show them to user to choose
262 // >1 links show them to user to choose
267 if(!$('#child_link').hasClass('disabled')){
263 if(!$('#child_link').hasClass('disabled')){
268 $.ajax({
264 $.ajax({
269 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
265 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
270 success: function(data) {
266 success: function(data) {
271 if(data.results.length === 0){
267 if(data.results.length === 0){
272 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
268 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
273 }
269 }
274 if(data.results.length === 1){
270 if(data.results.length === 1){
275 var commit = data.results[0];
271 var commit = data.results[0];
276 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
272 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
277 }
273 }
278 else if(data.results.length === 2){
274 else if(data.results.length === 2){
279 $('#child_link').addClass('disabled');
275 $('#child_link').addClass('disabled');
280 $('#child_link').addClass('double');
276 $('#child_link').addClass('double');
281 var _html = '';
277 var _html = '';
282 _html +='<a title="__title__" href="__url__">__rev__</a> '
278 _html +='<a title="__title__" href="__url__">__rev__</a> '
283 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
279 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
284 .replace('__title__', data.results[0].message)
280 .replace('__title__', data.results[0].message)
285 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
281 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
286 _html +=' | ';
282 _html +=' | ';
287 _html +='<a title="__title__" href="__url__">__rev__</a> '
283 _html +='<a title="__title__" href="__url__">__rev__</a> '
288 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
284 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
289 .replace('__title__', data.results[1].message)
285 .replace('__title__', data.results[1].message)
290 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
286 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
291 $('#child_link').html(_html);
287 $('#child_link').html(_html);
292 }
288 }
293 }
289 }
294 });
290 });
295 e.preventDefault();
291 e.preventDefault();
296 }
292 }
297 });
293 });
298
294
299 // prev links
295 // prev links
300 $('#parent_link').on('click', function(e){
296 $('#parent_link').on('click', function(e){
301 // fetch via ajax what is going to be the next link, if we have
297 // fetch via ajax what is going to be the next link, if we have
302 // >1 links show them to user to choose
298 // >1 links show them to user to choose
303 if(!$('#parent_link').hasClass('disabled')){
299 if(!$('#parent_link').hasClass('disabled')){
304 $.ajax({
300 $.ajax({
305 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
301 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
306 success: function(data) {
302 success: function(data) {
307 if(data.results.length === 0){
303 if(data.results.length === 0){
308 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
304 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
309 }
305 }
310 if(data.results.length === 1){
306 if(data.results.length === 1){
311 var commit = data.results[0];
307 var commit = data.results[0];
312 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
308 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
313 }
309 }
314 else if(data.results.length === 2){
310 else if(data.results.length === 2){
315 $('#parent_link').addClass('disabled');
311 $('#parent_link').addClass('disabled');
316 $('#parent_link').addClass('double');
312 $('#parent_link').addClass('double');
317 var _html = '';
313 var _html = '';
318 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
314 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
319 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
315 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
320 .replace('__title__', data.results[0].message)
316 .replace('__title__', data.results[0].message)
321 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
317 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
322 _html +=' | ';
318 _html +=' | ';
323 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
319 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
324 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
320 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
325 .replace('__title__', data.results[1].message)
321 .replace('__title__', data.results[1].message)
326 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
322 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
327 $('#parent_link').html(_html);
323 $('#parent_link').html(_html);
328 }
324 }
329 }
325 }
330 });
326 });
331 e.preventDefault();
327 e.preventDefault();
332 }
328 }
333 });
329 });
334
330
335 if (location.hash) {
331 if (location.hash) {
336 var result = splitDelimitedHash(location.hash);
332 var result = splitDelimitedHash(location.hash);
337 var line = $('html').find(result.loc);
333 var line = $('html').find(result.loc);
338 if (line.length > 0){
334 if (line.length > 0){
339 offsetScroll(line, 70);
335 offsetScroll(line, 70);
340 }
336 }
341 }
337 }
342
338
343 // browse tree @ revision
339 // browse tree @ revision
344 $('#files_link').on('click', function(e){
340 $('#files_link').on('click', function(e){
345 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
341 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
346 e.preventDefault();
342 e.preventDefault();
347 });
343 });
348
344
349 // inject comments into their proper positions
345 // inject comments into their proper positions
350 var file_comments = $('.inline-comment-placeholder');
346 var file_comments = $('.inline-comment-placeholder');
351 })
347 })
352 </script>
348 </script>
353
349
354 </%def>
350 </%def>
@@ -1,131 +1,108 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Commits') % c.repo_name} -
5 ${_('%s Commits') % c.repo_name} -
6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
7 ...
7 ...
8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
10 %if c.rhodecode_name:
10 %if c.rhodecode_name:
11 &middot; ${h.branding(c.rhodecode_name)}
11 &middot; ${h.branding(c.rhodecode_name)}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%def name="breadcrumbs_links()">
15 <%def name="breadcrumbs_links()">
16 ${_('Commits')} -
16 ${_('Commits')} -
17 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
17 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
18 ...
18 ...
19 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
19 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
20 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
20 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='changelog')}
28 ${self.repo_menu(active='changelog')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32 <div class="summary-header">
32 <div class="summary-header">
33 <div class="title">
33 <div class="title">
34 ${self.repo_page_title(c.rhodecode_db_repo)}
34 ${self.repo_page_title(c.rhodecode_db_repo)}
35 </div>
35 </div>
36 </div>
36 </div>
37
37
38
38
39 <div class="summary changeset">
39 <div class="summary changeset">
40 <div class="summary-detail">
40 <div class="summary-detail">
41 <div class="summary-detail-header">
41 <div class="summary-detail-header">
42 <span class="breadcrumbs files_location">
42 <span class="breadcrumbs files_location">
43 <h4>
43 <h4>
44 ${_('Commit Range')}
44 ${_('Commit Range')}
45 <code>
45 <code>
46 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
46 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
47 </code>
47 </code>
48 </h4>
48 </h4>
49 </span>
49 </span>
50 </div>
50 </div>
51
51
52 <div class="fieldset">
52 <div class="fieldset">
53 <div class="left-label">
53 <div class="left-label">
54 ${_('Diff option')}:
54 ${_('Diff option')}:
55 </div>
55 </div>
56 <div class="right-content">
56 <div class="right-content">
57 <div class="btn btn-primary">
57 <div class="btn btn-primary">
58 <a href="${h.route_path('repo_compare',
58 <a href="${h.route_path('repo_compare',
59 repo_name=c.repo_name,
59 repo_name=c.repo_name,
60 source_ref_type='rev',
60 source_ref_type='rev',
61 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
61 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
62 target_ref_type='rev',
62 target_ref_type='rev',
63 target_ref=c.commit_ranges[-1].raw_id)}"
63 target_ref=c.commit_ranges[-1].raw_id)}"
64 >
64 >
65 ${_('Show combined compare')}
65 ${_('Show combined compare')}
66 </a>
66 </a>
67 </div>
67 </div>
68 </div>
68 </div>
69 </div>
69 </div>
70
70
71 <%doc>
72 ##TODO(marcink): implement this and diff menus
73 <div class="fieldset">
74 <div class="left-label">
75 ${_('Diff options')}:
76 </div>
77 <div class="right-content">
78 <div class="diff-actions">
79 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
80 ${_('Raw Diff')}
81 </a>
82 |
83 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
84 ${_('Patch Diff')}
85 </a>
86 |
87 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id='?',_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
88 ${_('Download Diff')}
89 </a>
90 </div>
91 </div>
92 </div>
93 </%doc>
94 </div> <!-- end summary-detail -->
71 </div> <!-- end summary-detail -->
95
72
96 </div> <!-- end summary -->
73 </div> <!-- end summary -->
97
74
98 <div id="changeset_compare_view_content">
75 <div id="changeset_compare_view_content">
99 <div class="pull-left">
76 <div class="pull-left">
100 <div class="btn-group">
77 <div class="btn-group">
101 <a
78 <a
102 class="btn"
79 class="btn"
103 href="#"
80 href="#"
104 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
81 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
105 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
82 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
106 </a>
83 </a>
107 <a
84 <a
108 class="btn"
85 class="btn"
109 href="#"
86 href="#"
110 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
87 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
111 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
88 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
112 </a>
89 </a>
113 </div>
90 </div>
114 </div>
91 </div>
115 ## Commit range generated below
92 ## Commit range generated below
116 <%include file="../compare/compare_commits.mako"/>
93 <%include file="../compare/compare_commits.mako"/>
117 <div class="cs_files">
94 <div class="cs_files">
118 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
95 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
119 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
96 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
120 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
97 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
121 ${cbdiffs.render_diffset_menu()}
98 ${cbdiffs.render_diffset_menu()}
122 %for commit in c.commit_ranges:
99 %for commit in c.commit_ranges:
123 ${cbdiffs.render_diffset(
100 ${cbdiffs.render_diffset(
124 diffset=c.changes[commit.raw_id],
101 diffset=c.changes[commit.raw_id],
125 collapse_when_files_over=5,
102 collapse_when_files_over=5,
126 commit=commit,
103 commit=commit,
127 )}
104 )}
128 %endfor
105 %endfor
129 </div>
106 </div>
130 </div>
107 </div>
131 </%def>
108 </%def>
@@ -1,971 +1,1016 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
2
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 %></%def>
5 %></%def>
6
6
7 <%def name="action_class(action)">
7 <%def name="action_class(action)">
8 <%
8 <%
9 return {
9 return {
10 '-': 'cb-deletion',
10 '-': 'cb-deletion',
11 '+': 'cb-addition',
11 '+': 'cb-addition',
12 ' ': 'cb-context',
12 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
14 %>
14 %>
15 </%def>
15 </%def>
16
16
17 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
18 <%
18 <%
19 return {
19 return {
20 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
23 %>
23 %>
24 </%def>
24 </%def>
25
25
26
26
27
27
28 <%def name="render_diffset(diffset, commit=None,
28 <%def name="render_diffset(diffset, commit=None,
29
29
30 # collapse all file diff entries when there are more than this amount of files in the diff
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
31 collapse_when_files_over=20,
32
32
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
34 lines_changed_limit=500,
35
35
36 # add a ruler at to the output
36 # add a ruler at to the output
37 ruler_at_chars=0,
37 ruler_at_chars=0,
38
38
39 # show inline comments
39 # show inline comments
40 use_comments=False,
40 use_comments=False,
41
41
42 # disable new comments
42 # disable new comments
43 disable_new_comments=False,
43 disable_new_comments=False,
44
44
45 # special file-comments that were deleted in previous versions
45 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
46 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None,
47 deleted_files_comments=None,
48
48
49 # for cache purpose
49 # for cache purpose
50 inline_comments=None,
50 inline_comments=None,
51
51
52 )">
52 )">
53 %if use_comments:
53 %if use_comments:
54 <div id="cb-comments-inline-container-template" class="js-template">
54 <div id="cb-comments-inline-container-template" class="js-template">
55 ${inline_comments_container([], inline_comments)}
55 ${inline_comments_container([], inline_comments)}
56 </div>
56 </div>
57 <div class="js-template" id="cb-comment-inline-form-template">
57 <div class="js-template" id="cb-comment-inline-form-template">
58 <div class="comment-inline-form ac">
58 <div class="comment-inline-form ac">
59
59
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 ## render template for inline comments
61 ## render template for inline comments
62 ${commentblock.comment_form(form_type='inline')}
62 ${commentblock.comment_form(form_type='inline')}
63 %else:
63 %else:
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 <div class="pull-left">
65 <div class="pull-left">
66 <div class="comment-help pull-right">
66 <div class="comment-help pull-right">
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="comment-button pull-right">
70 <div class="comment-button pull-right">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 ${_('Cancel')}
72 ${_('Cancel')}
73 </button>
73 </button>
74 </div>
74 </div>
75 <div class="clearfix"></div>
75 <div class="clearfix"></div>
76 ${h.end_form()}
76 ${h.end_form()}
77 %endif
77 %endif
78 </div>
78 </div>
79 </div>
79 </div>
80
80
81 %endif
81 %endif
82 <%
82 <%
83 collapse_all = len(diffset.files) > collapse_when_files_over
83 collapse_all = len(diffset.files) > collapse_when_files_over
84 %>
84 %>
85
85
86 %if c.user_session_attrs["diffmode"] == 'sideside':
86 %if c.user_session_attrs["diffmode"] == 'sideside':
87 <style>
87 <style>
88 .wrapper {
88 .wrapper {
89 max-width: 1600px !important;
89 max-width: 1600px !important;
90 }
90 }
91 </style>
91 </style>
92 %endif
92 %endif
93
93
94 %if ruler_at_chars:
94 %if ruler_at_chars:
95 <style>
95 <style>
96 .diff table.cb .cb-content:after {
96 .diff table.cb .cb-content:after {
97 content: "";
97 content: "";
98 border-left: 1px solid blue;
98 border-left: 1px solid blue;
99 position: absolute;
99 position: absolute;
100 top: 0;
100 top: 0;
101 height: 18px;
101 height: 18px;
102 opacity: .2;
102 opacity: .2;
103 z-index: 10;
103 z-index: 10;
104 //## +5 to account for diff action (+/-)
104 //## +5 to account for diff action (+/-)
105 left: ${ruler_at_chars + 5}ch;
105 left: ${ruler_at_chars + 5}ch;
106 </style>
106 </style>
107 %endif
107 %endif
108
108
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 %if commit:
111 %if commit:
112 <div class="pull-right">
112 <div class="pull-right">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 ${_('Browse Files')}
114 ${_('Browse Files')}
115 </a>
115 </a>
116 </div>
116 </div>
117 %endif
117 %endif
118 <h2 class="clearinner">
118 <h2 class="clearinner">
119 ## invidual commit
119 ## invidual commit
120 % if commit:
120 % if commit:
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
122 ${h.age_component(commit.date)}
122 ${h.age_component(commit.date)}
123 % if diffset.limited_diff:
123 % if diffset.limited_diff:
124 - ${_('The requested commit is too big and content was truncated.')}
124 - ${_('The requested commit is too big and content was truncated.')}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
128 ## compare diff, has no file-selector and we want to show stats anyway
128 ## compare diff, has no file-selector and we want to show stats anyway
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
132 % endif
132 % endif
133 % else:
133 % else:
134 ## pull requests/compare
134 ## pull requests/compare
135 ${_('File Changes')}
135 ${_('File Changes')}
136 % endif
136 % endif
137
137
138 </h2>
138 </h2>
139 </div>
139 </div>
140
140
141 %if diffset.has_hidden_changes:
141 %if diffset.has_hidden_changes:
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
143 %elif not diffset.files:
143 %elif not diffset.files:
144 <p class="empty_data">${_('No files')}</p>
144 <p class="empty_data">${_('No files')}</p>
145 %endif
145 %endif
146
146
147 <div class="filediffs">
147 <div class="filediffs">
148
148
149 ## initial value could be marked as False later on
149 ## initial value could be marked as False later on
150 <% over_lines_changed_limit = False %>
150 <% over_lines_changed_limit = False %>
151 %for i, filediff in enumerate(diffset.files):
151 %for i, filediff in enumerate(diffset.files):
152
152
153 <%
153 <%
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
155 over_lines_changed_limit = lines_changed > lines_changed_limit
155 over_lines_changed_limit = lines_changed > lines_changed_limit
156 %>
156 %>
157 ## anchor with support of sticky header
157 ## anchor with support of sticky header
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
159
159
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
161 <div
161 <div
162 class="filediff"
162 class="filediff"
163 data-f-path="${filediff.patch['filename']}"
163 data-f-path="${filediff.patch['filename']}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
165 >
165 >
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
167 <div class="filediff-collapse-indicator"></div>
167 <div class="filediff-collapse-indicator"></div>
168 ${diff_ops(filediff)}
168 ${diff_ops(filediff)}
169 </label>
169 </label>
170
170
171 ${diff_menu(filediff, use_comments=use_comments)}
171 ${diff_menu(filediff, use_comments=use_comments)}
172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
173
173
174 ## new/deleted/empty content case
174 ## new/deleted/empty content case
175 % if not filediff.hunks:
175 % if not filediff.hunks:
176 ## Comment container, on "fakes" hunk that contains all data to render comments
176 ## Comment container, on "fakes" hunk that contains all data to render comments
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
178 % endif
178 % endif
179
179
180 %if filediff.limited_diff:
180 %if filediff.limited_diff:
181 <tr class="cb-warning cb-collapser">
181 <tr class="cb-warning cb-collapser">
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
183 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
183 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
184 </td>
184 </td>
185 </tr>
185 </tr>
186 %else:
186 %else:
187 %if over_lines_changed_limit:
187 %if over_lines_changed_limit:
188 <tr class="cb-warning cb-collapser">
188 <tr class="cb-warning cb-collapser">
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 <a href="#" class="cb-expand"
191 <a href="#" class="cb-expand"
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
193 </a>
193 </a>
194 <a href="#" class="cb-collapse"
194 <a href="#" class="cb-collapse"
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
196 </a>
196 </a>
197 </td>
197 </td>
198 </tr>
198 </tr>
199 %endif
199 %endif
200 %endif
200 %endif
201
201
202 % for hunk in filediff.hunks:
202 % for hunk in filediff.hunks:
203 <tr class="cb-hunk">
203 <tr class="cb-hunk">
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
205 ## TODO: dan: add ajax loading of more context here
205 ## TODO: dan: add ajax loading of more context here
206 ## <a href="#">
206 ## <a href="#">
207 <i class="icon-more"></i>
207 <i class="icon-more"></i>
208 ## </a>
208 ## </a>
209 </td>
209 </td>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
211 @@
211 @@
212 -${hunk.source_start},${hunk.source_length}
212 -${hunk.source_start},${hunk.source_length}
213 +${hunk.target_start},${hunk.target_length}
213 +${hunk.target_start},${hunk.target_length}
214 ${hunk.section_header}
214 ${hunk.section_header}
215 </td>
215 </td>
216 </tr>
216 </tr>
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 % endfor
218 % endfor
219
219
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
221
221
222 ## outdated comments that do not fit into currently displayed lines
222 ## outdated comments that do not fit into currently displayed lines
223 % for lineno, comments in unmatched_comments.items():
223 % for lineno, comments in unmatched_comments.items():
224
224
225 %if c.user_session_attrs["diffmode"] == 'unified':
225 %if c.user_session_attrs["diffmode"] == 'unified':
226 % if loop.index == 0:
226 % if loop.index == 0:
227 <tr class="cb-hunk">
227 <tr class="cb-hunk">
228 <td colspan="3"></td>
228 <td colspan="3"></td>
229 <td>
229 <td>
230 <div>
230 <div>
231 ${_('Unmatched inline comments below')}
231 ${_('Unmatched inline comments below')}
232 </div>
232 </div>
233 </td>
233 </td>
234 </tr>
234 </tr>
235 % endif
235 % endif
236 <tr class="cb-line">
236 <tr class="cb-line">
237 <td class="cb-data cb-context"></td>
237 <td class="cb-data cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
240 <td class="cb-content cb-context">
240 <td class="cb-content cb-context">
241 ${inline_comments_container(comments, inline_comments)}
241 ${inline_comments_container(comments, inline_comments)}
242 </td>
242 </td>
243 </tr>
243 </tr>
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
245 % if loop.index == 0:
245 % if loop.index == 0:
246 <tr class="cb-comment-info">
246 <tr class="cb-comment-info">
247 <td colspan="2"></td>
247 <td colspan="2"></td>
248 <td class="cb-line">
248 <td class="cb-line">
249 <div>
249 <div>
250 ${_('Unmatched inline comments below')}
250 ${_('Unmatched inline comments below')}
251 </div>
251 </div>
252 </td>
252 </td>
253 <td colspan="2"></td>
253 <td colspan="2"></td>
254 <td class="cb-line">
254 <td class="cb-line">
255 <div>
255 <div>
256 ${_('Unmatched comments below')}
256 ${_('Unmatched comments below')}
257 </div>
257 </div>
258 </td>
258 </td>
259 </tr>
259 </tr>
260 % endif
260 % endif
261 <tr class="cb-line">
261 <tr class="cb-line">
262 <td class="cb-data cb-context"></td>
262 <td class="cb-data cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
264 <td class="cb-content cb-context">
264 <td class="cb-content cb-context">
265 % if lineno.startswith('o'):
265 % if lineno.startswith('o'):
266 ${inline_comments_container(comments, inline_comments)}
266 ${inline_comments_container(comments, inline_comments)}
267 % endif
267 % endif
268 </td>
268 </td>
269
269
270 <td class="cb-data cb-context"></td>
270 <td class="cb-data cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
272 <td class="cb-content cb-context">
272 <td class="cb-content cb-context">
273 % if lineno.startswith('n'):
273 % if lineno.startswith('n'):
274 ${inline_comments_container(comments, inline_comments)}
274 ${inline_comments_container(comments, inline_comments)}
275 % endif
275 % endif
276 </td>
276 </td>
277 </tr>
277 </tr>
278 %endif
278 %endif
279
279
280 % endfor
280 % endfor
281
281
282 </table>
282 </table>
283 </div>
283 </div>
284 %endfor
284 %endfor
285
285
286 ## outdated comments that are made for a file that has been deleted
286 ## outdated comments that are made for a file that has been deleted
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
288 <%
288 <%
289 display_state = 'display: none'
289 display_state = 'display: none'
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
291 if open_comments_in_file:
291 if open_comments_in_file:
292 display_state = ''
292 display_state = ''
293 %>
293 %>
294 <div class="filediffs filediff-outdated" style="${display_state}">
294 <div class="filediffs filediff-outdated" style="${display_state}">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
298 <div class="filediff-collapse-indicator"></div>
298 <div class="filediff-collapse-indicator"></div>
299 <span class="pill">
299 <span class="pill">
300 ## file was deleted
300 ## file was deleted
301 <strong>${filename}</strong>
301 <strong>${filename}</strong>
302 </span>
302 </span>
303 <span class="pill-group" style="float: left">
303 <span class="pill-group" style="float: left">
304 ## file op, doesn't need translation
304 ## file op, doesn't need translation
305 <span class="pill" op="removed">removed in this version</span>
305 <span class="pill" op="removed">removed in this version</span>
306 </span>
306 </span>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
308 <span class="pill-group" style="float: right">
308 <span class="pill-group" style="float: right">
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
310 </span>
310 </span>
311 </label>
311 </label>
312
312
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
314 <tr>
314 <tr>
315 % if c.user_session_attrs["diffmode"] == 'unified':
315 % if c.user_session_attrs["diffmode"] == 'unified':
316 <td></td>
316 <td></td>
317 %endif
317 %endif
318
318
319 <td></td>
319 <td></td>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
322 </td>
322 </td>
323 </tr>
323 </tr>
324 %if c.user_session_attrs["diffmode"] == 'unified':
324 %if c.user_session_attrs["diffmode"] == 'unified':
325 <tr class="cb-line">
325 <tr class="cb-line">
326 <td class="cb-data cb-context"></td>
326 <td class="cb-data cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
329 <td class="cb-content cb-context">
329 <td class="cb-content cb-context">
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
331 </td>
331 </td>
332 </tr>
332 </tr>
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
334 <tr class="cb-line">
334 <tr class="cb-line">
335 <td class="cb-data cb-context"></td>
335 <td class="cb-data cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
337 <td class="cb-content cb-context"></td>
337 <td class="cb-content cb-context"></td>
338
338
339 <td class="cb-data cb-context"></td>
339 <td class="cb-data cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
341 <td class="cb-content cb-context">
341 <td class="cb-content cb-context">
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
343 </td>
343 </td>
344 </tr>
344 </tr>
345 %endif
345 %endif
346 </table>
346 </table>
347 </div>
347 </div>
348 </div>
348 </div>
349 % endfor
349 % endfor
350
350
351 </div>
351 </div>
352 </div>
352 </div>
353 </%def>
353 </%def>
354
354
355 <%def name="diff_ops(filediff)">
355 <%def name="diff_ops(filediff)">
356 <%
356 <%
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
359 %>
359 %>
360 <span class="pill">
360 <span class="pill">
361 %if filediff.source_file_path and filediff.target_file_path:
361 %if filediff.source_file_path and filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
363 ## file was renamed, or copied
363 ## file was renamed, or copied
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
366 <% final_path = filediff.target_file_path %>
366 <% final_path = filediff.target_file_path %>
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
369 <% final_path = filediff.target_file_path %>
369 <% final_path = filediff.target_file_path %>
370 %endif
370 %endif
371 %else:
371 %else:
372 ## file was modified
372 ## file was modified
373 <strong>${filediff.source_file_path}</strong>
373 <strong>${filediff.source_file_path}</strong>
374 <% final_path = filediff.source_file_path %>
374 <% final_path = filediff.source_file_path %>
375 %endif
375 %endif
376 %else:
376 %else:
377 %if filediff.source_file_path:
377 %if filediff.source_file_path:
378 ## file was deleted
378 ## file was deleted
379 <strong>${filediff.source_file_path}</strong>
379 <strong>${filediff.source_file_path}</strong>
380 <% final_path = filediff.source_file_path %>
380 <% final_path = filediff.source_file_path %>
381 %else:
381 %else:
382 ## file was added
382 ## file was added
383 <strong>${filediff.target_file_path}</strong>
383 <strong>${filediff.target_file_path}</strong>
384 <% final_path = filediff.target_file_path %>
384 <% final_path = filediff.target_file_path %>
385 %endif
385 %endif
386 %endif
386 %endif
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
388 </span>
388 </span>
389 ## anchor link
389 ## anchor link
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
391
391
392 <span class="pill-group" style="float: right">
392 <span class="pill-group" style="float: right">
393
393
394 ## ops pills
394 ## ops pills
395 %if filediff.limited_diff:
395 %if filediff.limited_diff:
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 %endif
397 %endif
398
398
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
400 <span class="pill" op="created">created</span>
400 <span class="pill" op="created">created</span>
401 %if filediff['target_mode'].startswith('120'):
401 %if filediff['target_mode'].startswith('120'):
402 <span class="pill" op="symlink">symlink</span>
402 <span class="pill" op="symlink">symlink</span>
403 %else:
403 %else:
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
405 %endif
405 %endif
406 %endif
406 %endif
407
407
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
409 <span class="pill" op="renamed">renamed</span>
409 <span class="pill" op="renamed">renamed</span>
410 %endif
410 %endif
411
411
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
413 <span class="pill" op="copied">copied</span>
413 <span class="pill" op="copied">copied</span>
414 %endif
414 %endif
415
415
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
417 <span class="pill" op="removed">removed</span>
417 <span class="pill" op="removed">removed</span>
418 %endif
418 %endif
419
419
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
421 <span class="pill" op="mode">
421 <span class="pill" op="mode">
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
423 </span>
423 </span>
424 %endif
424 %endif
425
425
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
427 <span class="pill" op="binary">binary</span>
427 <span class="pill" op="binary">binary</span>
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
429 <span class="pill" op="modified">modified</span>
429 <span class="pill" op="modified">modified</span>
430 %endif
430 %endif
431 %endif
431 %endif
432
432
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
435
435
436 </span>
436 </span>
437
437
438 </%def>
438 </%def>
439
439
440 <%def name="nice_mode(filemode)">
440 <%def name="nice_mode(filemode)">
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
442 </%def>
442 </%def>
443
443
444 <%def name="diff_menu(filediff, use_comments=False)">
444 <%def name="diff_menu(filediff, use_comments=False)">
445 <div class="filediff-menu">
445 <div class="filediff-menu">
446 %if filediff.diffset.source_ref:
446 %if filediff.diffset.source_ref:
447 %if filediff.operation in ['D', 'M']:
447 %if filediff.operation in ['D', 'M']:
448 <a
448 <a
449 class="tooltip"
449 class="tooltip"
450 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
450 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
451 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
451 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
452 >
452 >
453 ${_('Show file before')}
453 ${_('Show file before')}
454 </a> |
454 </a> |
455 %else:
455 %else:
456 <span
456 <span
457 class="tooltip"
457 class="tooltip"
458 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
458 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
459 >
459 >
460 ${_('Show file before')}
460 ${_('Show file before')}
461 </span> |
461 </span> |
462 %endif
462 %endif
463 %if filediff.operation in ['A', 'M']:
463 %if filediff.operation in ['A', 'M']:
464 <a
464 <a
465 class="tooltip"
465 class="tooltip"
466 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
466 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
467 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
467 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
468 >
468 >
469 ${_('Show file after')}
469 ${_('Show file after')}
470 </a> |
470 </a>
471 %else:
471 %else:
472 <span
472 <span
473 class="tooltip"
473 class="tooltip"
474 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
474 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
475 >
475 >
476 ${_('Show file after')}
476 ${_('Show file after')}
477 </span> |
477 </span>
478 %endif
479 <a
480 class="tooltip"
481 title="${h.tooltip(_('Raw diff'))}"
482 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw'))}"
483 >
484 ${_('Raw diff')}
485 </a> |
486 <a
487 class="tooltip"
488 title="${h.tooltip(_('Download diff'))}"
489 href="${h.route_path('repo_files_diff',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path, _query=dict(diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download'))}"
490 >
491 ${_('Download diff')}
492 </a>
493 % if use_comments:
494 |
495 % endif
496
497 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
498 %if hasattr(c, 'ignorews_url'):
499 ${c.ignorews_url(request, h.FID(filediff.raw_id, filediff.patch['filename']))}
500 %endif
501 %if hasattr(c, 'context_url'):
502 ${c.context_url(request, h.FID(filediff.raw_id, filediff.patch['filename']))}
503 %endif
478 %endif
504
479
505 %if use_comments:
480 % if use_comments:
481 |
506 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
482 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
507 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
483 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
508 </a>
484 </a>
509 %endif
485 % endif
486
510 %endif
487 %endif
511 </div>
488 </div>
512 </%def>
489 </%def>
513
490
514
491
515 <%def name="inline_comments_container(comments, inline_comments)">
492 <%def name="inline_comments_container(comments, inline_comments)">
516 <div class="inline-comments">
493 <div class="inline-comments">
517 %for comment in comments:
494 %for comment in comments:
518 ${commentblock.comment_block(comment, inline=True)}
495 ${commentblock.comment_block(comment, inline=True)}
519 %endfor
496 %endfor
520 % if comments and comments[-1].outdated:
497 % if comments and comments[-1].outdated:
521 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
498 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
522 style="display: none;}">
499 style="display: none;}">
523 ${_('Add another comment')}
500 ${_('Add another comment')}
524 </span>
501 </span>
525 % else:
502 % else:
526 <span onclick="return Rhodecode.comments.createComment(this)"
503 <span onclick="return Rhodecode.comments.createComment(this)"
527 class="btn btn-secondary cb-comment-add-button">
504 class="btn btn-secondary cb-comment-add-button">
528 ${_('Add another comment')}
505 ${_('Add another comment')}
529 </span>
506 </span>
530 % endif
507 % endif
531
508
532 </div>
509 </div>
533 </%def>
510 </%def>
534
511
535 <%!
512 <%!
536 def get_comments_for(diff_type, comments, filename, line_version, line_number):
513 def get_comments_for(diff_type, comments, filename, line_version, line_number):
537 if hasattr(filename, 'unicode_path'):
514 if hasattr(filename, 'unicode_path'):
538 filename = filename.unicode_path
515 filename = filename.unicode_path
539
516
540 if not isinstance(filename, basestring):
517 if not isinstance(filename, basestring):
541 return None
518 return None
542
519
543 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
520 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
544
521
545 if comments and filename in comments:
522 if comments and filename in comments:
546 file_comments = comments[filename]
523 file_comments = comments[filename]
547 if line_key in file_comments:
524 if line_key in file_comments:
548 data = file_comments.pop(line_key)
525 data = file_comments.pop(line_key)
549 return data
526 return data
550 %>
527 %>
551
528
552 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
529 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
553 %for i, line in enumerate(hunk.sideside):
530 %for i, line in enumerate(hunk.sideside):
554 <%
531 <%
555 old_line_anchor, new_line_anchor = None, None
532 old_line_anchor, new_line_anchor = None, None
556
533
557 if line.original.lineno:
534 if line.original.lineno:
558 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
535 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
559 if line.modified.lineno:
536 if line.modified.lineno:
560 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
537 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
561 %>
538 %>
562
539
563 <tr class="cb-line">
540 <tr class="cb-line">
564 <td class="cb-data ${action_class(line.original.action)}"
541 <td class="cb-data ${action_class(line.original.action)}"
565 data-line-no="${line.original.lineno}"
542 data-line-no="${line.original.lineno}"
566 >
543 >
567 <div>
544 <div>
568
545
569 <% line_old_comments = None %>
546 <% line_old_comments = None %>
570 %if line.original.get_comment_args:
547 %if line.original.get_comment_args:
571 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
548 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
572 %endif
549 %endif
573 %if line_old_comments:
550 %if line_old_comments:
574 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
551 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
575 % if has_outdated:
552 % if has_outdated:
576 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
553 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
577 % else:
554 % else:
578 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
555 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
579 % endif
556 % endif
580 %endif
557 %endif
581 </div>
558 </div>
582 </td>
559 </td>
583 <td class="cb-lineno ${action_class(line.original.action)}"
560 <td class="cb-lineno ${action_class(line.original.action)}"
584 data-line-no="${line.original.lineno}"
561 data-line-no="${line.original.lineno}"
585 %if old_line_anchor:
562 %if old_line_anchor:
586 id="${old_line_anchor}"
563 id="${old_line_anchor}"
587 %endif
564 %endif
588 >
565 >
589 %if line.original.lineno:
566 %if line.original.lineno:
590 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
567 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
591 %endif
568 %endif
592 </td>
569 </td>
593 <td class="cb-content ${action_class(line.original.action)}"
570 <td class="cb-content ${action_class(line.original.action)}"
594 data-line-no="o${line.original.lineno}"
571 data-line-no="o${line.original.lineno}"
595 >
572 >
596 %if use_comments and line.original.lineno:
573 %if use_comments and line.original.lineno:
597 ${render_add_comment_button()}
574 ${render_add_comment_button()}
598 %endif
575 %endif
599 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
576 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
600
577
601 %if use_comments and line.original.lineno and line_old_comments:
578 %if use_comments and line.original.lineno and line_old_comments:
602 ${inline_comments_container(line_old_comments, inline_comments)}
579 ${inline_comments_container(line_old_comments, inline_comments)}
603 %endif
580 %endif
604
581
605 </td>
582 </td>
606 <td class="cb-data ${action_class(line.modified.action)}"
583 <td class="cb-data ${action_class(line.modified.action)}"
607 data-line-no="${line.modified.lineno}"
584 data-line-no="${line.modified.lineno}"
608 >
585 >
609 <div>
586 <div>
610
587
611 %if line.modified.get_comment_args:
588 %if line.modified.get_comment_args:
612 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
589 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
613 %else:
590 %else:
614 <% line_new_comments = None%>
591 <% line_new_comments = None%>
615 %endif
592 %endif
616 %if line_new_comments:
593 %if line_new_comments:
617 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
594 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
618 % if has_outdated:
595 % if has_outdated:
619 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
596 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
620 % else:
597 % else:
621 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
598 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
622 % endif
599 % endif
623 %endif
600 %endif
624 </div>
601 </div>
625 </td>
602 </td>
626 <td class="cb-lineno ${action_class(line.modified.action)}"
603 <td class="cb-lineno ${action_class(line.modified.action)}"
627 data-line-no="${line.modified.lineno}"
604 data-line-no="${line.modified.lineno}"
628 %if new_line_anchor:
605 %if new_line_anchor:
629 id="${new_line_anchor}"
606 id="${new_line_anchor}"
630 %endif
607 %endif
631 >
608 >
632 %if line.modified.lineno:
609 %if line.modified.lineno:
633 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
610 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
634 %endif
611 %endif
635 </td>
612 </td>
636 <td class="cb-content ${action_class(line.modified.action)}"
613 <td class="cb-content ${action_class(line.modified.action)}"
637 data-line-no="n${line.modified.lineno}"
614 data-line-no="n${line.modified.lineno}"
638 >
615 >
639 %if use_comments and line.modified.lineno:
616 %if use_comments and line.modified.lineno:
640 ${render_add_comment_button()}
617 ${render_add_comment_button()}
641 %endif
618 %endif
642 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
619 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
643 %if use_comments and line.modified.lineno and line_new_comments:
620 %if use_comments and line.modified.lineno and line_new_comments:
644 ${inline_comments_container(line_new_comments, inline_comments)}
621 ${inline_comments_container(line_new_comments, inline_comments)}
645 %endif
622 %endif
646 </td>
623 </td>
647 </tr>
624 </tr>
648 %endfor
625 %endfor
649 </%def>
626 </%def>
650
627
651
628
652 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
629 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
653 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
630 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
654
631
655 <%
632 <%
656 old_line_anchor, new_line_anchor = None, None
633 old_line_anchor, new_line_anchor = None, None
657 if old_line_no:
634 if old_line_no:
658 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
635 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
659 if new_line_no:
636 if new_line_no:
660 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
637 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
661 %>
638 %>
662 <tr class="cb-line">
639 <tr class="cb-line">
663 <td class="cb-data ${action_class(action)}">
640 <td class="cb-data ${action_class(action)}">
664 <div>
641 <div>
665
642
666 %if comments_args:
643 %if comments_args:
667 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
644 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
668 %else:
645 %else:
669 <% comments = None %>
646 <% comments = None %>
670 %endif
647 %endif
671
648
672 % if comments:
649 % if comments:
673 <% has_outdated = any([x.outdated for x in comments]) %>
650 <% has_outdated = any([x.outdated for x in comments]) %>
674 % if has_outdated:
651 % if has_outdated:
675 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
652 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
676 % else:
653 % else:
677 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
654 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
678 % endif
655 % endif
679 % endif
656 % endif
680 </div>
657 </div>
681 </td>
658 </td>
682 <td class="cb-lineno ${action_class(action)}"
659 <td class="cb-lineno ${action_class(action)}"
683 data-line-no="${old_line_no}"
660 data-line-no="${old_line_no}"
684 %if old_line_anchor:
661 %if old_line_anchor:
685 id="${old_line_anchor}"
662 id="${old_line_anchor}"
686 %endif
663 %endif
687 >
664 >
688 %if old_line_anchor:
665 %if old_line_anchor:
689 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
666 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
690 %endif
667 %endif
691 </td>
668 </td>
692 <td class="cb-lineno ${action_class(action)}"
669 <td class="cb-lineno ${action_class(action)}"
693 data-line-no="${new_line_no}"
670 data-line-no="${new_line_no}"
694 %if new_line_anchor:
671 %if new_line_anchor:
695 id="${new_line_anchor}"
672 id="${new_line_anchor}"
696 %endif
673 %endif
697 >
674 >
698 %if new_line_anchor:
675 %if new_line_anchor:
699 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
676 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
700 %endif
677 %endif
701 </td>
678 </td>
702 <td class="cb-content ${action_class(action)}"
679 <td class="cb-content ${action_class(action)}"
703 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
680 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
704 >
681 >
705 %if use_comments:
682 %if use_comments:
706 ${render_add_comment_button()}
683 ${render_add_comment_button()}
707 %endif
684 %endif
708 <span class="cb-code">${action} ${content or '' | n}</span>
685 <span class="cb-code">${action} ${content or '' | n}</span>
709 %if use_comments and comments:
686 %if use_comments and comments:
710 ${inline_comments_container(comments, inline_comments)}
687 ${inline_comments_container(comments, inline_comments)}
711 %endif
688 %endif
712 </td>
689 </td>
713 </tr>
690 </tr>
714 %endfor
691 %endfor
715 </%def>
692 </%def>
716
693
717
694
718 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
695 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
719 % if diff_mode == 'unified':
696 % if diff_mode == 'unified':
720 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
697 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
721 % elif diff_mode == 'sideside':
698 % elif diff_mode == 'sideside':
722 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
699 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
723 % else:
700 % else:
724 <tr class="cb-line">
701 <tr class="cb-line">
725 <td>unknown diff mode</td>
702 <td>unknown diff mode</td>
726 </tr>
703 </tr>
727 % endif
704 % endif
728 </%def>file changes
705 </%def>file changes
729
706
730
707
731 <%def name="render_add_comment_button()">
708 <%def name="render_add_comment_button()">
732 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
709 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
733 <span><i class="icon-comment"></i></span>
710 <span><i class="icon-comment"></i></span>
734 </button>
711 </button>
735 </%def>
712 </%def>
736
713
737 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
714 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
738
715
739 <div id="diff-file-sticky" class="diffset-menu clearinner">
716 <div id="diff-file-sticky" class="diffset-menu clearinner">
740 ## auto adjustable
717 ## auto adjustable
741 <div class="sidebar__inner">
718 <div class="sidebar__inner">
742 <div class="sidebar__bar">
719 <div class="sidebar__bar">
743 <div class="pull-right">
720 <div class="pull-right">
744 <div class="btn-group">
721 <div class="btn-group">
745
722
723 ## DIFF OPTIONS via Select2
724 <div class="pull-left">
725 ${h.hidden('diff_menu')}
726 </div>
727
746 <a
728 <a
747 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
729 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
748 title="${h.tooltip(_('View side by side'))}"
730 title="${h.tooltip(_('View side by side'))}"
749 href="${h.current_route_path(request, diffmode='sideside')}">
731 href="${h.current_route_path(request, diffmode='sideside')}">
750 <span>${_('Side by Side')}</span>
732 <span>${_('Side by Side')}</span>
751 </a>
733 </a>
734
752 <a
735 <a
753 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
736 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
754 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
737 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
755 <span>${_('Unified')}</span>
738 <span>${_('Unified')}</span>
756 </a>
739 </a>
740
757 % if range_diff_on is True:
741 % if range_diff_on is True:
758 <a
742 <a
759 title="${_('Turn off: Show the diff as commit range')}"
743 title="${_('Turn off: Show the diff as commit range')}"
760 class="btn btn-primary"
744 class="btn btn-primary"
761 href="${h.current_route_path(request, **{"range-diff":"0"})}">
745 href="${h.current_route_path(request, **{"range-diff":"0"})}">
762 <span>${_('Range Diff')}</span>
746 <span>${_('Range Diff')}</span>
763 </a>
747 </a>
764 % elif range_diff_on is False:
748 % elif range_diff_on is False:
765 <a
749 <a
766 title="${_('Show the diff as commit range')}"
750 title="${_('Show the diff as commit range')}"
767 class="btn"
751 class="btn"
768 href="${h.current_route_path(request, **{"range-diff":"1"})}">
752 href="${h.current_route_path(request, **{"range-diff":"1"})}">
769 <span>${_('Range Diff')}</span>
753 <span>${_('Range Diff')}</span>
770 </a>
754 </a>
771 % endif
755 % endif
772 </div>
756 </div>
773 </div>
757 </div>
774 <div class="pull-left">
758 <div class="pull-left">
775 <div class="btn-group">
759 <div class="btn-group">
776 <div class="pull-left">
760 <div class="pull-left">
777 ${h.hidden('file_filter')}
761 ${h.hidden('file_filter')}
778 </div>
762 </div>
779 <a
763 <a
780 class="btn"
764 class="btn"
781 href="#"
765 href="#"
782 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
766 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
783 <a
767 <a
784 class="btn"
768 class="btn"
785 href="#"
769 href="#"
786 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
770 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
787 <a
788 class="btn"
789 href="#"
790 onclick="updateSticky();return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
791
792 </div>
771 </div>
793 </div>
772 </div>
794 </div>
773 </div>
795 <div class="fpath-placeholder">
774 <div class="fpath-placeholder">
796 <i class="icon-file-text"></i>
775 <i class="icon-file-text"></i>
797 <strong class="fpath-placeholder-text">
776 <strong class="fpath-placeholder-text">
798 Context file:
777 Context file:
799 </strong>
778 </strong>
800 </div>
779 </div>
801 <div class="sidebar_inner_shadow"></div>
780 <div class="sidebar_inner_shadow"></div>
802 </div>
781 </div>
803 </div>
782 </div>
804
783
805 % if diffset:
784 % if diffset:
806
785
807 %if diffset.limited_diff:
786 %if diffset.limited_diff:
808 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
787 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
809 %else:
788 %else:
810 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
789 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
811 %endif
790 %endif
812 ## case on range-diff placeholder needs to be updated
791 ## case on range-diff placeholder needs to be updated
813 % if range_diff_on is True:
792 % if range_diff_on is True:
814 <% file_placeholder = _('Disabled on range diff') %>
793 <% file_placeholder = _('Disabled on range diff') %>
815 % endif
794 % endif
816
795
817 <script>
796 <script>
818
797
819 var feedFilesOptions = function (query, initialData) {
798 var feedFilesOptions = function (query, initialData) {
820 var data = {results: []};
799 var data = {results: []};
821 var isQuery = typeof query.term !== 'undefined';
800 var isQuery = typeof query.term !== 'undefined';
822
801
823 var section = _gettext('Changed files');
802 var section = _gettext('Changed files');
824 var filteredData = [];
803 var filteredData = [];
825
804
826 //filter results
805 //filter results
827 $.each(initialData.results, function (idx, value) {
806 $.each(initialData.results, function (idx, value) {
828
807
829 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
808 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
830 filteredData.push({
809 filteredData.push({
831 'id': this.id,
810 'id': this.id,
832 'text': this.text,
811 'text': this.text,
833 "ops": this.ops,
812 "ops": this.ops,
834 })
813 })
835 }
814 }
836
815
837 });
816 });
838
817
839 data.results = filteredData;
818 data.results = filteredData;
840
819
841 query.callback(data);
820 query.callback(data);
842 };
821 };
843
822
844 var formatFileResult = function(result, container, query, escapeMarkup) {
823 var formatFileResult = function(result, container, query, escapeMarkup) {
845 return function(data, escapeMarkup) {
824 return function(data, escapeMarkup) {
846 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
825 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
847 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
826 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
848 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
827 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
849 '<span class="pill" op="added">{0}</span>' +
828 '<span class="pill" op="added">{0}</span>' +
850 '<span class="pill" op="deleted">{1}</span>' +
829 '<span class="pill" op="deleted">{1}</span>' +
851 '</span>'
830 '</span>'
852 ;
831 ;
853 var added = data['ops']['added'];
832 var added = data['ops']['added'];
854 if (added === 0) {
833 if (added === 0) {
855 // don't show +0
834 // don't show +0
856 added = 0;
835 added = 0;
857 } else {
836 } else {
858 added = '+' + added;
837 added = '+' + added;
859 }
838 }
860
839
861 var deleted = -1*data['ops']['deleted'];
840 var deleted = -1*data['ops']['deleted'];
862
841
863 tmpl += pill.format(added, deleted);
842 tmpl += pill.format(added, deleted);
864 return container.format(tmpl);
843 return container.format(tmpl);
865
844
866 }(result, escapeMarkup);
845 }(result, escapeMarkup);
867 };
846 };
868 var preloadData = {
847 var preloadData = {
869 results: [
848 results: [
870 % for filediff in diffset.files:
849 % for filediff in diffset.files:
871 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
850 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
872 text:"${filediff.patch['filename']}",
851 text:"${filediff.patch['filename']}",
873 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
852 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
874 % endfor
853 % endfor
875 ]
854 ]
876 };
855 };
877
856
878 $(document).ready(function () {
857 $(document).ready(function () {
879
858
880 var fileFilter = $("#file_filter").select2({
859 var fileFilter = $("#file_filter").select2({
881 'dropdownAutoWidth': true,
860 'dropdownAutoWidth': true,
882 'width': 'auto',
861 'width': 'auto',
883 'placeholder': "${file_placeholder}",
862 'placeholder': "${file_placeholder}",
884 containerCssClass: "drop-menu",
863 containerCssClass: "drop-menu",
885 dropdownCssClass: "drop-menu-dropdown",
864 dropdownCssClass: "drop-menu-dropdown",
886 data: preloadData,
865 data: preloadData,
887 query: function(query) {
866 query: function(query) {
888 feedFilesOptions(query, preloadData);
867 feedFilesOptions(query, preloadData);
889 },
868 },
890 formatResult: formatFileResult
869 formatResult: formatFileResult
891 });
870 });
892 % if range_diff_on is True:
871 % if range_diff_on is True:
893 fileFilter.select2("enable", false);
872 fileFilter.select2("enable", false);
894
873
895 % endif
874 % endif
896
875
897 $("#file_filter").on('click', function (e) {
876 $("#file_filter").on('click', function (e) {
898 e.preventDefault();
877 e.preventDefault();
899 var selected = $('#file_filter').select2('data');
878 var selected = $('#file_filter').select2('data');
900 var idSelector = "#"+selected.id;
879 var idSelector = "#"+selected.id;
901 window.location.hash = idSelector;
880 window.location.hash = idSelector;
902 // expand the container if we quick-select the field
881 // expand the container if we quick-select the field
903 $(idSelector).next().prop('checked', false);
882 $(idSelector).next().prop('checked', false);
904 updateSticky()
883 updateSticky()
905 });
884 });
906
885
907 var contextPrefix = _gettext('Context file: ');
886 var contextPrefix = _gettext('Context file: ');
908 ## sticky sidebar
887 ## sticky sidebar
909 var sidebarElement = document.getElementById('diff-file-sticky');
888 var sidebarElement = document.getElementById('diff-file-sticky');
910 sidebar = new StickySidebar(sidebarElement, {
889 sidebar = new StickySidebar(sidebarElement, {
911 topSpacing: 0,
890 topSpacing: 0,
912 bottomSpacing: 0,
891 bottomSpacing: 0,
913 innerWrapperSelector: '.sidebar__inner'
892 innerWrapperSelector: '.sidebar__inner'
914 });
893 });
915 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
894 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
916 // reset our file so it's not holding new value
895 // reset our file so it's not holding new value
917 $('.fpath-placeholder-text').html(contextPrefix)
896 $('.fpath-placeholder-text').html(contextPrefix)
918 });
897 });
919
898
920 updateSticky = function () {
899 updateSticky = function () {
921 sidebar.updateSticky();
900 sidebar.updateSticky();
922 Waypoint.refreshAll();
901 Waypoint.refreshAll();
923 };
902 };
924
903
925 var animateText = $.debounce(100, function(fPath, anchorId) {
904 var animateText = $.debounce(100, function(fPath, anchorId) {
926 // animate setting the text
905 // animate setting the text
927 var callback = function () {
906 var callback = function () {
928 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
907 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
929 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
908 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
930 };
909 };
931 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
910 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
932 });
911 });
933
912
934 ## dynamic file waypoints
913 ## dynamic file waypoints
935 var setFPathInfo = function(fPath, anchorId){
914 var setFPathInfo = function(fPath, anchorId){
936 animateText(fPath, anchorId)
915 animateText(fPath, anchorId)
937 };
916 };
938
917
939 var codeBlock = $('.filediff');
918 var codeBlock = $('.filediff');
940 // forward waypoint
919 // forward waypoint
941 codeBlock.waypoint(
920 codeBlock.waypoint(
942 function(direction) {
921 function(direction) {
943 if (direction === "down"){
922 if (direction === "down"){
944 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
923 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
945 }
924 }
946 }, {
925 }, {
947 offset: 70,
926 offset: 70,
948 context: '.fpath-placeholder'
927 context: '.fpath-placeholder'
949 }
928 }
950 );
929 );
951
930
952 // backward waypoint
931 // backward waypoint
953 codeBlock.waypoint(
932 codeBlock.waypoint(
954 function(direction) {
933 function(direction) {
955 if (direction === "up"){
934 if (direction === "up"){
956 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
935 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
957 }
936 }
958 }, {
937 }, {
959 offset: function () {
938 offset: function () {
960 return -this.element.clientHeight + 90
939 return -this.element.clientHeight + 90
961 },
940 },
962 context: '.fpath-placeholder'
941 context: '.fpath-placeholder'
963 }
942 }
964 );
943 );
965
944
945 var preloadData = {
946 results: [
947 ## Wide diff mode
948 {
949 id: 1,
950 text: _gettext('Toggle Wide Mode Diff'),
951 action: function () {
952 updateSticky();
953 Rhodecode.comments.toggleWideMode(this);
954 return null;
955 },
956 url: null,
957 },
958
959 ## Whitespace change
960 % if request.GET.get('ignorews', '') == '1':
961 {
962 id: 2,
963 text: _gettext('Show whitespace changes'),
964 action: function () {},
965 url: "${h.current_route_path(request, ignorews=0)|n}"
966 },
967 % else:
968 {
969 id: 2,
970 text: _gettext('Hide whitespace changes'),
971 action: function () {},
972 url: "${h.current_route_path(request, ignorews=1)|n}"
973 },
974 % endif
975
976 ## FULL CONTEXT
977 % if request.GET.get('fullcontext', '') == '1':
978 {
979 id: 3,
980 text: _gettext('Hide full context diff'),
981 action: function () {},
982 url: "${h.current_route_path(request, fullcontext=0)|n}"
983 },
984 % else:
985 {
986 id: 3,
987 text: _gettext('Show full context diff'),
988 action: function () {},
989 url: "${h.current_route_path(request, fullcontext=1)|n}"
990 },
991 % endif
992
993 ]
994 };
995
996 $("#diff_menu").select2({
997 minimumResultsForSearch: -1,
998 containerCssClass: "drop-menu",
999 dropdownCssClass: "drop-menu-dropdown",
1000 dropdownAutoWidth: true,
1001 data: preloadData,
1002 placeholder: "${_('Diff Options')}",
1003 });
1004 $("#diff_menu").on('select2-selecting', function (e) {
1005 e.choice.action();
1006 if (e.choice.url !== null) {
1007 window.location = e.choice.url
1008 }
1009 });
1010
966 });
1011 });
967
1012
968 </script>
1013 </script>
969 % endif
1014 % endif
970
1015
971 </%def> No newline at end of file
1016 </%def>
@@ -1,333 +1,309 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 %if c.compare_home:
6 %if c.compare_home:
7 ${_('%s Compare') % c.repo_name}
7 ${_('%s Compare') % c.repo_name}
8 %else:
8 %else:
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 %endif
10 %endif
11 %if c.rhodecode_name:
11 %if c.rhodecode_name:
12 &middot; ${h.branding(c.rhodecode_name)}
12 &middot; ${h.branding(c.rhodecode_name)}
13 %endif
13 %endif
14 </%def>
14 </%def>
15
15
16 <%def name="breadcrumbs_links()">
16 <%def name="breadcrumbs_links()">
17 ${_ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
17 ${_ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
18 </%def>
18 </%def>
19
19
20 <%def name="menu_bar_nav()">
20 <%def name="menu_bar_nav()">
21 ${self.menu_items(active='repositories')}
21 ${self.menu_items(active='repositories')}
22 </%def>
22 </%def>
23
23
24 <%def name="menu_bar_subnav()">
24 <%def name="menu_bar_subnav()">
25 ${self.repo_menu(active='compare')}
25 ${self.repo_menu(active='compare')}
26 </%def>
26 </%def>
27
27
28 <%def name="main()">
28 <%def name="main()">
29 <script type="text/javascript">
29 <script type="text/javascript">
30 // set fake commitId on this commit-range page
30 // set fake commitId on this commit-range page
31 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
31 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
32 </script>
32 </script>
33
33
34 <div class="box">
34 <div class="box">
35 <div class="title">
35 <div class="title">
36 ${self.repo_page_title(c.rhodecode_db_repo)}
36 ${self.repo_page_title(c.rhodecode_db_repo)}
37 </div>
37 </div>
38
38
39 <div class="summary changeset">
39 <div class="summary changeset">
40 <div class="summary-detail">
40 <div class="summary-detail">
41 <div class="summary-detail-header">
41 <div class="summary-detail-header">
42 <span class="breadcrumbs files_location">
42 <span class="breadcrumbs files_location">
43 <h4>
43 <h4>
44 ${_('Compare Commits')}
44 ${_('Compare Commits')}
45 % if c.file_path:
45 % if c.file_path:
46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
47 % endif
47 % endif
48
48
49 % if c.commit_ranges:
49 % if c.commit_ranges:
50 <code>
50 <code>
51 r${c.source_commit.idx}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.idx}:${h.short_id(c.target_commit.raw_id)}
51 r${c.source_commit.idx}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.idx}:${h.short_id(c.target_commit.raw_id)}
52 </code>
52 </code>
53 % endif
53 % endif
54 </h4>
54 </h4>
55 </span>
55 </span>
56 </div>
56 </div>
57
57
58 <div class="fieldset">
58 <div class="fieldset">
59 <div class="left-label">
59 <div class="left-label">
60 ${_('Target')}:
60 ${_('Target')}:
61 </div>
61 </div>
62 <div class="right-content">
62 <div class="right-content">
63 <div>
63 <div>
64 <div class="code-header" >
64 <div class="code-header" >
65 <div class="compare_header">
65 <div class="compare_header">
66 ## The hidden elements are replaced with a select2 widget
66 ## The hidden elements are replaced with a select2 widget
67 ${h.hidden('compare_source')}
67 ${h.hidden('compare_source')}
68 </div>
68 </div>
69 </div>
69 </div>
70 </div>
70 </div>
71 </div>
71 </div>
72 </div>
72 </div>
73
73
74 <div class="fieldset">
74 <div class="fieldset">
75 <div class="left-label">
75 <div class="left-label">
76 ${_('Source')}:
76 ${_('Source')}:
77 </div>
77 </div>
78 <div class="right-content">
78 <div class="right-content">
79 <div>
79 <div>
80 <div class="code-header" >
80 <div class="code-header" >
81 <div class="compare_header">
81 <div class="compare_header">
82 ## The hidden elements are replaced with a select2 widget
82 ## The hidden elements are replaced with a select2 widget
83 ${h.hidden('compare_target')}
83 ${h.hidden('compare_target')}
84 </div>
84 </div>
85 </div>
85 </div>
86 </div>
86 </div>
87 </div>
87 </div>
88 </div>
88 </div>
89
89
90 <div class="fieldset">
90 <div class="fieldset">
91 <div class="left-label">
91 <div class="left-label">
92 ${_('Actions')}:
92 ${_('Actions')}:
93 </div>
93 </div>
94 <div class="right-content">
94 <div class="right-content">
95 <div>
95 <div>
96 <div class="code-header" >
96 <div class="code-header" >
97 <div class="compare_header">
97 <div class="compare_header">
98
98
99 <div class="compare-buttons">
99 <div class="compare-buttons">
100 % if c.compare_home:
100 % if c.compare_home:
101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
102
102
103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
105 <div id="changeset_compare_view_content">
105 <div id="changeset_compare_view_content">
106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
107 </div>
107 </div>
108
108
109 % elif c.preview_mode:
109 % elif c.preview_mode:
110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
113
113
114 % else:
114 % else:
115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
117
117
118 ## allow comment only if there are commits to comment on
118 ## allow comment only if there are commits to comment on
119 % if c.diffset and c.diffset.files and c.commit_ranges:
119 % if c.diffset and c.diffset.files and c.commit_ranges:
120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
121 % else:
121 % else:
122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
123 % endif
123 % endif
124 % endif
124 % endif
125 </div>
125 </div>
126 </div>
126 </div>
127 </div>
127 </div>
128 </div>
128 </div>
129 </div>
129 </div>
130 </div>
130 </div>
131
131
132 <%doc>
133 ##TODO(marcink): implement this and diff menus
134 <div class="fieldset">
135 <div class="left-label">
136 ${_('Diff options')}:
137 </div>
138 <div class="right-content">
139 <div class="diff-actions">
140 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 ${_('Raw Diff')}
142 </a>
143 |
144 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 ${_('Patch Diff')}
146 </a>
147 |
148 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id='?',_query=dict(diff='download'))}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 ${_('Download Diff')}
150 </a>
151 </div>
152 </div>
153 </div>
154 </%doc>
155
156 ## commit status form
132 ## commit status form
157 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
133 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
158 <div class="left-label">
134 <div class="left-label">
159 ${_('Commit status')}:
135 ${_('Commit status')}:
160 </div>
136 </div>
161 <div class="right-content">
137 <div class="right-content">
162 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
138 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
163 ## main comment form and it status
139 ## main comment form and it status
164 <%
140 <%
165 def revs(_revs):
141 def revs(_revs):
166 form_inputs = []
142 form_inputs = []
167 for cs in _revs:
143 for cs in _revs:
168 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
144 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
169 form_inputs.append(tmpl)
145 form_inputs.append(tmpl)
170 return form_inputs
146 return form_inputs
171 %>
147 %>
172 <div>
148 <div>
173 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
149 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
174 </div>
150 </div>
175 </div>
151 </div>
176 </div>
152 </div>
177
153
178 </div> <!-- end summary-detail -->
154 </div> <!-- end summary-detail -->
179 </div> <!-- end summary -->
155 </div> <!-- end summary -->
180
156
181 ## use JS script to load it quickly before potentially large diffs render long time
157 ## use JS script to load it quickly before potentially large diffs render long time
182 ## this prevents from situation when large diffs block rendering of select2 fields
158 ## this prevents from situation when large diffs block rendering of select2 fields
183 <script type="text/javascript">
159 <script type="text/javascript">
184
160
185 var cache = {};
161 var cache = {};
186
162
187 var formatSelection = function(repoName){
163 var formatSelection = function(repoName){
188 return function(data, container, escapeMarkup) {
164 return function(data, container, escapeMarkup) {
189 var selection = data ? this.text(data) : "";
165 var selection = data ? this.text(data) : "";
190 return escapeMarkup('{0}@{1}'.format(repoName, selection));
166 return escapeMarkup('{0}@{1}'.format(repoName, selection));
191 }
167 }
192 };
168 };
193
169
194 var feedCompareData = function(query, cachedValue){
170 var feedCompareData = function(query, cachedValue){
195 var data = {results: []};
171 var data = {results: []};
196 //filter results
172 //filter results
197 $.each(cachedValue.results, function() {
173 $.each(cachedValue.results, function() {
198 var section = this.text;
174 var section = this.text;
199 var children = [];
175 var children = [];
200 $.each(this.children, function() {
176 $.each(this.children, function() {
201 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
177 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
202 children.push({
178 children.push({
203 'id': this.id,
179 'id': this.id,
204 'text': this.text,
180 'text': this.text,
205 'type': this.type
181 'type': this.type
206 })
182 })
207 }
183 }
208 });
184 });
209 data.results.push({
185 data.results.push({
210 'text': section,
186 'text': section,
211 'children': children
187 'children': children
212 })
188 })
213 });
189 });
214 //push the typed in changeset
190 //push the typed in changeset
215 data.results.push({
191 data.results.push({
216 'text': _gettext('specify commit'),
192 'text': _gettext('specify commit'),
217 'children': [{
193 'children': [{
218 'id': query.term,
194 'id': query.term,
219 'text': query.term,
195 'text': query.term,
220 'type': 'rev'
196 'type': 'rev'
221 }]
197 }]
222 });
198 });
223 query.callback(data);
199 query.callback(data);
224 };
200 };
225
201
226 var loadCompareData = function(repoName, query, cache){
202 var loadCompareData = function(repoName, query, cache){
227 $.ajax({
203 $.ajax({
228 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
204 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
229 data: {},
205 data: {},
230 dataType: 'json',
206 dataType: 'json',
231 type: 'GET',
207 type: 'GET',
232 success: function(data) {
208 success: function(data) {
233 cache[repoName] = data;
209 cache[repoName] = data;
234 query.callback({results: data.results});
210 query.callback({results: data.results});
235 }
211 }
236 })
212 })
237 };
213 };
238
214
239 var enable_fields = ${"false" if c.preview_mode else "true"};
215 var enable_fields = ${"false" if c.preview_mode else "true"};
240 $("#compare_source").select2({
216 $("#compare_source").select2({
241 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
217 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
242 containerCssClass: "drop-menu",
218 containerCssClass: "drop-menu",
243 dropdownCssClass: "drop-menu-dropdown",
219 dropdownCssClass: "drop-menu-dropdown",
244 formatSelection: formatSelection("${c.source_repo.repo_name}"),
220 formatSelection: formatSelection("${c.source_repo.repo_name}"),
245 dropdownAutoWidth: true,
221 dropdownAutoWidth: true,
246 query: function(query) {
222 query: function(query) {
247 var repoName = '${c.source_repo.repo_name}';
223 var repoName = '${c.source_repo.repo_name}';
248 var cachedValue = cache[repoName];
224 var cachedValue = cache[repoName];
249
225
250 if (cachedValue){
226 if (cachedValue){
251 feedCompareData(query, cachedValue);
227 feedCompareData(query, cachedValue);
252 }
228 }
253 else {
229 else {
254 loadCompareData(repoName, query, cache);
230 loadCompareData(repoName, query, cache);
255 }
231 }
256 }
232 }
257 }).select2("enable", enable_fields);
233 }).select2("enable", enable_fields);
258
234
259 $("#compare_target").select2({
235 $("#compare_target").select2({
260 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
236 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
261 dropdownAutoWidth: true,
237 dropdownAutoWidth: true,
262 containerCssClass: "drop-menu",
238 containerCssClass: "drop-menu",
263 dropdownCssClass: "drop-menu-dropdown",
239 dropdownCssClass: "drop-menu-dropdown",
264 formatSelection: formatSelection("${c.target_repo.repo_name}"),
240 formatSelection: formatSelection("${c.target_repo.repo_name}"),
265 query: function(query) {
241 query: function(query) {
266 var repoName = '${c.target_repo.repo_name}';
242 var repoName = '${c.target_repo.repo_name}';
267 var cachedValue = cache[repoName];
243 var cachedValue = cache[repoName];
268
244
269 if (cachedValue){
245 if (cachedValue){
270 feedCompareData(query, cachedValue);
246 feedCompareData(query, cachedValue);
271 }
247 }
272 else {
248 else {
273 loadCompareData(repoName, query, cache);
249 loadCompareData(repoName, query, cache);
274 }
250 }
275 }
251 }
276 }).select2("enable", enable_fields);
252 }).select2("enable", enable_fields);
277 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
253 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
278 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
254 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
279
255
280 $('#compare_revs').on('click', function(e) {
256 $('#compare_revs').on('click', function(e) {
281 var source = $('#compare_source').select2('data') || initial_compare_source;
257 var source = $('#compare_source').select2('data') || initial_compare_source;
282 var target = $('#compare_target').select2('data') || initial_compare_target;
258 var target = $('#compare_target').select2('data') || initial_compare_target;
283 if (source && target) {
259 if (source && target) {
284 var url_data = {
260 var url_data = {
285 repo_name: "${c.repo_name}",
261 repo_name: "${c.repo_name}",
286 source_ref: source.id,
262 source_ref: source.id,
287 source_ref_type: source.type,
263 source_ref_type: source.type,
288 target_ref: target.id,
264 target_ref: target.id,
289 target_ref_type: target.type
265 target_ref_type: target.type
290 };
266 };
291 window.location = pyroutes.url('repo_compare', url_data);
267 window.location = pyroutes.url('repo_compare', url_data);
292 }
268 }
293 });
269 });
294 $('#compare_changeset_status_toggle').on('click', function(e) {
270 $('#compare_changeset_status_toggle').on('click', function(e) {
295 $('#compare_changeset_status').toggle();
271 $('#compare_changeset_status').toggle();
296 });
272 });
297
273
298 </script>
274 </script>
299
275
300 ## table diff data
276 ## table diff data
301 <div class="table">
277 <div class="table">
302
278
303
279
304 % if not c.compare_home:
280 % if not c.compare_home:
305 <div id="changeset_compare_view_content">
281 <div id="changeset_compare_view_content">
306 <div class="pull-left">
282 <div class="pull-left">
307 <div class="btn-group">
283 <div class="btn-group">
308 <a
284 <a
309 class="btn"
285 class="btn"
310 href="#"
286 href="#"
311 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
287 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
312 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
288 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
313 </a>
289 </a>
314 <a
290 <a
315 class="btn"
291 class="btn"
316 href="#"
292 href="#"
317 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
293 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
318 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
294 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
319 </a>
295 </a>
320 </div>
296 </div>
321 </div>
297 </div>
322 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
298 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
323 ## commit compare generated below
299 ## commit compare generated below
324 <%include file="compare_commits.mako"/>
300 <%include file="compare_commits.mako"/>
325 ${cbdiffs.render_diffset_menu(c.diffset)}
301 ${cbdiffs.render_diffset_menu(c.diffset)}
326 ${cbdiffs.render_diffset(c.diffset)}
302 ${cbdiffs.render_diffset(c.diffset)}
327 </div>
303 </div>
328 % endif
304 % endif
329
305
330 </div>
306 </div>
331 </div>
307 </div>
332
308
333 </%def>
309 </%def>
@@ -1,870 +1,871 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 @pytest.mark.usefixtures('config_stub')
44 @pytest.mark.usefixtures('config_stub')
45 class TestPullRequestModel(object):
45 class TestPullRequestModel(object):
46
46
47 @pytest.fixture
47 @pytest.fixture
48 def pull_request(self, request, backend, pr_util):
48 def pull_request(self, request, backend, pr_util):
49 """
49 """
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
58
58
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
61 self.comment_patcher = mock.patch(
61 self.comment_patcher = mock.patch(
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 self.comment_patcher.start()
63 self.comment_patcher.start()
64 self.notification_patcher = mock.patch(
64 self.notification_patcher = mock.patch(
65 'rhodecode.model.notification.NotificationModel.create')
65 'rhodecode.model.notification.NotificationModel.create')
66 self.notification_patcher.start()
66 self.notification_patcher.start()
67 self.helper_patcher = mock.patch(
67 self.helper_patcher = mock.patch(
68 'rhodecode.lib.helpers.route_path')
68 'rhodecode.lib.helpers.route_path')
69 self.helper_patcher.start()
69 self.helper_patcher.start()
70
70
71 self.hook_patcher = mock.patch.object(PullRequestModel,
71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 '_trigger_pull_request_hook')
72 '_trigger_pull_request_hook')
73 self.hook_mock = self.hook_patcher.start()
73 self.hook_mock = self.hook_patcher.start()
74
74
75 self.invalidation_patcher = mock.patch(
75 self.invalidation_patcher = mock.patch(
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 self.invalidation_mock = self.invalidation_patcher.start()
77 self.invalidation_mock = self.invalidation_patcher.start()
78
78
79 self.pull_request = pr_util.create_pull_request(
79 self.pull_request = pr_util.create_pull_request(
80 mergeable=True, name_suffix=u'Δ…Δ‡')
80 mergeable=True, name_suffix=u'Δ…Δ‡')
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84 self.repo_id = self.pull_request.target_repo.repo_id
84 self.repo_id = self.pull_request.target_repo.repo_id
85
85
86 @request.addfinalizer
86 @request.addfinalizer
87 def cleanup_pull_request():
87 def cleanup_pull_request():
88 calls = [mock.call(
88 calls = [mock.call(
89 self.pull_request, self.pull_request.author, 'create')]
89 self.pull_request, self.pull_request.author, 'create')]
90 self.hook_mock.assert_has_calls(calls)
90 self.hook_mock.assert_has_calls(calls)
91
91
92 self.workspace_remove_patcher.stop()
92 self.workspace_remove_patcher.stop()
93 self.merge_patcher.stop()
93 self.merge_patcher.stop()
94 self.comment_patcher.stop()
94 self.comment_patcher.stop()
95 self.notification_patcher.stop()
95 self.notification_patcher.stop()
96 self.helper_patcher.stop()
96 self.helper_patcher.stop()
97 self.hook_patcher.stop()
97 self.hook_patcher.stop()
98 self.invalidation_patcher.stop()
98 self.invalidation_patcher.stop()
99
99
100 return self.pull_request
100 return self.pull_request
101
101
102 def test_get_all(self, pull_request):
102 def test_get_all(self, pull_request):
103 prs = PullRequestModel().get_all(pull_request.target_repo)
103 prs = PullRequestModel().get_all(pull_request.target_repo)
104 assert isinstance(prs, list)
104 assert isinstance(prs, list)
105 assert len(prs) == 1
105 assert len(prs) == 1
106
106
107 def test_count_all(self, pull_request):
107 def test_count_all(self, pull_request):
108 pr_count = PullRequestModel().count_all(pull_request.target_repo)
108 pr_count = PullRequestModel().count_all(pull_request.target_repo)
109 assert pr_count == 1
109 assert pr_count == 1
110
110
111 def test_get_awaiting_review(self, pull_request):
111 def test_get_awaiting_review(self, pull_request):
112 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
112 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
113 assert isinstance(prs, list)
113 assert isinstance(prs, list)
114 assert len(prs) == 1
114 assert len(prs) == 1
115
115
116 def test_count_awaiting_review(self, pull_request):
116 def test_count_awaiting_review(self, pull_request):
117 pr_count = PullRequestModel().count_awaiting_review(
117 pr_count = PullRequestModel().count_awaiting_review(
118 pull_request.target_repo)
118 pull_request.target_repo)
119 assert pr_count == 1
119 assert pr_count == 1
120
120
121 def test_get_awaiting_my_review(self, pull_request):
121 def test_get_awaiting_my_review(self, pull_request):
122 PullRequestModel().update_reviewers(
122 PullRequestModel().update_reviewers(
123 pull_request, [(pull_request.author, ['author'], False, [])],
123 pull_request, [(pull_request.author, ['author'], False, [])],
124 pull_request.author)
124 pull_request.author)
125 prs = PullRequestModel().get_awaiting_my_review(
125 prs = PullRequestModel().get_awaiting_my_review(
126 pull_request.target_repo, user_id=pull_request.author.user_id)
126 pull_request.target_repo, user_id=pull_request.author.user_id)
127 assert isinstance(prs, list)
127 assert isinstance(prs, list)
128 assert len(prs) == 1
128 assert len(prs) == 1
129
129
130 def test_count_awaiting_my_review(self, pull_request):
130 def test_count_awaiting_my_review(self, pull_request):
131 PullRequestModel().update_reviewers(
131 PullRequestModel().update_reviewers(
132 pull_request, [(pull_request.author, ['author'], False, [])],
132 pull_request, [(pull_request.author, ['author'], False, [])],
133 pull_request.author)
133 pull_request.author)
134 pr_count = PullRequestModel().count_awaiting_my_review(
134 pr_count = PullRequestModel().count_awaiting_my_review(
135 pull_request.target_repo, user_id=pull_request.author.user_id)
135 pull_request.target_repo, user_id=pull_request.author.user_id)
136 assert pr_count == 1
136 assert pr_count == 1
137
137
138 def test_delete_calls_cleanup_merge(self, pull_request):
138 def test_delete_calls_cleanup_merge(self, pull_request):
139 repo_id = pull_request.target_repo.repo_id
139 repo_id = pull_request.target_repo.repo_id
140 PullRequestModel().delete(pull_request, pull_request.author)
140 PullRequestModel().delete(pull_request, pull_request.author)
141
141
142 self.workspace_remove_mock.assert_called_once_with(
142 self.workspace_remove_mock.assert_called_once_with(
143 repo_id, self.workspace_id)
143 repo_id, self.workspace_id)
144
144
145 def test_close_calls_cleanup_and_hook(self, pull_request):
145 def test_close_calls_cleanup_and_hook(self, pull_request):
146 PullRequestModel().close_pull_request(
146 PullRequestModel().close_pull_request(
147 pull_request, pull_request.author)
147 pull_request, pull_request.author)
148 repo_id = pull_request.target_repo.repo_id
148 repo_id = pull_request.target_repo.repo_id
149
149
150 self.workspace_remove_mock.assert_called_once_with(
150 self.workspace_remove_mock.assert_called_once_with(
151 repo_id, self.workspace_id)
151 repo_id, self.workspace_id)
152 self.hook_mock.assert_called_with(
152 self.hook_mock.assert_called_with(
153 self.pull_request, self.pull_request.author, 'close')
153 self.pull_request, self.pull_request.author, 'close')
154
154
155 def test_merge_status(self, pull_request):
155 def test_merge_status(self, pull_request):
156 self.merge_mock.return_value = MergeResponse(
156 self.merge_mock.return_value = MergeResponse(
157 True, False, None, MergeFailureReason.NONE)
157 True, False, None, MergeFailureReason.NONE)
158
158
159 assert pull_request._last_merge_source_rev is None
159 assert pull_request._last_merge_source_rev is None
160 assert pull_request._last_merge_target_rev is None
160 assert pull_request._last_merge_target_rev is None
161 assert pull_request.last_merge_status is None
161 assert pull_request.last_merge_status is None
162
162
163 status, msg = PullRequestModel().merge_status(pull_request)
163 status, msg = PullRequestModel().merge_status(pull_request)
164 assert status is True
164 assert status is True
165 assert msg.eval() == 'This pull request can be automatically merged.'
165 assert msg.eval() == 'This pull request can be automatically merged.'
166 self.merge_mock.assert_called_with(
166 self.merge_mock.assert_called_with(
167 self.repo_id, self.workspace_id,
167 self.repo_id, self.workspace_id,
168 pull_request.target_ref_parts,
168 pull_request.target_ref_parts,
169 pull_request.source_repo.scm_instance(),
169 pull_request.source_repo.scm_instance(),
170 pull_request.source_ref_parts, dry_run=True,
170 pull_request.source_ref_parts, dry_run=True,
171 use_rebase=False, close_branch=False)
171 use_rebase=False, close_branch=False)
172
172
173 assert pull_request._last_merge_source_rev == self.source_commit
173 assert pull_request._last_merge_source_rev == self.source_commit
174 assert pull_request._last_merge_target_rev == self.target_commit
174 assert pull_request._last_merge_target_rev == self.target_commit
175 assert pull_request.last_merge_status is MergeFailureReason.NONE
175 assert pull_request.last_merge_status is MergeFailureReason.NONE
176
176
177 self.merge_mock.reset_mock()
177 self.merge_mock.reset_mock()
178 status, msg = PullRequestModel().merge_status(pull_request)
178 status, msg = PullRequestModel().merge_status(pull_request)
179 assert status is True
179 assert status is True
180 assert msg.eval() == 'This pull request can be automatically merged.'
180 assert msg.eval() == 'This pull request can be automatically merged.'
181 assert self.merge_mock.called is False
181 assert self.merge_mock.called is False
182
182
183 def test_merge_status_known_failure(self, pull_request):
183 def test_merge_status_known_failure(self, pull_request):
184 self.merge_mock.return_value = MergeResponse(
184 self.merge_mock.return_value = MergeResponse(
185 False, False, None, MergeFailureReason.MERGE_FAILED)
185 False, False, None, MergeFailureReason.MERGE_FAILED)
186
186
187 assert pull_request._last_merge_source_rev is None
187 assert pull_request._last_merge_source_rev is None
188 assert pull_request._last_merge_target_rev is None
188 assert pull_request._last_merge_target_rev is None
189 assert pull_request.last_merge_status is None
189 assert pull_request.last_merge_status is None
190
190
191 status, msg = PullRequestModel().merge_status(pull_request)
191 status, msg = PullRequestModel().merge_status(pull_request)
192 assert status is False
192 assert status is False
193 assert (
193 assert (
194 msg.eval() ==
194 msg.eval() ==
195 'This pull request cannot be merged because of merge conflicts.')
195 'This pull request cannot be merged because of merge conflicts.')
196 self.merge_mock.assert_called_with(
196 self.merge_mock.assert_called_with(
197 self.repo_id, self.workspace_id,
197 self.repo_id, self.workspace_id,
198 pull_request.target_ref_parts,
198 pull_request.target_ref_parts,
199 pull_request.source_repo.scm_instance(),
199 pull_request.source_repo.scm_instance(),
200 pull_request.source_ref_parts, dry_run=True,
200 pull_request.source_ref_parts, dry_run=True,
201 use_rebase=False, close_branch=False)
201 use_rebase=False, close_branch=False)
202
202
203 assert pull_request._last_merge_source_rev == self.source_commit
203 assert pull_request._last_merge_source_rev == self.source_commit
204 assert pull_request._last_merge_target_rev == self.target_commit
204 assert pull_request._last_merge_target_rev == self.target_commit
205 assert (
205 assert (
206 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
206 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
207
207
208 self.merge_mock.reset_mock()
208 self.merge_mock.reset_mock()
209 status, msg = PullRequestModel().merge_status(pull_request)
209 status, msg = PullRequestModel().merge_status(pull_request)
210 assert status is False
210 assert status is False
211 assert (
211 assert (
212 msg.eval() ==
212 msg.eval() ==
213 'This pull request cannot be merged because of merge conflicts.')
213 'This pull request cannot be merged because of merge conflicts.')
214 assert self.merge_mock.called is False
214 assert self.merge_mock.called is False
215
215
216 def test_merge_status_unknown_failure(self, pull_request):
216 def test_merge_status_unknown_failure(self, pull_request):
217 self.merge_mock.return_value = MergeResponse(
217 self.merge_mock.return_value = MergeResponse(
218 False, False, None, MergeFailureReason.UNKNOWN)
218 False, False, None, MergeFailureReason.UNKNOWN)
219
219
220 assert pull_request._last_merge_source_rev is None
220 assert pull_request._last_merge_source_rev is None
221 assert pull_request._last_merge_target_rev is None
221 assert pull_request._last_merge_target_rev is None
222 assert pull_request.last_merge_status is None
222 assert pull_request.last_merge_status is None
223
223
224 status, msg = PullRequestModel().merge_status(pull_request)
224 status, msg = PullRequestModel().merge_status(pull_request)
225 assert status is False
225 assert status is False
226 assert msg.eval() == (
226 assert msg.eval() == (
227 'This pull request cannot be merged because of an unhandled'
227 'This pull request cannot be merged because of an unhandled'
228 ' exception.')
228 ' exception.')
229 self.merge_mock.assert_called_with(
229 self.merge_mock.assert_called_with(
230 self.repo_id, self.workspace_id,
230 self.repo_id, self.workspace_id,
231 pull_request.target_ref_parts,
231 pull_request.target_ref_parts,
232 pull_request.source_repo.scm_instance(),
232 pull_request.source_repo.scm_instance(),
233 pull_request.source_ref_parts, dry_run=True,
233 pull_request.source_ref_parts, dry_run=True,
234 use_rebase=False, close_branch=False)
234 use_rebase=False, close_branch=False)
235
235
236 assert pull_request._last_merge_source_rev is None
236 assert pull_request._last_merge_source_rev is None
237 assert pull_request._last_merge_target_rev is None
237 assert pull_request._last_merge_target_rev is None
238 assert pull_request.last_merge_status is None
238 assert pull_request.last_merge_status is None
239
239
240 self.merge_mock.reset_mock()
240 self.merge_mock.reset_mock()
241 status, msg = PullRequestModel().merge_status(pull_request)
241 status, msg = PullRequestModel().merge_status(pull_request)
242 assert status is False
242 assert status is False
243 assert msg.eval() == (
243 assert msg.eval() == (
244 'This pull request cannot be merged because of an unhandled'
244 'This pull request cannot be merged because of an unhandled'
245 ' exception.')
245 ' exception.')
246 assert self.merge_mock.called is True
246 assert self.merge_mock.called is True
247
247
248 def test_merge_status_when_target_is_locked(self, pull_request):
248 def test_merge_status_when_target_is_locked(self, pull_request):
249 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
249 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
250 status, msg = PullRequestModel().merge_status(pull_request)
250 status, msg = PullRequestModel().merge_status(pull_request)
251 assert status is False
251 assert status is False
252 assert msg.eval() == (
252 assert msg.eval() == (
253 'This pull request cannot be merged because the target repository'
253 'This pull request cannot be merged because the target repository'
254 ' is locked.')
254 ' is locked.')
255
255
256 def test_merge_status_requirements_check_target(self, pull_request):
256 def test_merge_status_requirements_check_target(self, pull_request):
257
257
258 def has_largefiles(self, repo):
258 def has_largefiles(self, repo):
259 return repo == pull_request.source_repo
259 return repo == pull_request.source_repo
260
260
261 patcher = mock.patch.object(
261 patcher = mock.patch.object(
262 PullRequestModel, '_has_largefiles', has_largefiles)
262 PullRequestModel, '_has_largefiles', has_largefiles)
263 with patcher:
263 with patcher:
264 status, msg = PullRequestModel().merge_status(pull_request)
264 status, msg = PullRequestModel().merge_status(pull_request)
265
265
266 assert status is False
266 assert status is False
267 assert msg == 'Target repository large files support is disabled.'
267 assert msg == 'Target repository large files support is disabled.'
268
268
269 def test_merge_status_requirements_check_source(self, pull_request):
269 def test_merge_status_requirements_check_source(self, pull_request):
270
270
271 def has_largefiles(self, repo):
271 def has_largefiles(self, repo):
272 return repo == pull_request.target_repo
272 return repo == pull_request.target_repo
273
273
274 patcher = mock.patch.object(
274 patcher = mock.patch.object(
275 PullRequestModel, '_has_largefiles', has_largefiles)
275 PullRequestModel, '_has_largefiles', has_largefiles)
276 with patcher:
276 with patcher:
277 status, msg = PullRequestModel().merge_status(pull_request)
277 status, msg = PullRequestModel().merge_status(pull_request)
278
278
279 assert status is False
279 assert status is False
280 assert msg == 'Source repository large files support is disabled.'
280 assert msg == 'Source repository large files support is disabled.'
281
281
282 def test_merge(self, pull_request, merge_extras):
282 def test_merge(self, pull_request, merge_extras):
283 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
283 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
284 merge_ref = Reference(
284 merge_ref = Reference(
285 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
285 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
286 self.merge_mock.return_value = MergeResponse(
286 self.merge_mock.return_value = MergeResponse(
287 True, True, merge_ref, MergeFailureReason.NONE)
287 True, True, merge_ref, MergeFailureReason.NONE)
288
288
289 merge_extras['repository'] = pull_request.target_repo.repo_name
289 merge_extras['repository'] = pull_request.target_repo.repo_name
290 PullRequestModel().merge_repo(
290 PullRequestModel().merge_repo(
291 pull_request, pull_request.author, extras=merge_extras)
291 pull_request, pull_request.author, extras=merge_extras)
292
292
293 message = (
293 message = (
294 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
294 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
295 u'\n\n {pr_title}'.format(
295 u'\n\n {pr_title}'.format(
296 pr_id=pull_request.pull_request_id,
296 pr_id=pull_request.pull_request_id,
297 source_repo=safe_unicode(
297 source_repo=safe_unicode(
298 pull_request.source_repo.scm_instance().name),
298 pull_request.source_repo.scm_instance().name),
299 source_ref_name=pull_request.source_ref_parts.name,
299 source_ref_name=pull_request.source_ref_parts.name,
300 pr_title=safe_unicode(pull_request.title)
300 pr_title=safe_unicode(pull_request.title)
301 )
301 )
302 )
302 )
303 self.merge_mock.assert_called_with(
303 self.merge_mock.assert_called_with(
304 self.repo_id, self.workspace_id,
304 self.repo_id, self.workspace_id,
305 pull_request.target_ref_parts,
305 pull_request.target_ref_parts,
306 pull_request.source_repo.scm_instance(),
306 pull_request.source_repo.scm_instance(),
307 pull_request.source_ref_parts,
307 pull_request.source_ref_parts,
308 user_name=user.short_contact, user_email=user.email, message=message,
308 user_name=user.short_contact, user_email=user.email, message=message,
309 use_rebase=False, close_branch=False
309 use_rebase=False, close_branch=False
310 )
310 )
311 self.invalidation_mock.assert_called_once_with(
311 self.invalidation_mock.assert_called_once_with(
312 pull_request.target_repo.repo_name)
312 pull_request.target_repo.repo_name)
313
313
314 self.hook_mock.assert_called_with(
314 self.hook_mock.assert_called_with(
315 self.pull_request, self.pull_request.author, 'merge')
315 self.pull_request, self.pull_request.author, 'merge')
316
316
317 pull_request = PullRequest.get(pull_request.pull_request_id)
317 pull_request = PullRequest.get(pull_request.pull_request_id)
318 assert (
318 assert (
319 pull_request.merge_rev ==
319 pull_request.merge_rev ==
320 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
320 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
321
321
322 def test_merge_failed(self, pull_request, merge_extras):
322 def test_merge_failed(self, pull_request, merge_extras):
323 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
323 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
324 merge_ref = Reference(
324 merge_ref = Reference(
325 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
325 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
326 self.merge_mock.return_value = MergeResponse(
326 self.merge_mock.return_value = MergeResponse(
327 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
327 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
328
328
329 merge_extras['repository'] = pull_request.target_repo.repo_name
329 merge_extras['repository'] = pull_request.target_repo.repo_name
330 PullRequestModel().merge_repo(
330 PullRequestModel().merge_repo(
331 pull_request, pull_request.author, extras=merge_extras)
331 pull_request, pull_request.author, extras=merge_extras)
332
332
333 message = (
333 message = (
334 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
334 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
335 u'\n\n {pr_title}'.format(
335 u'\n\n {pr_title}'.format(
336 pr_id=pull_request.pull_request_id,
336 pr_id=pull_request.pull_request_id,
337 source_repo=safe_unicode(
337 source_repo=safe_unicode(
338 pull_request.source_repo.scm_instance().name),
338 pull_request.source_repo.scm_instance().name),
339 source_ref_name=pull_request.source_ref_parts.name,
339 source_ref_name=pull_request.source_ref_parts.name,
340 pr_title=safe_unicode(pull_request.title)
340 pr_title=safe_unicode(pull_request.title)
341 )
341 )
342 )
342 )
343 self.merge_mock.assert_called_with(
343 self.merge_mock.assert_called_with(
344 self.repo_id, self.workspace_id,
344 self.repo_id, self.workspace_id,
345 pull_request.target_ref_parts,
345 pull_request.target_ref_parts,
346 pull_request.source_repo.scm_instance(),
346 pull_request.source_repo.scm_instance(),
347 pull_request.source_ref_parts,
347 pull_request.source_ref_parts,
348 user_name=user.short_contact, user_email=user.email, message=message,
348 user_name=user.short_contact, user_email=user.email, message=message,
349 use_rebase=False, close_branch=False
349 use_rebase=False, close_branch=False
350 )
350 )
351
351
352 pull_request = PullRequest.get(pull_request.pull_request_id)
352 pull_request = PullRequest.get(pull_request.pull_request_id)
353 assert self.invalidation_mock.called is False
353 assert self.invalidation_mock.called is False
354 assert pull_request.merge_rev is None
354 assert pull_request.merge_rev is None
355
355
356 def test_get_commit_ids(self, pull_request):
356 def test_get_commit_ids(self, pull_request):
357 # The PR has been not merget yet, so expect an exception
357 # The PR has been not merget yet, so expect an exception
358 with pytest.raises(ValueError):
358 with pytest.raises(ValueError):
359 PullRequestModel()._get_commit_ids(pull_request)
359 PullRequestModel()._get_commit_ids(pull_request)
360
360
361 # Merge revision is in the revisions list
361 # Merge revision is in the revisions list
362 pull_request.merge_rev = pull_request.revisions[0]
362 pull_request.merge_rev = pull_request.revisions[0]
363 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
363 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
364 assert commit_ids == pull_request.revisions
364 assert commit_ids == pull_request.revisions
365
365
366 # Merge revision is not in the revisions list
366 # Merge revision is not in the revisions list
367 pull_request.merge_rev = 'f000' * 10
367 pull_request.merge_rev = 'f000' * 10
368 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
368 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
369 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
369 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
370
370
371 def test_get_diff_from_pr_version(self, pull_request):
371 def test_get_diff_from_pr_version(self, pull_request):
372 source_repo = pull_request.source_repo
372 source_repo = pull_request.source_repo
373 source_ref_id = pull_request.source_ref_parts.commit_id
373 source_ref_id = pull_request.source_ref_parts.commit_id
374 target_ref_id = pull_request.target_ref_parts.commit_id
374 target_ref_id = pull_request.target_ref_parts.commit_id
375 diff = PullRequestModel()._get_diff_from_pr_or_version(
375 diff = PullRequestModel()._get_diff_from_pr_or_version(
376 source_repo, source_ref_id, target_ref_id, context=6)
376 source_repo, source_ref_id, target_ref_id,
377 hide_whitespace_changes=False, diff_context=6)
377 assert 'file_1' in diff.raw
378 assert 'file_1' in diff.raw
378
379
379 def test_generate_title_returns_unicode(self):
380 def test_generate_title_returns_unicode(self):
380 title = PullRequestModel().generate_pullrequest_title(
381 title = PullRequestModel().generate_pullrequest_title(
381 source='source-dummy',
382 source='source-dummy',
382 source_ref='source-ref-dummy',
383 source_ref='source-ref-dummy',
383 target='target-dummy',
384 target='target-dummy',
384 )
385 )
385 assert type(title) == unicode
386 assert type(title) == unicode
386
387
387
388
388 @pytest.mark.usefixtures('config_stub')
389 @pytest.mark.usefixtures('config_stub')
389 class TestIntegrationMerge(object):
390 class TestIntegrationMerge(object):
390 @pytest.mark.parametrize('extra_config', (
391 @pytest.mark.parametrize('extra_config', (
391 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
392 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
392 ))
393 ))
393 def test_merge_triggers_push_hooks(
394 def test_merge_triggers_push_hooks(
394 self, pr_util, user_admin, capture_rcextensions, merge_extras,
395 self, pr_util, user_admin, capture_rcextensions, merge_extras,
395 extra_config):
396 extra_config):
396
397
397 pull_request = pr_util.create_pull_request(
398 pull_request = pr_util.create_pull_request(
398 approved=True, mergeable=True)
399 approved=True, mergeable=True)
399 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
400 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
400 merge_extras['repository'] = pull_request.target_repo.repo_name
401 merge_extras['repository'] = pull_request.target_repo.repo_name
401 Session().commit()
402 Session().commit()
402
403
403 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
404 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
404 merge_state = PullRequestModel().merge_repo(
405 merge_state = PullRequestModel().merge_repo(
405 pull_request, user_admin, extras=merge_extras)
406 pull_request, user_admin, extras=merge_extras)
406
407
407 assert merge_state.executed
408 assert merge_state.executed
408 assert '_pre_push_hook' in capture_rcextensions
409 assert '_pre_push_hook' in capture_rcextensions
409 assert '_push_hook' in capture_rcextensions
410 assert '_push_hook' in capture_rcextensions
410
411
411 def test_merge_can_be_rejected_by_pre_push_hook(
412 def test_merge_can_be_rejected_by_pre_push_hook(
412 self, pr_util, user_admin, capture_rcextensions, merge_extras):
413 self, pr_util, user_admin, capture_rcextensions, merge_extras):
413 pull_request = pr_util.create_pull_request(
414 pull_request = pr_util.create_pull_request(
414 approved=True, mergeable=True)
415 approved=True, mergeable=True)
415 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
416 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
416 merge_extras['repository'] = pull_request.target_repo.repo_name
417 merge_extras['repository'] = pull_request.target_repo.repo_name
417 Session().commit()
418 Session().commit()
418
419
419 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
420 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
420 pre_pull.side_effect = RepositoryError("Disallow push!")
421 pre_pull.side_effect = RepositoryError("Disallow push!")
421 merge_status = PullRequestModel().merge_repo(
422 merge_status = PullRequestModel().merge_repo(
422 pull_request, user_admin, extras=merge_extras)
423 pull_request, user_admin, extras=merge_extras)
423
424
424 assert not merge_status.executed
425 assert not merge_status.executed
425 assert 'pre_push' not in capture_rcextensions
426 assert 'pre_push' not in capture_rcextensions
426 assert 'post_push' not in capture_rcextensions
427 assert 'post_push' not in capture_rcextensions
427
428
428 def test_merge_fails_if_target_is_locked(
429 def test_merge_fails_if_target_is_locked(
429 self, pr_util, user_regular, merge_extras):
430 self, pr_util, user_regular, merge_extras):
430 pull_request = pr_util.create_pull_request(
431 pull_request = pr_util.create_pull_request(
431 approved=True, mergeable=True)
432 approved=True, mergeable=True)
432 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
433 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
433 pull_request.target_repo.locked = locked_by
434 pull_request.target_repo.locked = locked_by
434 # TODO: johbo: Check if this can work based on the database, currently
435 # TODO: johbo: Check if this can work based on the database, currently
435 # all data is pre-computed, that's why just updating the DB is not
436 # all data is pre-computed, that's why just updating the DB is not
436 # enough.
437 # enough.
437 merge_extras['locked_by'] = locked_by
438 merge_extras['locked_by'] = locked_by
438 merge_extras['repository'] = pull_request.target_repo.repo_name
439 merge_extras['repository'] = pull_request.target_repo.repo_name
439 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
440 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
440 Session().commit()
441 Session().commit()
441 merge_status = PullRequestModel().merge_repo(
442 merge_status = PullRequestModel().merge_repo(
442 pull_request, user_regular, extras=merge_extras)
443 pull_request, user_regular, extras=merge_extras)
443 assert not merge_status.executed
444 assert not merge_status.executed
444
445
445
446
446 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
447 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
447 (False, 1, 0),
448 (False, 1, 0),
448 (True, 0, 1),
449 (True, 0, 1),
449 ])
450 ])
450 def test_outdated_comments(
451 def test_outdated_comments(
451 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
452 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
452 pull_request = pr_util.create_pull_request()
453 pull_request = pr_util.create_pull_request()
453 pr_util.create_inline_comment(file_path='not_in_updated_diff')
454 pr_util.create_inline_comment(file_path='not_in_updated_diff')
454
455
455 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
456 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
456 pr_util.add_one_commit()
457 pr_util.add_one_commit()
457 assert_inline_comments(
458 assert_inline_comments(
458 pull_request, visible=inlines_count, outdated=outdated_count)
459 pull_request, visible=inlines_count, outdated=outdated_count)
459 outdated_comment_mock.assert_called_with(pull_request)
460 outdated_comment_mock.assert_called_with(pull_request)
460
461
461
462
462 @pytest.fixture
463 @pytest.fixture
463 def merge_extras(user_regular):
464 def merge_extras(user_regular):
464 """
465 """
465 Context for the vcs operation when running a merge.
466 Context for the vcs operation when running a merge.
466 """
467 """
467 extras = {
468 extras = {
468 'ip': '127.0.0.1',
469 'ip': '127.0.0.1',
469 'username': user_regular.username,
470 'username': user_regular.username,
470 'user_id': user_regular.user_id,
471 'user_id': user_regular.user_id,
471 'action': 'push',
472 'action': 'push',
472 'repository': 'fake_target_repo_name',
473 'repository': 'fake_target_repo_name',
473 'scm': 'git',
474 'scm': 'git',
474 'config': 'fake_config_ini_path',
475 'config': 'fake_config_ini_path',
475 'repo_store': '',
476 'repo_store': '',
476 'make_lock': None,
477 'make_lock': None,
477 'locked_by': [None, None, None],
478 'locked_by': [None, None, None],
478 'server_url': 'http://test.example.com:5000',
479 'server_url': 'http://test.example.com:5000',
479 'hooks': ['push', 'pull'],
480 'hooks': ['push', 'pull'],
480 'is_shadow_repo': False,
481 'is_shadow_repo': False,
481 }
482 }
482 return extras
483 return extras
483
484
484
485
485 @pytest.mark.usefixtures('config_stub')
486 @pytest.mark.usefixtures('config_stub')
486 class TestUpdateCommentHandling(object):
487 class TestUpdateCommentHandling(object):
487
488
488 @pytest.fixture(autouse=True, scope='class')
489 @pytest.fixture(autouse=True, scope='class')
489 def enable_outdated_comments(self, request, baseapp):
490 def enable_outdated_comments(self, request, baseapp):
490 config_patch = mock.patch.dict(
491 config_patch = mock.patch.dict(
491 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
492 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
492 config_patch.start()
493 config_patch.start()
493
494
494 @request.addfinalizer
495 @request.addfinalizer
495 def cleanup():
496 def cleanup():
496 config_patch.stop()
497 config_patch.stop()
497
498
498 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
499 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
499 commits = [
500 commits = [
500 {'message': 'a'},
501 {'message': 'a'},
501 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
502 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
502 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
503 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
503 ]
504 ]
504 pull_request = pr_util.create_pull_request(
505 pull_request = pr_util.create_pull_request(
505 commits=commits, target_head='a', source_head='b', revisions=['b'])
506 commits=commits, target_head='a', source_head='b', revisions=['b'])
506 pr_util.create_inline_comment(file_path='file_b')
507 pr_util.create_inline_comment(file_path='file_b')
507 pr_util.add_one_commit(head='c')
508 pr_util.add_one_commit(head='c')
508
509
509 assert_inline_comments(pull_request, visible=1, outdated=0)
510 assert_inline_comments(pull_request, visible=1, outdated=0)
510
511
511 def test_comment_stays_unflagged_on_change_above(self, pr_util):
512 def test_comment_stays_unflagged_on_change_above(self, pr_util):
512 original_content = ''.join(
513 original_content = ''.join(
513 ['line {}\n'.format(x) for x in range(1, 11)])
514 ['line {}\n'.format(x) for x in range(1, 11)])
514 updated_content = 'new_line_at_top\n' + original_content
515 updated_content = 'new_line_at_top\n' + original_content
515 commits = [
516 commits = [
516 {'message': 'a'},
517 {'message': 'a'},
517 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
518 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
518 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
519 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
519 ]
520 ]
520 pull_request = pr_util.create_pull_request(
521 pull_request = pr_util.create_pull_request(
521 commits=commits, target_head='a', source_head='b', revisions=['b'])
522 commits=commits, target_head='a', source_head='b', revisions=['b'])
522
523
523 with outdated_comments_patcher():
524 with outdated_comments_patcher():
524 comment = pr_util.create_inline_comment(
525 comment = pr_util.create_inline_comment(
525 line_no=u'n8', file_path='file_b')
526 line_no=u'n8', file_path='file_b')
526 pr_util.add_one_commit(head='c')
527 pr_util.add_one_commit(head='c')
527
528
528 assert_inline_comments(pull_request, visible=1, outdated=0)
529 assert_inline_comments(pull_request, visible=1, outdated=0)
529 assert comment.line_no == u'n9'
530 assert comment.line_no == u'n9'
530
531
531 def test_comment_stays_unflagged_on_change_below(self, pr_util):
532 def test_comment_stays_unflagged_on_change_below(self, pr_util):
532 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
533 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
533 updated_content = original_content + 'new_line_at_end\n'
534 updated_content = original_content + 'new_line_at_end\n'
534 commits = [
535 commits = [
535 {'message': 'a'},
536 {'message': 'a'},
536 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
537 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
537 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
538 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
538 ]
539 ]
539 pull_request = pr_util.create_pull_request(
540 pull_request = pr_util.create_pull_request(
540 commits=commits, target_head='a', source_head='b', revisions=['b'])
541 commits=commits, target_head='a', source_head='b', revisions=['b'])
541 pr_util.create_inline_comment(file_path='file_b')
542 pr_util.create_inline_comment(file_path='file_b')
542 pr_util.add_one_commit(head='c')
543 pr_util.add_one_commit(head='c')
543
544
544 assert_inline_comments(pull_request, visible=1, outdated=0)
545 assert_inline_comments(pull_request, visible=1, outdated=0)
545
546
546 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
547 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
547 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
548 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
548 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
549 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
549 change_lines = list(base_lines)
550 change_lines = list(base_lines)
550 change_lines.insert(6, 'line 6a added\n')
551 change_lines.insert(6, 'line 6a added\n')
551
552
552 # Changes on the last line of sight
553 # Changes on the last line of sight
553 update_lines = list(change_lines)
554 update_lines = list(change_lines)
554 update_lines[0] = 'line 1 changed\n'
555 update_lines[0] = 'line 1 changed\n'
555 update_lines[-1] = 'line 12 changed\n'
556 update_lines[-1] = 'line 12 changed\n'
556
557
557 def file_b(lines):
558 def file_b(lines):
558 return FileNode('file_b', ''.join(lines))
559 return FileNode('file_b', ''.join(lines))
559
560
560 commits = [
561 commits = [
561 {'message': 'a', 'added': [file_b(base_lines)]},
562 {'message': 'a', 'added': [file_b(base_lines)]},
562 {'message': 'b', 'changed': [file_b(change_lines)]},
563 {'message': 'b', 'changed': [file_b(change_lines)]},
563 {'message': 'c', 'changed': [file_b(update_lines)]},
564 {'message': 'c', 'changed': [file_b(update_lines)]},
564 ]
565 ]
565
566
566 pull_request = pr_util.create_pull_request(
567 pull_request = pr_util.create_pull_request(
567 commits=commits, target_head='a', source_head='b', revisions=['b'])
568 commits=commits, target_head='a', source_head='b', revisions=['b'])
568 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
569 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
569
570
570 with outdated_comments_patcher():
571 with outdated_comments_patcher():
571 pr_util.add_one_commit(head='c')
572 pr_util.add_one_commit(head='c')
572 assert_inline_comments(pull_request, visible=0, outdated=1)
573 assert_inline_comments(pull_request, visible=0, outdated=1)
573
574
574 @pytest.mark.parametrize("change, content", [
575 @pytest.mark.parametrize("change, content", [
575 ('changed', 'changed\n'),
576 ('changed', 'changed\n'),
576 ('removed', ''),
577 ('removed', ''),
577 ], ids=['changed', 'removed'])
578 ], ids=['changed', 'removed'])
578 def test_comment_flagged_on_change(self, pr_util, change, content):
579 def test_comment_flagged_on_change(self, pr_util, change, content):
579 commits = [
580 commits = [
580 {'message': 'a'},
581 {'message': 'a'},
581 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
582 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
582 {'message': 'c', change: [FileNode('file_b', content)]},
583 {'message': 'c', change: [FileNode('file_b', content)]},
583 ]
584 ]
584 pull_request = pr_util.create_pull_request(
585 pull_request = pr_util.create_pull_request(
585 commits=commits, target_head='a', source_head='b', revisions=['b'])
586 commits=commits, target_head='a', source_head='b', revisions=['b'])
586 pr_util.create_inline_comment(file_path='file_b')
587 pr_util.create_inline_comment(file_path='file_b')
587
588
588 with outdated_comments_patcher():
589 with outdated_comments_patcher():
589 pr_util.add_one_commit(head='c')
590 pr_util.add_one_commit(head='c')
590 assert_inline_comments(pull_request, visible=0, outdated=1)
591 assert_inline_comments(pull_request, visible=0, outdated=1)
591
592
592
593
593 @pytest.mark.usefixtures('config_stub')
594 @pytest.mark.usefixtures('config_stub')
594 class TestUpdateChangedFiles(object):
595 class TestUpdateChangedFiles(object):
595
596
596 def test_no_changes_on_unchanged_diff(self, pr_util):
597 def test_no_changes_on_unchanged_diff(self, pr_util):
597 commits = [
598 commits = [
598 {'message': 'a'},
599 {'message': 'a'},
599 {'message': 'b',
600 {'message': 'b',
600 'added': [FileNode('file_b', 'test_content b\n')]},
601 'added': [FileNode('file_b', 'test_content b\n')]},
601 {'message': 'c',
602 {'message': 'c',
602 'added': [FileNode('file_c', 'test_content c\n')]},
603 'added': [FileNode('file_c', 'test_content c\n')]},
603 ]
604 ]
604 # open a PR from a to b, adding file_b
605 # open a PR from a to b, adding file_b
605 pull_request = pr_util.create_pull_request(
606 pull_request = pr_util.create_pull_request(
606 commits=commits, target_head='a', source_head='b', revisions=['b'],
607 commits=commits, target_head='a', source_head='b', revisions=['b'],
607 name_suffix='per-file-review')
608 name_suffix='per-file-review')
608
609
609 # modify PR adding new file file_c
610 # modify PR adding new file file_c
610 pr_util.add_one_commit(head='c')
611 pr_util.add_one_commit(head='c')
611
612
612 assert_pr_file_changes(
613 assert_pr_file_changes(
613 pull_request,
614 pull_request,
614 added=['file_c'],
615 added=['file_c'],
615 modified=[],
616 modified=[],
616 removed=[])
617 removed=[])
617
618
618 def test_modify_and_undo_modification_diff(self, pr_util):
619 def test_modify_and_undo_modification_diff(self, pr_util):
619 commits = [
620 commits = [
620 {'message': 'a'},
621 {'message': 'a'},
621 {'message': 'b',
622 {'message': 'b',
622 'added': [FileNode('file_b', 'test_content b\n')]},
623 'added': [FileNode('file_b', 'test_content b\n')]},
623 {'message': 'c',
624 {'message': 'c',
624 'changed': [FileNode('file_b', 'test_content b modified\n')]},
625 'changed': [FileNode('file_b', 'test_content b modified\n')]},
625 {'message': 'd',
626 {'message': 'd',
626 'changed': [FileNode('file_b', 'test_content b\n')]},
627 'changed': [FileNode('file_b', 'test_content b\n')]},
627 ]
628 ]
628 # open a PR from a to b, adding file_b
629 # open a PR from a to b, adding file_b
629 pull_request = pr_util.create_pull_request(
630 pull_request = pr_util.create_pull_request(
630 commits=commits, target_head='a', source_head='b', revisions=['b'],
631 commits=commits, target_head='a', source_head='b', revisions=['b'],
631 name_suffix='per-file-review')
632 name_suffix='per-file-review')
632
633
633 # modify PR modifying file file_b
634 # modify PR modifying file file_b
634 pr_util.add_one_commit(head='c')
635 pr_util.add_one_commit(head='c')
635
636
636 assert_pr_file_changes(
637 assert_pr_file_changes(
637 pull_request,
638 pull_request,
638 added=[],
639 added=[],
639 modified=['file_b'],
640 modified=['file_b'],
640 removed=[])
641 removed=[])
641
642
642 # move the head again to d, which rollbacks change,
643 # move the head again to d, which rollbacks change,
643 # meaning we should indicate no changes
644 # meaning we should indicate no changes
644 pr_util.add_one_commit(head='d')
645 pr_util.add_one_commit(head='d')
645
646
646 assert_pr_file_changes(
647 assert_pr_file_changes(
647 pull_request,
648 pull_request,
648 added=[],
649 added=[],
649 modified=[],
650 modified=[],
650 removed=[])
651 removed=[])
651
652
652 def test_updated_all_files_in_pr(self, pr_util):
653 def test_updated_all_files_in_pr(self, pr_util):
653 commits = [
654 commits = [
654 {'message': 'a'},
655 {'message': 'a'},
655 {'message': 'b', 'added': [
656 {'message': 'b', 'added': [
656 FileNode('file_a', 'test_content a\n'),
657 FileNode('file_a', 'test_content a\n'),
657 FileNode('file_b', 'test_content b\n'),
658 FileNode('file_b', 'test_content b\n'),
658 FileNode('file_c', 'test_content c\n')]},
659 FileNode('file_c', 'test_content c\n')]},
659 {'message': 'c', 'changed': [
660 {'message': 'c', 'changed': [
660 FileNode('file_a', 'test_content a changed\n'),
661 FileNode('file_a', 'test_content a changed\n'),
661 FileNode('file_b', 'test_content b changed\n'),
662 FileNode('file_b', 'test_content b changed\n'),
662 FileNode('file_c', 'test_content c changed\n')]},
663 FileNode('file_c', 'test_content c changed\n')]},
663 ]
664 ]
664 # open a PR from a to b, changing 3 files
665 # open a PR from a to b, changing 3 files
665 pull_request = pr_util.create_pull_request(
666 pull_request = pr_util.create_pull_request(
666 commits=commits, target_head='a', source_head='b', revisions=['b'],
667 commits=commits, target_head='a', source_head='b', revisions=['b'],
667 name_suffix='per-file-review')
668 name_suffix='per-file-review')
668
669
669 pr_util.add_one_commit(head='c')
670 pr_util.add_one_commit(head='c')
670
671
671 assert_pr_file_changes(
672 assert_pr_file_changes(
672 pull_request,
673 pull_request,
673 added=[],
674 added=[],
674 modified=['file_a', 'file_b', 'file_c'],
675 modified=['file_a', 'file_b', 'file_c'],
675 removed=[])
676 removed=[])
676
677
677 def test_updated_and_removed_all_files_in_pr(self, pr_util):
678 def test_updated_and_removed_all_files_in_pr(self, pr_util):
678 commits = [
679 commits = [
679 {'message': 'a'},
680 {'message': 'a'},
680 {'message': 'b', 'added': [
681 {'message': 'b', 'added': [
681 FileNode('file_a', 'test_content a\n'),
682 FileNode('file_a', 'test_content a\n'),
682 FileNode('file_b', 'test_content b\n'),
683 FileNode('file_b', 'test_content b\n'),
683 FileNode('file_c', 'test_content c\n')]},
684 FileNode('file_c', 'test_content c\n')]},
684 {'message': 'c', 'removed': [
685 {'message': 'c', 'removed': [
685 FileNode('file_a', 'test_content a changed\n'),
686 FileNode('file_a', 'test_content a changed\n'),
686 FileNode('file_b', 'test_content b changed\n'),
687 FileNode('file_b', 'test_content b changed\n'),
687 FileNode('file_c', 'test_content c changed\n')]},
688 FileNode('file_c', 'test_content c changed\n')]},
688 ]
689 ]
689 # open a PR from a to b, removing 3 files
690 # open a PR from a to b, removing 3 files
690 pull_request = pr_util.create_pull_request(
691 pull_request = pr_util.create_pull_request(
691 commits=commits, target_head='a', source_head='b', revisions=['b'],
692 commits=commits, target_head='a', source_head='b', revisions=['b'],
692 name_suffix='per-file-review')
693 name_suffix='per-file-review')
693
694
694 pr_util.add_one_commit(head='c')
695 pr_util.add_one_commit(head='c')
695
696
696 assert_pr_file_changes(
697 assert_pr_file_changes(
697 pull_request,
698 pull_request,
698 added=[],
699 added=[],
699 modified=[],
700 modified=[],
700 removed=['file_a', 'file_b', 'file_c'])
701 removed=['file_a', 'file_b', 'file_c'])
701
702
702
703
703 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
704 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
704 model = PullRequestModel()
705 model = PullRequestModel()
705 pull_request = pr_util.create_pull_request()
706 pull_request = pr_util.create_pull_request()
706 pr_util.update_source_repository()
707 pr_util.update_source_repository()
707
708
708 model.update_commits(pull_request)
709 model.update_commits(pull_request)
709
710
710 # Expect that it has a version entry now
711 # Expect that it has a version entry now
711 assert len(model.get_versions(pull_request)) == 1
712 assert len(model.get_versions(pull_request)) == 1
712
713
713
714
714 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
715 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
715 pull_request = pr_util.create_pull_request()
716 pull_request = pr_util.create_pull_request()
716 model = PullRequestModel()
717 model = PullRequestModel()
717 model.update_commits(pull_request)
718 model.update_commits(pull_request)
718
719
719 # Expect that it still has no versions
720 # Expect that it still has no versions
720 assert len(model.get_versions(pull_request)) == 0
721 assert len(model.get_versions(pull_request)) == 0
721
722
722
723
723 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
724 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
724 model = PullRequestModel()
725 model = PullRequestModel()
725 pull_request = pr_util.create_pull_request()
726 pull_request = pr_util.create_pull_request()
726 comment = pr_util.create_comment()
727 comment = pr_util.create_comment()
727 pr_util.update_source_repository()
728 pr_util.update_source_repository()
728
729
729 model.update_commits(pull_request)
730 model.update_commits(pull_request)
730
731
731 # Expect that the comment is linked to the pr version now
732 # Expect that the comment is linked to the pr version now
732 assert comment.pull_request_version == model.get_versions(pull_request)[0]
733 assert comment.pull_request_version == model.get_versions(pull_request)[0]
733
734
734
735
735 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
736 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
736 model = PullRequestModel()
737 model = PullRequestModel()
737 pull_request = pr_util.create_pull_request()
738 pull_request = pr_util.create_pull_request()
738 pr_util.update_source_repository()
739 pr_util.update_source_repository()
739 pr_util.update_source_repository()
740 pr_util.update_source_repository()
740
741
741 model.update_commits(pull_request)
742 model.update_commits(pull_request)
742
743
743 # Expect to find a new comment about the change
744 # Expect to find a new comment about the change
744 expected_message = textwrap.dedent(
745 expected_message = textwrap.dedent(
745 """\
746 """\
746 Pull request updated. Auto status change to |under_review|
747 Pull request updated. Auto status change to |under_review|
747
748
748 .. role:: added
749 .. role:: added
749 .. role:: removed
750 .. role:: removed
750 .. parsed-literal::
751 .. parsed-literal::
751
752
752 Changed commits:
753 Changed commits:
753 * :added:`1 added`
754 * :added:`1 added`
754 * :removed:`0 removed`
755 * :removed:`0 removed`
755
756
756 Changed files:
757 Changed files:
757 * `A file_2 <#a_c--92ed3b5f07b4>`_
758 * `A file_2 <#a_c--92ed3b5f07b4>`_
758
759
759 .. |under_review| replace:: *"Under Review"*"""
760 .. |under_review| replace:: *"Under Review"*"""
760 )
761 )
761 pull_request_comments = sorted(
762 pull_request_comments = sorted(
762 pull_request.comments, key=lambda c: c.modified_at)
763 pull_request.comments, key=lambda c: c.modified_at)
763 update_comment = pull_request_comments[-1]
764 update_comment = pull_request_comments[-1]
764 assert update_comment.text == expected_message
765 assert update_comment.text == expected_message
765
766
766
767
767 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
768 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
768 pull_request = pr_util.create_pull_request()
769 pull_request = pr_util.create_pull_request()
769
770
770 # Avoiding default values
771 # Avoiding default values
771 pull_request.status = PullRequest.STATUS_CLOSED
772 pull_request.status = PullRequest.STATUS_CLOSED
772 pull_request._last_merge_source_rev = "0" * 40
773 pull_request._last_merge_source_rev = "0" * 40
773 pull_request._last_merge_target_rev = "1" * 40
774 pull_request._last_merge_target_rev = "1" * 40
774 pull_request.last_merge_status = 1
775 pull_request.last_merge_status = 1
775 pull_request.merge_rev = "2" * 40
776 pull_request.merge_rev = "2" * 40
776
777
777 # Remember automatic values
778 # Remember automatic values
778 created_on = pull_request.created_on
779 created_on = pull_request.created_on
779 updated_on = pull_request.updated_on
780 updated_on = pull_request.updated_on
780
781
781 # Create a new version of the pull request
782 # Create a new version of the pull request
782 version = PullRequestModel()._create_version_from_snapshot(pull_request)
783 version = PullRequestModel()._create_version_from_snapshot(pull_request)
783
784
784 # Check attributes
785 # Check attributes
785 assert version.title == pr_util.create_parameters['title']
786 assert version.title == pr_util.create_parameters['title']
786 assert version.description == pr_util.create_parameters['description']
787 assert version.description == pr_util.create_parameters['description']
787 assert version.status == PullRequest.STATUS_CLOSED
788 assert version.status == PullRequest.STATUS_CLOSED
788
789
789 # versions get updated created_on
790 # versions get updated created_on
790 assert version.created_on != created_on
791 assert version.created_on != created_on
791
792
792 assert version.updated_on == updated_on
793 assert version.updated_on == updated_on
793 assert version.user_id == pull_request.user_id
794 assert version.user_id == pull_request.user_id
794 assert version.revisions == pr_util.create_parameters['revisions']
795 assert version.revisions == pr_util.create_parameters['revisions']
795 assert version.source_repo == pr_util.source_repository
796 assert version.source_repo == pr_util.source_repository
796 assert version.source_ref == pr_util.create_parameters['source_ref']
797 assert version.source_ref == pr_util.create_parameters['source_ref']
797 assert version.target_repo == pr_util.target_repository
798 assert version.target_repo == pr_util.target_repository
798 assert version.target_ref == pr_util.create_parameters['target_ref']
799 assert version.target_ref == pr_util.create_parameters['target_ref']
799 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
800 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
800 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
801 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
801 assert version.last_merge_status == pull_request.last_merge_status
802 assert version.last_merge_status == pull_request.last_merge_status
802 assert version.merge_rev == pull_request.merge_rev
803 assert version.merge_rev == pull_request.merge_rev
803 assert version.pull_request == pull_request
804 assert version.pull_request == pull_request
804
805
805
806
806 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
807 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
807 version1 = pr_util.create_version_of_pull_request()
808 version1 = pr_util.create_version_of_pull_request()
808 comment_linked = pr_util.create_comment(linked_to=version1)
809 comment_linked = pr_util.create_comment(linked_to=version1)
809 comment_unlinked = pr_util.create_comment()
810 comment_unlinked = pr_util.create_comment()
810 version2 = pr_util.create_version_of_pull_request()
811 version2 = pr_util.create_version_of_pull_request()
811
812
812 PullRequestModel()._link_comments_to_version(version2)
813 PullRequestModel()._link_comments_to_version(version2)
813
814
814 # Expect that only the new comment is linked to version2
815 # Expect that only the new comment is linked to version2
815 assert (
816 assert (
816 comment_unlinked.pull_request_version_id ==
817 comment_unlinked.pull_request_version_id ==
817 version2.pull_request_version_id)
818 version2.pull_request_version_id)
818 assert (
819 assert (
819 comment_linked.pull_request_version_id ==
820 comment_linked.pull_request_version_id ==
820 version1.pull_request_version_id)
821 version1.pull_request_version_id)
821 assert (
822 assert (
822 comment_unlinked.pull_request_version_id !=
823 comment_unlinked.pull_request_version_id !=
823 comment_linked.pull_request_version_id)
824 comment_linked.pull_request_version_id)
824
825
825
826
826 def test_calculate_commits():
827 def test_calculate_commits():
827 old_ids = [1, 2, 3]
828 old_ids = [1, 2, 3]
828 new_ids = [1, 3, 4, 5]
829 new_ids = [1, 3, 4, 5]
829 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
830 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
830 assert change.added == [4, 5]
831 assert change.added == [4, 5]
831 assert change.common == [1, 3]
832 assert change.common == [1, 3]
832 assert change.removed == [2]
833 assert change.removed == [2]
833 assert change.total == [1, 3, 4, 5]
834 assert change.total == [1, 3, 4, 5]
834
835
835
836
836 def assert_inline_comments(pull_request, visible=None, outdated=None):
837 def assert_inline_comments(pull_request, visible=None, outdated=None):
837 if visible is not None:
838 if visible is not None:
838 inline_comments = CommentsModel().get_inline_comments(
839 inline_comments = CommentsModel().get_inline_comments(
839 pull_request.target_repo.repo_id, pull_request=pull_request)
840 pull_request.target_repo.repo_id, pull_request=pull_request)
840 inline_cnt = CommentsModel().get_inline_comments_count(
841 inline_cnt = CommentsModel().get_inline_comments_count(
841 inline_comments)
842 inline_comments)
842 assert inline_cnt == visible
843 assert inline_cnt == visible
843 if outdated is not None:
844 if outdated is not None:
844 outdated_comments = CommentsModel().get_outdated_comments(
845 outdated_comments = CommentsModel().get_outdated_comments(
845 pull_request.target_repo.repo_id, pull_request)
846 pull_request.target_repo.repo_id, pull_request)
846 assert len(outdated_comments) == outdated
847 assert len(outdated_comments) == outdated
847
848
848
849
849 def assert_pr_file_changes(
850 def assert_pr_file_changes(
850 pull_request, added=None, modified=None, removed=None):
851 pull_request, added=None, modified=None, removed=None):
851 pr_versions = PullRequestModel().get_versions(pull_request)
852 pr_versions = PullRequestModel().get_versions(pull_request)
852 # always use first version, ie original PR to calculate changes
853 # always use first version, ie original PR to calculate changes
853 pull_request_version = pr_versions[0]
854 pull_request_version = pr_versions[0]
854 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
855 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
855 pull_request, pull_request_version)
856 pull_request, pull_request_version)
856 file_changes = PullRequestModel()._calculate_file_changes(
857 file_changes = PullRequestModel()._calculate_file_changes(
857 old_diff_data, new_diff_data)
858 old_diff_data, new_diff_data)
858
859
859 assert added == file_changes.added, \
860 assert added == file_changes.added, \
860 'expected added:%s vs value:%s' % (added, file_changes.added)
861 'expected added:%s vs value:%s' % (added, file_changes.added)
861 assert modified == file_changes.modified, \
862 assert modified == file_changes.modified, \
862 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
863 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
863 assert removed == file_changes.removed, \
864 assert removed == file_changes.removed, \
864 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
865 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
865
866
866
867
867 def outdated_comments_patcher(use_outdated=True):
868 def outdated_comments_patcher(use_outdated=True):
868 return mock.patch.object(
869 return mock.patch.object(
869 CommentsModel, 'use_outdated_comments',
870 CommentsModel, 'use_outdated_comments',
870 return_value=use_outdated)
871 return_value=use_outdated)
@@ -1,70 +1,71 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.vcs.nodes import FileNode
23 from rhodecode.lib.vcs.nodes import FileNode
24 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
24 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26
26
27
27
28 @pytest.mark.usefixtures('config_stub')
28 @pytest.mark.usefixtures('config_stub')
29 @pytest.mark.backends('git')
29 @pytest.mark.backends('git')
30 class TestGetDiffForPrOrVersion(object):
30 class TestGetDiffForPrOrVersion(object):
31
31
32 def test_works_for_missing_git_references(self, pr_util):
32 def test_works_for_missing_git_references(self, pr_util):
33 pull_request = self._prepare_pull_request(pr_util)
33 pull_request = self._prepare_pull_request(pr_util)
34 removed_commit_id = pr_util.remove_one_commit()
34 removed_commit_id = pr_util.remove_one_commit()
35 self.assert_commit_cannot_be_accessed(removed_commit_id, pull_request)
35 self.assert_commit_cannot_be_accessed(removed_commit_id, pull_request)
36
36
37 self.assert_diff_can_be_fetched(pull_request)
37 self.assert_diff_can_be_fetched(pull_request)
38
38
39 def test_works_for_missing_git_references_during_update(self, pr_util):
39 def test_works_for_missing_git_references_during_update(self, pr_util):
40 pull_request = self._prepare_pull_request(pr_util)
40 pull_request = self._prepare_pull_request(pr_util)
41 removed_commit_id = pr_util.remove_one_commit()
41 removed_commit_id = pr_util.remove_one_commit()
42 self.assert_commit_cannot_be_accessed(removed_commit_id, pull_request)
42 self.assert_commit_cannot_be_accessed(removed_commit_id, pull_request)
43
43
44 pr_version = PullRequestModel().get_versions(pull_request)[0]
44 pr_version = PullRequestModel().get_versions(pull_request)[0]
45 self.assert_diff_can_be_fetched(pr_version)
45 self.assert_diff_can_be_fetched(pr_version)
46
46
47 def _prepare_pull_request(self, pr_util):
47 def _prepare_pull_request(self, pr_util):
48 commits = [
48 commits = [
49 {'message': 'a'},
49 {'message': 'a'},
50 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
50 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
51 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
51 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
52 ]
52 ]
53 pull_request = pr_util.create_pull_request(
53 pull_request = pr_util.create_pull_request(
54 commits=commits, target_head='a', source_head='c',
54 commits=commits, target_head='a', source_head='c',
55 revisions=['b', 'c'])
55 revisions=['b', 'c'])
56 return pull_request
56 return pull_request
57
57
58 def assert_diff_can_be_fetched(self, pr_or_version):
58 def assert_diff_can_be_fetched(self, pr_or_version):
59 source_repo = pr_or_version.source_repo
59 source_repo = pr_or_version.source_repo
60 source_ref_id = pr_or_version.source_ref_parts.commit_id
60 source_ref_id = pr_or_version.source_ref_parts.commit_id
61 target_ref_id = pr_or_version.target_ref_parts.commit_id
61 target_ref_id = pr_or_version.target_ref_parts.commit_id
62 diff = PullRequestModel()._get_diff_from_pr_or_version(
62 diff = PullRequestModel()._get_diff_from_pr_or_version(
63 source_repo, source_ref_id, target_ref_id, context=6)
63 source_repo, source_ref_id, target_ref_id,
64 hide_whitespace_changes=False, diff_context=6)
64 assert 'file_b' in diff.raw
65 assert 'file_b' in diff.raw
65
66
66 def assert_commit_cannot_be_accessed(
67 def assert_commit_cannot_be_accessed(
67 self, removed_commit_id, pull_request):
68 self, removed_commit_id, pull_request):
68 source_vcs = pull_request.source_repo.scm_instance()
69 source_vcs = pull_request.source_repo.scm_instance()
69 with pytest.raises(CommitDoesNotExistError):
70 with pytest.raises(CommitDoesNotExistError):
70 source_vcs.get_commit(commit_id=removed_commit_id)
71 source_vcs.get_commit(commit_id=removed_commit_id)
General Comments 0
You need to be logged in to leave comments. Login now