##// END OF EJS Templates
comments: properly show version of pull request into added comments....
marcink -
r1286:e783fdd1 default
parent child Browse files
Show More

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

@@ -1,468 +1,470 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 # auto collapse if we have more than limit
201 # auto collapse if we have more than limit
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204
204
205 c.commit_statuses = ChangesetStatus.STATUSES
205 c.commit_statuses = ChangesetStatus.STATUSES
206 c.inline_comments = []
206 c.inline_comments = []
207 c.files = []
207 c.files = []
208
208
209 c.statuses = []
209 c.statuses = []
210 c.comments = []
210 c.comments = []
211 if len(c.commit_ranges) == 1:
211 if len(c.commit_ranges) == 1:
212 commit = c.commit_ranges[0]
212 commit = c.commit_ranges[0]
213 c.comments = ChangesetCommentsModel().get_comments(
213 c.comments = ChangesetCommentsModel().get_comments(
214 c.rhodecode_db_repo.repo_id,
214 c.rhodecode_db_repo.repo_id,
215 revision=commit.raw_id)
215 revision=commit.raw_id)
216 c.statuses.append(ChangesetStatusModel().get_status(
216 c.statuses.append(ChangesetStatusModel().get_status(
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 # comments from PR
218 # comments from PR
219 statuses = ChangesetStatusModel().get_statuses(
219 statuses = ChangesetStatusModel().get_statuses(
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 with_revisions=True)
221 with_revisions=True)
222 prs = set(st.pull_request for st in statuses
222 prs = set(st.pull_request for st in statuses
223 if st.pull_request is not None)
223 if st.pull_request is not None)
224 # from associated statuses, check the pull requests, and
224 # from associated statuses, check the pull requests, and
225 # show comments from them
225 # show comments from them
226 for pr in prs:
226 for pr in prs:
227 c.comments.extend(pr.comments)
227 c.comments.extend(pr.comments)
228
228
229 # Iterate over ranges (default commit view is always one commit)
229 # Iterate over ranges (default commit view is always one commit)
230 for commit in c.commit_ranges:
230 for commit in c.commit_ranges:
231 c.changes[commit.raw_id] = []
231 c.changes[commit.raw_id] = []
232
232
233 commit2 = commit
233 commit2 = commit
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235
235
236 _diff = c.rhodecode_repo.get_diff(
236 _diff = c.rhodecode_repo.get_diff(
237 commit1, commit2,
237 commit1, commit2,
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 diff_processor = diffs.DiffProcessor(
239 diff_processor = diffs.DiffProcessor(
240 _diff, format='newdiff', diff_limit=diff_limit,
240 _diff, format='newdiff', diff_limit=diff_limit,
241 file_limit=file_limit, show_full_diff=fulldiff)
241 file_limit=file_limit, show_full_diff=fulldiff)
242
242
243 commit_changes = OrderedDict()
243 commit_changes = OrderedDict()
244 if method == 'show':
244 if method == 'show':
245 _parsed = diff_processor.prepare()
245 _parsed = diff_processor.prepare()
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247
247
248 _parsed = diff_processor.prepare()
248 _parsed = diff_processor.prepare()
249
249
250 def _node_getter(commit):
250 def _node_getter(commit):
251 def get_node(fname):
251 def get_node(fname):
252 try:
252 try:
253 return commit.get_node(fname)
253 return commit.get_node(fname)
254 except NodeDoesNotExistError:
254 except NodeDoesNotExistError:
255 return None
255 return None
256 return get_node
256 return get_node
257
257
258 inline_comments = ChangesetCommentsModel().get_inline_comments(
258 inline_comments = ChangesetCommentsModel().get_inline_comments(
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
261 inline_comments)
261 inline_comments)
262
262
263 diffset = codeblocks.DiffSet(
263 diffset = codeblocks.DiffSet(
264 repo_name=c.repo_name,
264 repo_name=c.repo_name,
265 source_node_getter=_node_getter(commit1),
265 source_node_getter=_node_getter(commit1),
266 target_node_getter=_node_getter(commit2),
266 target_node_getter=_node_getter(commit2),
267 comments=inline_comments
267 comments=inline_comments
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 c.changes[commit.raw_id] = diffset
269 c.changes[commit.raw_id] = diffset
270 else:
270 else:
271 # downloads/raw we only need RAW diff nothing else
271 # downloads/raw we only need RAW diff nothing else
272 diff = diff_processor.as_raw()
272 diff = diff_processor.as_raw()
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274
274
275 # sort comments by how they were generated
275 # sort comments by how they were generated
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277
277
278
278
279 if len(c.commit_ranges) == 1:
279 if len(c.commit_ranges) == 1:
280 c.commit = c.commit_ranges[0]
280 c.commit = c.commit_ranges[0]
281 c.parent_tmpl = ''.join(
281 c.parent_tmpl = ''.join(
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 if method == 'download':
283 if method == 'download':
284 response.content_type = 'text/plain'
284 response.content_type = 'text/plain'
285 response.content_disposition = (
285 response.content_disposition = (
286 'attachment; filename=%s.diff' % commit_id_range[:12])
286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 return diff
287 return diff
288 elif method == 'patch':
288 elif method == 'patch':
289 response.content_type = 'text/plain'
289 response.content_type = 'text/plain'
290 c.diff = safe_unicode(diff)
290 c.diff = safe_unicode(diff)
291 return render('changeset/patch_changeset.mako')
291 return render('changeset/patch_changeset.mako')
292 elif method == 'raw':
292 elif method == 'raw':
293 response.content_type = 'text/plain'
293 response.content_type = 'text/plain'
294 return diff
294 return diff
295 elif method == 'show':
295 elif method == 'show':
296 if len(c.commit_ranges) == 1:
296 if len(c.commit_ranges) == 1:
297 return render('changeset/changeset.mako')
297 return render('changeset/changeset.mako')
298 else:
298 else:
299 c.ancestor = None
299 c.ancestor = None
300 c.target_repo = c.rhodecode_db_repo
300 c.target_repo = c.rhodecode_db_repo
301 return render('changeset/changeset_range.mako')
301 return render('changeset/changeset_range.mako')
302
302
303 @LoginRequired()
303 @LoginRequired()
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 'repository.admin')
305 'repository.admin')
306 def index(self, revision, method='show'):
306 def index(self, revision, method='show'):
307 return self._index(revision, method=method)
307 return self._index(revision, method=method)
308
308
309 @LoginRequired()
309 @LoginRequired()
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 'repository.admin')
311 'repository.admin')
312 def changeset_raw(self, revision):
312 def changeset_raw(self, revision):
313 return self._index(revision, method='raw')
313 return self._index(revision, method='raw')
314
314
315 @LoginRequired()
315 @LoginRequired()
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 'repository.admin')
317 'repository.admin')
318 def changeset_patch(self, revision):
318 def changeset_patch(self, revision):
319 return self._index(revision, method='patch')
319 return self._index(revision, method='patch')
320
320
321 @LoginRequired()
321 @LoginRequired()
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 'repository.admin')
323 'repository.admin')
324 def changeset_download(self, revision):
324 def changeset_download(self, revision):
325 return self._index(revision, method='download')
325 return self._index(revision, method='download')
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @NotAnonymous()
328 @NotAnonymous()
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 'repository.admin')
330 'repository.admin')
331 @auth.CSRFRequired()
331 @auth.CSRFRequired()
332 @jsonify
332 @jsonify
333 def comment(self, repo_name, revision):
333 def comment(self, repo_name, revision):
334 commit_id = revision
334 commit_id = revision
335 status = request.POST.get('changeset_status', None)
335 status = request.POST.get('changeset_status', None)
336 text = request.POST.get('text')
336 text = request.POST.get('text')
337 if status:
337 if status:
338 text = text or (_('Status change %(transition_icon)s %(status)s')
338 text = text or (_('Status change %(transition_icon)s %(status)s')
339 % {'transition_icon': '>',
339 % {'transition_icon': '>',
340 'status': ChangesetStatus.get_status_lbl(status)})
340 'status': ChangesetStatus.get_status_lbl(status)})
341
341
342 multi_commit_ids = filter(
342 multi_commit_ids = filter(
343 lambda s: s not in ['', None],
343 lambda s: s not in ['', None],
344 request.POST.get('commit_ids', '').split(','),)
344 request.POST.get('commit_ids', '').split(','),)
345
345
346 commit_ids = multi_commit_ids or [commit_id]
346 commit_ids = multi_commit_ids or [commit_id]
347 comment = None
347 comment = None
348 for current_id in filter(None, commit_ids):
348 for current_id in filter(None, commit_ids):
349 c.co = comment = ChangesetCommentsModel().create(
349 c.co = comment = ChangesetCommentsModel().create(
350 text=text,
350 text=text,
351 repo=c.rhodecode_db_repo.repo_id,
351 repo=c.rhodecode_db_repo.repo_id,
352 user=c.rhodecode_user.user_id,
352 user=c.rhodecode_user.user_id,
353 revision=current_id,
353 revision=current_id,
354 f_path=request.POST.get('f_path'),
354 f_path=request.POST.get('f_path'),
355 line_no=request.POST.get('line'),
355 line_no=request.POST.get('line'),
356 status_change=(ChangesetStatus.get_status_lbl(status)
356 status_change=(ChangesetStatus.get_status_lbl(status)
357 if status else None),
357 if status else None),
358 status_change_type=status
358 status_change_type=status
359 )
359 )
360 c.inline_comment = True if comment.line_no else False
361
360 # get status if set !
362 # get status if set !
361 if status:
363 if status:
362 # if latest status was from pull request and it's closed
364 # if latest status was from pull request and it's closed
363 # disallow changing status !
365 # disallow changing status !
364 # dont_allow_on_closed_pull_request = True !
366 # dont_allow_on_closed_pull_request = True !
365
367
366 try:
368 try:
367 ChangesetStatusModel().set_status(
369 ChangesetStatusModel().set_status(
368 c.rhodecode_db_repo.repo_id,
370 c.rhodecode_db_repo.repo_id,
369 status,
371 status,
370 c.rhodecode_user.user_id,
372 c.rhodecode_user.user_id,
371 comment,
373 comment,
372 revision=current_id,
374 revision=current_id,
373 dont_allow_on_closed_pull_request=True
375 dont_allow_on_closed_pull_request=True
374 )
376 )
375 except StatusChangeOnClosedPullRequestError:
377 except StatusChangeOnClosedPullRequestError:
376 msg = _('Changing the status of a commit associated with '
378 msg = _('Changing the status of a commit associated with '
377 'a closed pull request is not allowed')
379 'a closed pull request is not allowed')
378 log.exception(msg)
380 log.exception(msg)
379 h.flash(msg, category='warning')
381 h.flash(msg, category='warning')
380 return redirect(h.url(
382 return redirect(h.url(
381 'changeset_home', repo_name=repo_name,
383 'changeset_home', repo_name=repo_name,
382 revision=current_id))
384 revision=current_id))
383
385
384 # finalize, commit and redirect
386 # finalize, commit and redirect
385 Session().commit()
387 Session().commit()
386
388
387 data = {
389 data = {
388 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
390 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
389 }
391 }
390 if comment:
392 if comment:
391 data.update(comment.get_dict())
393 data.update(comment.get_dict())
392 data.update({'rendered_text':
394 data.update({'rendered_text':
393 render('changeset/changeset_comment_block.mako')})
395 render('changeset/changeset_comment_block.mako')})
394
396
395 return data
397 return data
396
398
397 @LoginRequired()
399 @LoginRequired()
398 @NotAnonymous()
400 @NotAnonymous()
399 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
400 'repository.admin')
402 'repository.admin')
401 @auth.CSRFRequired()
403 @auth.CSRFRequired()
402 def preview_comment(self):
404 def preview_comment(self):
403 # Technically a CSRF token is not needed as no state changes with this
405 # Technically a CSRF token is not needed as no state changes with this
404 # call. However, as this is a POST is better to have it, so automated
406 # call. However, as this is a POST is better to have it, so automated
405 # tools don't flag it as potential CSRF.
407 # tools don't flag it as potential CSRF.
406 # Post is required because the payload could be bigger than the maximum
408 # Post is required because the payload could be bigger than the maximum
407 # allowed by GET.
409 # allowed by GET.
408 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
409 raise HTTPBadRequest()
411 raise HTTPBadRequest()
410 text = request.POST.get('text')
412 text = request.POST.get('text')
411 renderer = request.POST.get('renderer') or 'rst'
413 renderer = request.POST.get('renderer') or 'rst'
412 if text:
414 if text:
413 return h.render(text, renderer=renderer, mentions=True)
415 return h.render(text, renderer=renderer, mentions=True)
414 return ''
416 return ''
415
417
416 @LoginRequired()
418 @LoginRequired()
417 @NotAnonymous()
419 @NotAnonymous()
418 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 'repository.admin')
421 'repository.admin')
420 @auth.CSRFRequired()
422 @auth.CSRFRequired()
421 @jsonify
423 @jsonify
422 def delete_comment(self, repo_name, comment_id):
424 def delete_comment(self, repo_name, comment_id):
423 comment = ChangesetComment.get(comment_id)
425 comment = ChangesetComment.get(comment_id)
424 owner = (comment.author.user_id == c.rhodecode_user.user_id)
426 owner = (comment.author.user_id == c.rhodecode_user.user_id)
425 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
427 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
426 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
428 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
427 ChangesetCommentsModel().delete(comment=comment)
429 ChangesetCommentsModel().delete(comment=comment)
428 Session().commit()
430 Session().commit()
429 return True
431 return True
430 else:
432 else:
431 raise HTTPForbidden()
433 raise HTTPForbidden()
432
434
433 @LoginRequired()
435 @LoginRequired()
434 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
436 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
435 'repository.admin')
437 'repository.admin')
436 @jsonify
438 @jsonify
437 def changeset_info(self, repo_name, revision):
439 def changeset_info(self, repo_name, revision):
438 if request.is_xhr:
440 if request.is_xhr:
439 try:
441 try:
440 return c.rhodecode_repo.get_commit(commit_id=revision)
442 return c.rhodecode_repo.get_commit(commit_id=revision)
441 except CommitDoesNotExistError as e:
443 except CommitDoesNotExistError as e:
442 return EmptyCommit(message=str(e))
444 return EmptyCommit(message=str(e))
443 else:
445 else:
444 raise HTTPBadRequest()
446 raise HTTPBadRequest()
445
447
446 @LoginRequired()
448 @LoginRequired()
447 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
448 'repository.admin')
450 'repository.admin')
449 @jsonify
451 @jsonify
450 def changeset_children(self, repo_name, revision):
452 def changeset_children(self, repo_name, revision):
451 if request.is_xhr:
453 if request.is_xhr:
452 commit = c.rhodecode_repo.get_commit(commit_id=revision)
454 commit = c.rhodecode_repo.get_commit(commit_id=revision)
453 result = {"results": commit.children}
455 result = {"results": commit.children}
454 return result
456 return result
455 else:
457 else:
456 raise HTTPBadRequest()
458 raise HTTPBadRequest()
457
459
458 @LoginRequired()
460 @LoginRequired()
459 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
460 'repository.admin')
462 'repository.admin')
461 @jsonify
463 @jsonify
462 def changeset_parents(self, repo_name, revision):
464 def changeset_parents(self, repo_name, revision):
463 if request.is_xhr:
465 if request.is_xhr:
464 commit = c.rhodecode_repo.get_commit(commit_id=revision)
466 commit = c.rhodecode_repo.get_commit(commit_id=revision)
465 result = {"results": commit.parents}
467 result = {"results": commit.parents}
466 return result
468 return result
467 else:
469 else:
468 raise HTTPBadRequest()
470 raise HTTPBadRequest()
@@ -1,1020 +1,1024 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import ChangesetCommentsModel
58 from rhodecode.model.comment import ChangesetCommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel
63 from rhodecode.model.pull_request import PullRequestModel
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = ChangesetCommentsModel().get_all_comments(
261 comments = ChangesetCommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After succesfull merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 if self._meets_merge_pre_conditions(pull_request, user):
608 log.debug("Pre-conditions checked, trying to merge.")
608 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
609 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
611 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
612 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
613 self._merge_pull_request(pull_request, user, extras)
614
614
615 return redirect(url(
615 return redirect(url(
616 'pullrequest_show',
616 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
617 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
618 pull_request_id=pull_request.pull_request_id))
619
619
620 def _meets_merge_pre_conditions(self, pull_request, user):
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
622 raise HTTPForbidden()
623
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
627 h.flash(msg, category='error')
628 return False
628 return False
629
629
630 if (pull_request.calculated_review_status()
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
634 h.flash(msg, category='error')
635 return False
635 return False
636 return True
636 return True
637
637
638 def _merge_pull_request(self, pull_request, user, extras):
638 def _merge_pull_request(self, pull_request, user, extras):
639 merge_resp = PullRequestModel().merge(
639 merge_resp = PullRequestModel().merge(
640 pull_request, user, extras=extras)
640 pull_request, user, extras=extras)
641
641
642 if merge_resp.executed:
642 if merge_resp.executed:
643 log.debug("The merge was successful, closing the pull request.")
643 log.debug("The merge was successful, closing the pull request.")
644 PullRequestModel().close_pull_request(
644 PullRequestModel().close_pull_request(
645 pull_request.pull_request_id, user)
645 pull_request.pull_request_id, user)
646 Session().commit()
646 Session().commit()
647 msg = _('Pull request was successfully merged and closed.')
647 msg = _('Pull request was successfully merged and closed.')
648 h.flash(msg, category='success')
648 h.flash(msg, category='success')
649 else:
649 else:
650 log.debug(
650 log.debug(
651 "The merge was not successful. Merge response: %s",
651 "The merge was not successful. Merge response: %s",
652 merge_resp)
652 merge_resp)
653 msg = PullRequestModel().merge_status_message(
653 msg = PullRequestModel().merge_status_message(
654 merge_resp.failure_reason)
654 merge_resp.failure_reason)
655 h.flash(msg, category='error')
655 h.flash(msg, category='error')
656
656
657 def _update_reviewers(self, pull_request_id, review_members):
657 def _update_reviewers(self, pull_request_id, review_members):
658 reviewers = [
658 reviewers = [
659 (int(r['user_id']), r['reasons']) for r in review_members]
659 (int(r['user_id']), r['reasons']) for r in review_members]
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 Session().commit()
661 Session().commit()
662
662
663 def _reject_close(self, pull_request):
663 def _reject_close(self, pull_request):
664 if pull_request.is_closed():
664 if pull_request.is_closed():
665 raise HTTPForbidden()
665 raise HTTPForbidden()
666
666
667 PullRequestModel().close_pull_request_with_comment(
667 PullRequestModel().close_pull_request_with_comment(
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 Session().commit()
669 Session().commit()
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 'repository.admin')
674 'repository.admin')
675 @auth.CSRFRequired()
675 @auth.CSRFRequired()
676 @jsonify
676 @jsonify
677 def delete(self, repo_name, pull_request_id):
677 def delete(self, repo_name, pull_request_id):
678 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
680 # only owner can delete it !
680 # only owner can delete it !
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 PullRequestModel().delete(pull_request)
682 PullRequestModel().delete(pull_request)
683 Session().commit()
683 Session().commit()
684 h.flash(_('Successfully deleted pull request'),
684 h.flash(_('Successfully deleted pull request'),
685 category='success')
685 category='success')
686 return redirect(url('my_account_pullrequests'))
686 return redirect(url('my_account_pullrequests'))
687 raise HTTPForbidden()
687 raise HTTPForbidden()
688
688
689 def _get_pr_version(self, pull_request_id, version=None):
689 def _get_pr_version(self, pull_request_id, version=None):
690 pull_request_id = safe_int(pull_request_id)
690 pull_request_id = safe_int(pull_request_id)
691 at_version = None
691 at_version = None
692
692
693 if version and version == 'latest':
693 if version and version == 'latest':
694 pull_request_ver = PullRequest.get(pull_request_id)
694 pull_request_ver = PullRequest.get(pull_request_id)
695 pull_request_obj = pull_request_ver
695 pull_request_obj = pull_request_ver
696 _org_pull_request_obj = pull_request_obj
696 _org_pull_request_obj = pull_request_obj
697 at_version = 'latest'
697 at_version = 'latest'
698 elif version:
698 elif version:
699 pull_request_ver = PullRequestVersion.get_or_404(version)
699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 pull_request_obj = pull_request_ver
700 pull_request_obj = pull_request_ver
701 _org_pull_request_obj = pull_request_ver.pull_request
701 _org_pull_request_obj = pull_request_ver.pull_request
702 at_version = pull_request_ver.pull_request_version_id
702 at_version = pull_request_ver.pull_request_version_id
703 else:
703 else:
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705
705
706 pull_request_display_obj = PullRequest.get_pr_display_object(
706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 pull_request_obj, _org_pull_request_obj)
707 pull_request_obj, _org_pull_request_obj)
708 return _org_pull_request_obj, pull_request_obj, \
708 return _org_pull_request_obj, pull_request_obj, \
709 pull_request_display_obj, at_version
709 pull_request_display_obj, at_version
710
710
711 def _get_pr_version_changes(self, version, pull_request_latest):
711 def _get_pr_version_changes(self, version, pull_request_latest):
712 """
712 """
713 Generate changes commits, and diff data based on the current pr version
713 Generate changes commits, and diff data based on the current pr version
714 """
714 """
715
715
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717
717
718 # fake the version to add the "initial" state object
718 # fake the version to add the "initial" state object
719 pull_request_initial = PullRequest.get_pr_display_object(
719 pull_request_initial = PullRequest.get_pr_display_object(
720 pull_request_latest, pull_request_latest,
720 pull_request_latest, pull_request_latest,
721 internal_methods=['get_commit', 'versions'])
721 internal_methods=['get_commit', 'versions'])
722 pull_request_initial.revisions = []
722 pull_request_initial.revisions = []
723 pull_request_initial.source_repo.get_commit = types.MethodType(
723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727
727
728 _changes_versions = [pull_request_latest] + \
728 _changes_versions = [pull_request_latest] + \
729 list(reversed(c.versions)) + \
729 list(reversed(c.versions)) + \
730 [pull_request_initial]
730 [pull_request_initial]
731
731
732 if version == 'latest':
732 if version == 'latest':
733 index = 0
733 index = 0
734 else:
734 else:
735 for pos, prver in enumerate(_changes_versions):
735 for pos, prver in enumerate(_changes_versions):
736 ver = getattr(prver, 'pull_request_version_id', -1)
736 ver = getattr(prver, 'pull_request_version_id', -1)
737 if ver == safe_int(version):
737 if ver == safe_int(version):
738 index = pos
738 index = pos
739 break
739 break
740 else:
740 else:
741 index = 0
741 index = 0
742
742
743 cur_obj = _changes_versions[index]
743 cur_obj = _changes_versions[index]
744 prev_obj = _changes_versions[index + 1]
744 prev_obj = _changes_versions[index + 1]
745
745
746 old_commit_ids = set(prev_obj.revisions)
746 old_commit_ids = set(prev_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
748
748
749 changes = PullRequestModel()._calculate_commit_id_changes(
749 changes = PullRequestModel()._calculate_commit_id_changes(
750 old_commit_ids, new_commit_ids)
750 old_commit_ids, new_commit_ids)
751
751
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 cur_obj, prev_obj)
753 cur_obj, prev_obj)
754 file_changes = PullRequestModel()._calculate_file_changes(
754 file_changes = PullRequestModel()._calculate_file_changes(
755 old_diff_data, new_diff_data)
755 old_diff_data, new_diff_data)
756 return changes, file_changes
756 return changes, file_changes
757
757
758 @LoginRequired()
758 @LoginRequired()
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 'repository.admin')
760 'repository.admin')
761 def show(self, repo_name, pull_request_id):
761 def show(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
762 pull_request_id = safe_int(pull_request_id)
763 version = request.GET.get('version')
763 version = request.GET.get('version')
764
764
765 (pull_request_latest,
765 (pull_request_latest,
766 pull_request_at_ver,
766 pull_request_at_ver,
767 pull_request_display_obj,
767 pull_request_display_obj,
768 at_version) = self._get_pr_version(pull_request_id, version=version)
768 at_version) = self._get_pr_version(pull_request_id, version=version)
769
769
770 c.template_context['pull_request_data']['pull_request_id'] = \
770 c.template_context['pull_request_data']['pull_request_id'] = \
771 pull_request_id
771 pull_request_id
772
772
773 # pull_requests repo_name we opened it against
773 # pull_requests repo_name we opened it against
774 # ie. target_repo must match
774 # ie. target_repo must match
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 raise HTTPNotFound
776 raise HTTPNotFound
777
777
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 pull_request_at_ver)
779 pull_request_at_ver)
780
780
781 pr_closed = pull_request_latest.is_closed()
781 pr_closed = pull_request_latest.is_closed()
782 if at_version and not at_version == 'latest':
782 if at_version and not at_version == 'latest':
783 c.allowed_to_change_status = False
783 c.allowed_to_change_status = False
784 c.allowed_to_update = False
784 c.allowed_to_update = False
785 c.allowed_to_merge = False
785 c.allowed_to_merge = False
786 c.allowed_to_delete = False
786 c.allowed_to_delete = False
787 c.allowed_to_comment = False
787 c.allowed_to_comment = False
788 else:
788 else:
789 c.allowed_to_change_status = PullRequestModel(). \
789 c.allowed_to_change_status = PullRequestModel(). \
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 c.allowed_to_update = PullRequestModel().check_user_update(
791 c.allowed_to_update = PullRequestModel().check_user_update(
792 pull_request_latest, c.rhodecode_user) and not pr_closed
792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 pull_request_latest, c.rhodecode_user) and not pr_closed
794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 pull_request_latest, c.rhodecode_user) and not pr_closed
796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 c.allowed_to_comment = not pr_closed
797 c.allowed_to_comment = not pr_closed
798
798
799 cc_model = ChangesetCommentsModel()
799 cc_model = ChangesetCommentsModel()
800
800
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
804 pull_request_at_ver)
805 c.approval_msg = None
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
808 c.pr_merge_status = False
809
809
810 # inline comments
810 # inline comments
811 inline_comments = cc_model.get_inline_comments(
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
815 inline_comments, version=at_version, include_aggregates=True)
816
816
817 c.versions = pull_request_display_obj.versions()
817 c.at_version_num = at_version if at_version and at_version != 'latest' else None
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 c.at_version_num, c.versions)
821
818 is_outdated = lambda co: \
822 is_outdated = lambda co: \
819 not c.at_version_num \
823 not c.at_version_num \
820 or co.pull_request_version_id <= c.at_version_num
824 or co.pull_request_version_id <= c.at_version_num
821
825
822 # inline_comments_until_version
826 # inline_comments_until_version
823 if c.at_version_num:
827 if c.at_version_num:
824 # if we use version, then do not show later comments
828 # if we use version, then do not show later comments
825 # than current version
829 # than current version
826 paths = collections.defaultdict(lambda: collections.defaultdict(list))
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
827 for fname, per_line_comments in inline_comments.iteritems():
831 for fname, per_line_comments in inline_comments.iteritems():
828 for lno, comments in per_line_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
829 for co in comments:
833 for co in comments:
830 if co.pull_request_version_id and is_outdated(co):
834 if co.pull_request_version_id and is_outdated(co):
831 paths[co.f_path][co.line_no].append(co)
835 paths[co.f_path][co.line_no].append(co)
832 inline_comments = paths
836 inline_comments = paths
833
837
834 # outdated comments
838 # outdated comments
835 c.outdated_cnt = 0
839 c.outdated_cnt = 0
836 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
840 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
837 outdated_comments = cc_model.get_outdated_comments(
841 outdated_comments = cc_model.get_outdated_comments(
838 c.rhodecode_db_repo.repo_id,
842 c.rhodecode_db_repo.repo_id,
839 pull_request=pull_request_at_ver)
843 pull_request=pull_request_at_ver)
840
844
841 # Count outdated comments and check for deleted files
845 # Count outdated comments and check for deleted files
842 is_outdated = lambda co: \
846 is_outdated = lambda co: \
843 not c.at_version_num \
847 not c.at_version_num \
844 or co.pull_request_version_id < c.at_version_num
848 or co.pull_request_version_id < c.at_version_num
845 for file_name, lines in outdated_comments.iteritems():
849 for file_name, lines in outdated_comments.iteritems():
846 for comments in lines.values():
850 for comments in lines.values():
847 comments = [comm for comm in comments if is_outdated(comm)]
851 comments = [comm for comm in comments if is_outdated(comm)]
848 c.outdated_cnt += len(comments)
852 c.outdated_cnt += len(comments)
849
853
850 # load compare data into template context
854 # load compare data into template context
851 self._load_compare_data(pull_request_at_ver, inline_comments)
855 self._load_compare_data(pull_request_at_ver, inline_comments)
852
856
853 # this is a hack to properly display links, when creating PR, the
857 # this is a hack to properly display links, when creating PR, the
854 # compare view and others uses different notation, and
858 # compare view and others uses different notation, and
855 # compare_commits.mako renders links based on the target_repo.
859 # compare_commits.mako renders links based on the target_repo.
856 # We need to swap that here to generate it properly on the html side
860 # We need to swap that here to generate it properly on the html side
857 c.target_repo = c.source_repo
861 c.target_repo = c.source_repo
858
862
859 # general comments
863 # general comments
860 c.comments = cc_model.get_comments(
864 c.comments = cc_model.get_comments(
861 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
862
866
863 if c.allowed_to_update:
867 if c.allowed_to_update:
864 force_close = ('forced_closed', _('Close Pull Request'))
868 force_close = ('forced_closed', _('Close Pull Request'))
865 statuses = ChangesetStatus.STATUSES + [force_close]
869 statuses = ChangesetStatus.STATUSES + [force_close]
866 else:
870 else:
867 statuses = ChangesetStatus.STATUSES
871 statuses = ChangesetStatus.STATUSES
868 c.commit_statuses = statuses
872 c.commit_statuses = statuses
869
873
870 c.ancestor = None # TODO: add ancestor here
874 c.ancestor = None # TODO: add ancestor here
871 c.pull_request = pull_request_display_obj
875 c.pull_request = pull_request_display_obj
872 c.pull_request_latest = pull_request_latest
876 c.pull_request_latest = pull_request_latest
873 c.at_version = at_version
877 c.at_version = at_version
874
878
875 c.versions = pull_request_display_obj.versions()
876 c.changes = None
879 c.changes = None
877 c.file_changes = None
880 c.file_changes = None
878
881
879 c.show_version_changes = 1 # control flag, not used yet
882 c.show_version_changes = 1 # control flag, not used yet
880
883
881 if at_version and c.show_version_changes:
884 if at_version and c.show_version_changes:
882 c.changes, c.file_changes = self._get_pr_version_changes(
885 c.changes, c.file_changes = self._get_pr_version_changes(
883 version, pull_request_latest)
886 version, pull_request_latest)
884
887
885 return render('/pullrequests/pullrequest_show.mako')
888 return render('/pullrequests/pullrequest_show.mako')
886
889
887 @LoginRequired()
890 @LoginRequired()
888 @NotAnonymous()
891 @NotAnonymous()
889 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
890 'repository.admin')
893 'repository.admin')
891 @auth.CSRFRequired()
894 @auth.CSRFRequired()
892 @jsonify
895 @jsonify
893 def comment(self, repo_name, pull_request_id):
896 def comment(self, repo_name, pull_request_id):
894 pull_request_id = safe_int(pull_request_id)
897 pull_request_id = safe_int(pull_request_id)
895 pull_request = PullRequest.get_or_404(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
896 if pull_request.is_closed():
899 if pull_request.is_closed():
897 raise HTTPForbidden()
900 raise HTTPForbidden()
898
901
899 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
900 # as a changeset status, still we want to send it in one value.
903 # as a changeset status, still we want to send it in one value.
901 status = request.POST.get('changeset_status', None)
904 status = request.POST.get('changeset_status', None)
902 text = request.POST.get('text')
905 text = request.POST.get('text')
903 if status and '_closed' in status:
906 if status and '_closed' in status:
904 close_pr = True
907 close_pr = True
905 status = status.replace('_closed', '')
908 status = status.replace('_closed', '')
906 else:
909 else:
907 close_pr = False
910 close_pr = False
908
911
909 forced = (status == 'forced')
912 forced = (status == 'forced')
910 if forced:
913 if forced:
911 status = 'rejected'
914 status = 'rejected'
912
915
913 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 allowed_to_change_status = PullRequestModel().check_user_change_status(
914 pull_request, c.rhodecode_user)
917 pull_request, c.rhodecode_user)
915
918
916 if status and allowed_to_change_status:
919 if status and allowed_to_change_status:
917 message = (_('Status change %(transition_icon)s %(status)s')
920 message = (_('Status change %(transition_icon)s %(status)s')
918 % {'transition_icon': '>',
921 % {'transition_icon': '>',
919 'status': ChangesetStatus.get_status_lbl(status)})
922 'status': ChangesetStatus.get_status_lbl(status)})
920 if close_pr:
923 if close_pr:
921 message = _('Closing with') + ' ' + message
924 message = _('Closing with') + ' ' + message
922 text = text or message
925 text = text or message
923 comm = ChangesetCommentsModel().create(
926 comm = ChangesetCommentsModel().create(
924 text=text,
927 text=text,
925 repo=c.rhodecode_db_repo.repo_id,
928 repo=c.rhodecode_db_repo.repo_id,
926 user=c.rhodecode_user.user_id,
929 user=c.rhodecode_user.user_id,
927 pull_request=pull_request_id,
930 pull_request=pull_request_id,
928 f_path=request.POST.get('f_path'),
931 f_path=request.POST.get('f_path'),
929 line_no=request.POST.get('line'),
932 line_no=request.POST.get('line'),
930 status_change=(ChangesetStatus.get_status_lbl(status)
933 status_change=(ChangesetStatus.get_status_lbl(status)
931 if status and allowed_to_change_status else None),
934 if status and allowed_to_change_status else None),
932 status_change_type=(status
935 status_change_type=(status
933 if status and allowed_to_change_status else None),
936 if status and allowed_to_change_status else None),
934 closing_pr=close_pr
937 closing_pr=close_pr
935 )
938 )
936
939
937 if allowed_to_change_status:
940 if allowed_to_change_status:
938 old_calculated_status = pull_request.calculated_review_status()
941 old_calculated_status = pull_request.calculated_review_status()
939 # get status if set !
942 # get status if set !
940 if status:
943 if status:
941 ChangesetStatusModel().set_status(
944 ChangesetStatusModel().set_status(
942 c.rhodecode_db_repo.repo_id,
945 c.rhodecode_db_repo.repo_id,
943 status,
946 status,
944 c.rhodecode_user.user_id,
947 c.rhodecode_user.user_id,
945 comm,
948 comm,
946 pull_request=pull_request_id
949 pull_request=pull_request_id
947 )
950 )
948
951
949 Session().flush()
952 Session().flush()
950 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
953 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
951 # we now calculate the status of pull request, and based on that
954 # we now calculate the status of pull request, and based on that
952 # calculation we set the commits status
955 # calculation we set the commits status
953 calculated_status = pull_request.calculated_review_status()
956 calculated_status = pull_request.calculated_review_status()
954 if old_calculated_status != calculated_status:
957 if old_calculated_status != calculated_status:
955 PullRequestModel()._trigger_pull_request_hook(
958 PullRequestModel()._trigger_pull_request_hook(
956 pull_request, c.rhodecode_user, 'review_status_change')
959 pull_request, c.rhodecode_user, 'review_status_change')
957
960
958 calculated_status_lbl = ChangesetStatus.get_status_lbl(
961 calculated_status_lbl = ChangesetStatus.get_status_lbl(
959 calculated_status)
962 calculated_status)
960
963
961 if close_pr:
964 if close_pr:
962 status_completed = (
965 status_completed = (
963 calculated_status in [ChangesetStatus.STATUS_APPROVED,
966 calculated_status in [ChangesetStatus.STATUS_APPROVED,
964 ChangesetStatus.STATUS_REJECTED])
967 ChangesetStatus.STATUS_REJECTED])
965 if forced or status_completed:
968 if forced or status_completed:
966 PullRequestModel().close_pull_request(
969 PullRequestModel().close_pull_request(
967 pull_request_id, c.rhodecode_user)
970 pull_request_id, c.rhodecode_user)
968 else:
971 else:
969 h.flash(_('Closing pull request on other statuses than '
972 h.flash(_('Closing pull request on other statuses than '
970 'rejected or approved is forbidden. '
973 'rejected or approved is forbidden. '
971 'Calculated status from all reviewers '
974 'Calculated status from all reviewers '
972 'is currently: %s') % calculated_status_lbl,
975 'is currently: %s') % calculated_status_lbl,
973 category='warning')
976 category='warning')
974
977
975 Session().commit()
978 Session().commit()
976
979
977 if not request.is_xhr:
980 if not request.is_xhr:
978 return redirect(h.url('pullrequest_show', repo_name=repo_name,
981 return redirect(h.url('pullrequest_show', repo_name=repo_name,
979 pull_request_id=pull_request_id))
982 pull_request_id=pull_request_id))
980
983
981 data = {
984 data = {
982 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
985 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
983 }
986 }
984 if comm:
987 if comm:
985 c.co = comm
988 c.co = comm
989 c.inline_comment = True if comm.line_no else False
986 data.update(comm.get_dict())
990 data.update(comm.get_dict())
987 data.update({'rendered_text':
991 data.update({'rendered_text':
988 render('changeset/changeset_comment_block.mako')})
992 render('changeset/changeset_comment_block.mako')})
989
993
990 return data
994 return data
991
995
992 @LoginRequired()
996 @LoginRequired()
993 @NotAnonymous()
997 @NotAnonymous()
994 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
998 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
995 'repository.admin')
999 'repository.admin')
996 @auth.CSRFRequired()
1000 @auth.CSRFRequired()
997 @jsonify
1001 @jsonify
998 def delete_comment(self, repo_name, comment_id):
1002 def delete_comment(self, repo_name, comment_id):
999 return self._delete_comment(comment_id)
1003 return self._delete_comment(comment_id)
1000
1004
1001 def _delete_comment(self, comment_id):
1005 def _delete_comment(self, comment_id):
1002 comment_id = safe_int(comment_id)
1006 comment_id = safe_int(comment_id)
1003 co = ChangesetComment.get_or_404(comment_id)
1007 co = ChangesetComment.get_or_404(comment_id)
1004 if co.pull_request.is_closed():
1008 if co.pull_request.is_closed():
1005 # don't allow deleting comments on closed pull request
1009 # don't allow deleting comments on closed pull request
1006 raise HTTPForbidden()
1010 raise HTTPForbidden()
1007
1011
1008 is_owner = co.author.user_id == c.rhodecode_user.user_id
1012 is_owner = co.author.user_id == c.rhodecode_user.user_id
1009 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1013 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1010 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1014 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1011 old_calculated_status = co.pull_request.calculated_review_status()
1015 old_calculated_status = co.pull_request.calculated_review_status()
1012 ChangesetCommentsModel().delete(comment=co)
1016 ChangesetCommentsModel().delete(comment=co)
1013 Session().commit()
1017 Session().commit()
1014 calculated_status = co.pull_request.calculated_review_status()
1018 calculated_status = co.pull_request.calculated_review_status()
1015 if old_calculated_status != calculated_status:
1019 if old_calculated_status != calculated_status:
1016 PullRequestModel()._trigger_pull_request_hook(
1020 PullRequestModel()._trigger_pull_request_hook(
1017 co.pull_request, c.rhodecode_user, 'review_status_change')
1021 co.pull_request, c.rhodecode_user, 'review_status_change')
1018 return True
1022 return True
1019 else:
1023 else:
1020 raise HTTPForbidden()
1024 raise HTTPForbidden()
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,407 +1,432 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 .comments {
7 .comments {
8 width: 100%;
8 width: 100%;
9 }
9 }
10
10
11 tr.inline-comments div {
11 tr.inline-comments div {
12 max-width: 100%;
12 max-width: 100%;
13
13
14 p {
14 p {
15 white-space: normal;
15 white-space: normal;
16 }
16 }
17
17
18 code, pre, .code, dd {
18 code, pre, .code, dd {
19 overflow-x: auto;
19 overflow-x: auto;
20 width: 1062px;
20 width: 1062px;
21 }
21 }
22
22
23 dd {
23 dd {
24 width: auto;
24 width: auto;
25 }
25 }
26 }
26 }
27
27
28 #injected_page_comments {
28 #injected_page_comments {
29 .comment-previous-link,
29 .comment-previous-link,
30 .comment-next-link,
30 .comment-next-link,
31 .comment-links-divider {
31 .comment-links-divider {
32 display: none;
32 display: none;
33 }
33 }
34 }
34 }
35
35
36 .add-comment {
36 .add-comment {
37 margin-bottom: 10px;
37 margin-bottom: 10px;
38 }
38 }
39 .hide-comment-button .add-comment {
39 .hide-comment-button .add-comment {
40 display: none;
40 display: none;
41 }
41 }
42
42
43 .comment-bubble {
43 .comment-bubble {
44 color: @grey4;
44 color: @grey4;
45 margin-top: 4px;
45 margin-top: 4px;
46 margin-right: 30px;
46 margin-right: 30px;
47 visibility: hidden;
47 visibility: hidden;
48 }
48 }
49
49
50 .comment {
50 .comment {
51
52 &.comment-general {
53 border: 1px solid @grey5;
54 padding: 5px 5px 5px 5px;
55 }
56
51 margin: @padding 0;
57 margin: @padding 0;
52 padding: 4px 0 0 0;
58 padding: 4px 0 0 0;
53 line-height: 1em;
59 line-height: 1em;
54
60
55 .rc-user {
61 .rc-user {
56 min-width: 0;
62 min-width: 0;
57 margin: -2px .5em 0 0;
63 margin: -2px .5em 0 0;
58 }
64 }
59
65
60 .meta {
66 .meta {
61 position: relative;
67 position: relative;
62 width: 100%;
68 width: 100%;
63 margin: 0 0 .5em 0;
69 margin: 0 0 .5em 0;
70 border-bottom: 1px solid @grey5;
71 padding: 8px 0px;
64
72
65 &:hover .permalink {
73 &:hover .permalink {
66 visibility: visible;
74 visibility: visible;
67 color: @rcblue;
75 color: @rcblue;
68 }
76 }
69 }
77 }
70
78
71 .author,
79 .author,
72 .date {
80 .date {
73 display: inline;
81 display: inline;
74 margin: 0 .5 0 0;
75 padding: 0 .5 0 0;
76
82
77 &:after {
83 &:after {
78 content: ' | ';
84 content: ' | ';
79 color: @grey5;
85 color: @grey5;
80 }
86 }
81 }
87 }
82
88
89 .author-general img {
90 top: -3px;
91 }
92 .author-inline img {
93 top: -3px;
94 }
95
83 .status-change,
96 .status-change,
84 .permalink,
97 .permalink,
85 .changeset-status-lbl {
98 .changeset-status-lbl {
86 display: inline;
99 display: inline;
87 }
100 }
88
101
89 .permalink {
102 .permalink {
90 visibility: hidden;
103 visibility: hidden;
91 }
104 }
92
105
93 .comment-links-divider {
106 .comment-links-divider {
94 display: inline;
107 display: inline;
95 }
108 }
96
109
97 .comment-links-block {
110 .comment-links-block {
98 float:right;
111 float:right;
99 text-align: right;
112 text-align: right;
100 min-width: 85px;
113 min-width: 85px;
101
114
102 [class^="icon-"]:before,
115 [class^="icon-"]:before,
103 [class*=" icon-"]:before {
116 [class*=" icon-"]:before {
104 margin-left: 0;
117 margin-left: 0;
105 margin-right: 0;
118 margin-right: 0;
106 }
119 }
107 }
120 }
108
121
109 .comment-previous-link {
122 .comment-previous-link {
110 display: inline-block;
123 display: inline-block;
111
124
112 .arrow_comment_link{
125 .arrow_comment_link{
113 cursor: pointer;
126 cursor: pointer;
114 i {
127 i {
115 font-size:10px;
128 font-size:10px;
116 }
129 }
117 }
130 }
118 .arrow_comment_link.disabled {
131 .arrow_comment_link.disabled {
119 cursor: default;
132 cursor: default;
120 color: @grey5;
133 color: @grey5;
121 }
134 }
122 }
135 }
123
136
124 .comment-next-link {
137 .comment-next-link {
125 display: inline-block;
138 display: inline-block;
126
139
127 .arrow_comment_link{
140 .arrow_comment_link{
128 cursor: pointer;
141 cursor: pointer;
129 i {
142 i {
130 font-size:10px;
143 font-size:10px;
131 }
144 }
132 }
145 }
133 .arrow_comment_link.disabled {
146 .arrow_comment_link.disabled {
134 cursor: default;
147 cursor: default;
135 color: @grey5;
148 color: @grey5;
136 }
149 }
137 }
150 }
138
151
139 .flag_status {
152 .flag_status {
140 display: inline-block;
153 display: inline-block;
141 margin: -2px .5em 0 .25em
154 margin: -2px .5em 0 .25em
142 }
155 }
143
156
144 .delete-comment {
157 .delete-comment {
145 display: inline-block;
158 display: inline-block;
146 color: @rcblue;
159 color: @rcblue;
147
160
148 &:hover {
161 &:hover {
149 cursor: pointer;
162 cursor: pointer;
150 }
163 }
151 }
164 }
152
165
153
166
154 .text {
167 .text {
155 clear: both;
168 clear: both;
156 border: @border-thickness solid @grey5;
157 .border-radius(@border-radius);
169 .border-radius(@border-radius);
158 .box-sizing(border-box);
170 .box-sizing(border-box);
159
171
160 .markdown-block p,
172 .markdown-block p,
161 .rst-block p {
173 .rst-block p {
162 margin: .5em 0 !important;
174 margin: .5em 0 !important;
163 // TODO: lisa: This is needed because of other rst !important rules :[
175 // TODO: lisa: This is needed because of other rst !important rules :[
164 }
176 }
165 }
177 }
178
179 .pr-version {
180 float: left;
181 margin: 0px 4px;
182 }
183 .pr-version-inline {
184 float: left;
185 margin: 1px 4px;
186 }
187 .pr-version-num {
188 font-size: 10px;
189 }
190
166 }
191 }
167
192
168 .show-outdated-comments {
193 .show-outdated-comments {
169 display: inline;
194 display: inline;
170 color: @rcblue;
195 color: @rcblue;
171 }
196 }
172
197
173 // Comment Form
198 // Comment Form
174 div.comment-form {
199 div.comment-form {
175 margin-top: 20px;
200 margin-top: 20px;
176 }
201 }
177
202
178 .comment-form strong {
203 .comment-form strong {
179 display: block;
204 display: block;
180 margin-bottom: 15px;
205 margin-bottom: 15px;
181 }
206 }
182
207
183 .comment-form textarea {
208 .comment-form textarea {
184 width: 100%;
209 width: 100%;
185 height: 100px;
210 height: 100px;
186 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
211 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
187 }
212 }
188
213
189 form.comment-form {
214 form.comment-form {
190 margin-top: 10px;
215 margin-top: 10px;
191 margin-left: 10px;
216 margin-left: 10px;
192 }
217 }
193
218
194 .comment-inline-form .comment-block-ta,
219 .comment-inline-form .comment-block-ta,
195 .comment-form .comment-block-ta,
220 .comment-form .comment-block-ta,
196 .comment-form .preview-box {
221 .comment-form .preview-box {
197 .border-radius(@border-radius);
222 .border-radius(@border-radius);
198 .box-sizing(border-box);
223 .box-sizing(border-box);
199 background-color: white;
224 background-color: white;
200 }
225 }
201
226
202 .comment-form-submit {
227 .comment-form-submit {
203 margin-top: 5px;
228 margin-top: 5px;
204 margin-left: 525px;
229 margin-left: 525px;
205 }
230 }
206
231
207 .file-comments {
232 .file-comments {
208 display: none;
233 display: none;
209 }
234 }
210
235
211 .comment-form .preview-box.unloaded,
236 .comment-form .preview-box.unloaded,
212 .comment-inline-form .preview-box.unloaded {
237 .comment-inline-form .preview-box.unloaded {
213 height: 50px;
238 height: 50px;
214 text-align: center;
239 text-align: center;
215 padding: 20px;
240 padding: 20px;
216 background-color: white;
241 background-color: white;
217 }
242 }
218
243
219 .comment-footer {
244 .comment-footer {
220 position: relative;
245 position: relative;
221 width: 100%;
246 width: 100%;
222 min-height: 42px;
247 min-height: 42px;
223
248
224 .status_box,
249 .status_box,
225 .cancel-button {
250 .cancel-button {
226 float: left;
251 float: left;
227 display: inline-block;
252 display: inline-block;
228 }
253 }
229
254
230 .action-buttons {
255 .action-buttons {
231 float: right;
256 float: right;
232 display: inline-block;
257 display: inline-block;
233 }
258 }
234 }
259 }
235
260
236 .comment-form {
261 .comment-form {
237
262
238 .comment {
263 .comment {
239 margin-left: 10px;
264 margin-left: 10px;
240 }
265 }
241
266
242 .comment-help {
267 .comment-help {
243 color: @grey4;
268 color: @grey4;
244 padding: 5px 0 5px 0;
269 padding: 5px 0 5px 0;
245 }
270 }
246
271
247 .comment-title {
272 .comment-title {
248 padding: 5px 0 5px 0;
273 padding: 5px 0 5px 0;
249 }
274 }
250
275
251 .comment-button {
276 .comment-button {
252 display: inline-block;
277 display: inline-block;
253 }
278 }
254
279
255 .comment-button .comment-button-input {
280 .comment-button .comment-button-input {
256 margin-right: 0;
281 margin-right: 0;
257 }
282 }
258
283
259 .comment-footer {
284 .comment-footer {
260 margin-bottom: 110px;
285 margin-bottom: 110px;
261 margin-top: 10px;
286 margin-top: 10px;
262 }
287 }
263 }
288 }
264
289
265
290
266 .comment-form-login {
291 .comment-form-login {
267 .comment-help {
292 .comment-help {
268 padding: 0.9em; //same as the button
293 padding: 0.9em; //same as the button
269 }
294 }
270
295
271 div.clearfix {
296 div.clearfix {
272 clear: both;
297 clear: both;
273 width: 100%;
298 width: 100%;
274 display: block;
299 display: block;
275 }
300 }
276 }
301 }
277
302
278 .preview-box {
303 .preview-box {
279 min-height: 105px;
304 min-height: 105px;
280 margin-bottom: 15px;
305 margin-bottom: 15px;
281 background-color: white;
306 background-color: white;
282 .border-radius(@border-radius);
307 .border-radius(@border-radius);
283 .box-sizing(border-box);
308 .box-sizing(border-box);
284 }
309 }
285
310
286 .add-another-button {
311 .add-another-button {
287 margin-left: 10px;
312 margin-left: 10px;
288 margin-top: 10px;
313 margin-top: 10px;
289 margin-bottom: 10px;
314 margin-bottom: 10px;
290 }
315 }
291
316
292 .comment .buttons {
317 .comment .buttons {
293 float: right;
318 float: right;
294 margin: -1px 0px 0px 0px;
319 margin: -1px 0px 0px 0px;
295 }
320 }
296
321
297 // Inline Comment Form
322 // Inline Comment Form
298 .injected_diff .comment-inline-form,
323 .injected_diff .comment-inline-form,
299 .comment-inline-form {
324 .comment-inline-form {
300 background-color: white;
325 background-color: white;
301 margin-top: 10px;
326 margin-top: 10px;
302 margin-bottom: 20px;
327 margin-bottom: 20px;
303 }
328 }
304
329
305 .inline-form {
330 .inline-form {
306 padding: 10px 7px;
331 padding: 10px 7px;
307 }
332 }
308
333
309 .inline-form div {
334 .inline-form div {
310 max-width: 100%;
335 max-width: 100%;
311 }
336 }
312
337
313 .overlay {
338 .overlay {
314 display: none;
339 display: none;
315 position: absolute;
340 position: absolute;
316 width: 100%;
341 width: 100%;
317 text-align: center;
342 text-align: center;
318 vertical-align: middle;
343 vertical-align: middle;
319 font-size: 16px;
344 font-size: 16px;
320 background: none repeat scroll 0 0 white;
345 background: none repeat scroll 0 0 white;
321
346
322 &.submitting {
347 &.submitting {
323 display: block;
348 display: block;
324 opacity: 0.5;
349 opacity: 0.5;
325 z-index: 100;
350 z-index: 100;
326 }
351 }
327 }
352 }
328 .comment-inline-form .overlay.submitting .overlay-text {
353 .comment-inline-form .overlay.submitting .overlay-text {
329 margin-top: 5%;
354 margin-top: 5%;
330 }
355 }
331
356
332 .comment-inline-form .clearfix,
357 .comment-inline-form .clearfix,
333 .comment-form .clearfix {
358 .comment-form .clearfix {
334 .border-radius(@border-radius);
359 .border-radius(@border-radius);
335 margin: 0px;
360 margin: 0px;
336 }
361 }
337
362
338 .comment-inline-form .comment-footer {
363 .comment-inline-form .comment-footer {
339 margin: 10px 0px 0px 0px;
364 margin: 10px 0px 0px 0px;
340 }
365 }
341
366
342 .hide-inline-form-button {
367 .hide-inline-form-button {
343 margin-left: 5px;
368 margin-left: 5px;
344 }
369 }
345 .comment-button .hide-inline-form {
370 .comment-button .hide-inline-form {
346 background: white;
371 background: white;
347 }
372 }
348
373
349 .comment-area {
374 .comment-area {
350 padding: 8px 12px;
375 padding: 8px 12px;
351 border: 1px solid @grey5;
376 border: 1px solid @grey5;
352 .border-radius(@border-radius);
377 .border-radius(@border-radius);
353 }
378 }
354
379
355 .comment-area-header .nav-links {
380 .comment-area-header .nav-links {
356 display: flex;
381 display: flex;
357 flex-flow: row wrap;
382 flex-flow: row wrap;
358 -webkit-flex-flow: row wrap;
383 -webkit-flex-flow: row wrap;
359 width: 100%;
384 width: 100%;
360 }
385 }
361
386
362 .comment-area-footer {
387 .comment-area-footer {
363 display: flex;
388 display: flex;
364 }
389 }
365
390
366 .comment-footer .toolbar {
391 .comment-footer .toolbar {
367
392
368 }
393 }
369
394
370 .nav-links {
395 .nav-links {
371 padding: 0;
396 padding: 0;
372 margin: 0;
397 margin: 0;
373 list-style: none;
398 list-style: none;
374 height: auto;
399 height: auto;
375 border-bottom: 1px solid @grey5;
400 border-bottom: 1px solid @grey5;
376 }
401 }
377 .nav-links li {
402 .nav-links li {
378 display: inline-block;
403 display: inline-block;
379 }
404 }
380 .nav-links li:before {
405 .nav-links li:before {
381 content: "";
406 content: "";
382 }
407 }
383 .nav-links li a.disabled {
408 .nav-links li a.disabled {
384 cursor: not-allowed;
409 cursor: not-allowed;
385 }
410 }
386
411
387 .nav-links li.active a {
412 .nav-links li.active a {
388 border-bottom: 2px solid @rcblue;
413 border-bottom: 2px solid @rcblue;
389 color: #000;
414 color: #000;
390 font-weight: 600;
415 font-weight: 600;
391 }
416 }
392 .nav-links li a {
417 .nav-links li a {
393 display: inline-block;
418 display: inline-block;
394 padding: 0px 10px 5px 10px;
419 padding: 0px 10px 5px 10px;
395 margin-bottom: -1px;
420 margin-bottom: -1px;
396 font-size: 14px;
421 font-size: 14px;
397 line-height: 28px;
422 line-height: 28px;
398 color: #8f8f8f;
423 color: #8f8f8f;
399 border-bottom: 2px solid transparent;
424 border-bottom: 2px solid transparent;
400 }
425 }
401
426
402 .toolbar-text {
427 .toolbar-text {
403 float: left;
428 float: left;
404 margin: -5px 0px 0px 0px;
429 margin: -5px 0px 0px 0px;
405 font-size: 12px;
430 font-size: 12px;
406 }
431 }
407
432
@@ -1,4 +1,4 b''
1 ## this is a dummy html file for partial rendering on server and sending
1 ## this is a dummy html file for partial rendering on server and sending
2 ## generated output via ajax after comment submit
2 ## generated output via ajax after comment submit
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ${comment.comment_block(c.co, inline=True)}
4 ${comment.comment_block(c.co, inline=c.inline_comment)}
@@ -1,263 +1,297 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10
11
11 <div class="comment
12 <div class="comment
12 ${'comment-inline' if inline else ''}
13 ${'comment-inline' if inline else 'comment-general'}
13 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
14 id="comment-${comment.comment_id}"
15 id="comment-${comment.comment_id}"
15 line="${comment.line_no}"
16 line="${comment.line_no}"
16 data-comment-id="${comment.comment_id}"
17 data-comment-id="${comment.comment_id}"
17 style="${'display: none;' if outdated_at_ver else ''}">
18 style="${'display: none;' if outdated_at_ver else ''}">
18
19
19 <div class="meta">
20 <div class="meta">
20 <div class="author">
21 <div class="author ${'author-inline' if inline else 'author-general'}">
21 ${base.gravatar_with_user(comment.author.email, 16)}
22 ${base.gravatar_with_user(comment.author.email, 20)}
22 </div>
23 </div>
23 <div class="date">
24 <div class="date">
24 ${h.age_component(comment.modified_at, time_is_local=True)}
25 ${h.age_component(comment.modified_at, time_is_local=True)}
25 </div>
26 </div>
27 % if inline:
28 <span></span>
29 % else:
26 <div class="status-change">
30 <div class="status-change">
27 % if comment.pull_request:
31 % if comment.pull_request:
28 % if comment.outdated:
32 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
29 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
33 % if comment.status_change:
30 ${_('Outdated comment from pull request version {}').format(comment.pull_request_version_id)}
34 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
31 </a>
35 % else:
32 % else:
36 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
33 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
37 % endif
34 %if comment.status_change:
38 </a>
35 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
36 %else:
37 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
38 %endif
39 </a>
40 % endif
41 % else:
39 % else:
42 % if comment.status_change:
40 % if comment.status_change:
43 ${_('Status change on commit')}:
41 ${_('Status change on commit')}:
44 % else:
42 % else:
45 ${_('Comment on commit')}
43 ${_('Comment on commit')}
46 % endif
44 % endif
47 % endif
45 % endif
48 </div>
46 </div>
49 %if comment.status_change:
47 % endif
48
49 % if comment.status_change:
50 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
50 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
51 <div title="${_('Commit status')}" class="changeset-status-lbl">
51 <div title="${_('Commit status')}" class="changeset-status-lbl">
52 ${comment.status_change[0].status_lbl}
52 ${comment.status_change[0].status_lbl}
53 </div>
53 </div>
54 %endif
54 % endif
55
55 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
56 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
56
57
57 <div class="comment-links-block">
58 <div class="comment-links-block">
59
60 % if inline:
61 % if outdated_at_ver:
62 <div class="pr-version-inline">
63 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
64 <code class="pr-version-num">
65 outdated ${'v{}'.format(pr_index_ver)}
66 </code>
67 </a>
68 </div>
69 |
70 % endif
71 % else:
72 % if comment.pull_request_version_id and pr_index_ver:
73 |
74 <div class="pr-version">
75 % if comment.outdated:
76 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
77 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
78 </a>
79 % else:
80 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
81 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
82 <code class="pr-version-num">
83 ${'v{}'.format(pr_index_ver)}
84 </code>
85 </a>
86 </div>
87 % endif
88 </div>
89 % endif
90 % endif
91
58 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
92 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
59 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
93 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
60 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
94 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
61 ## permissions to delete
95 ## permissions to delete
62 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
96 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
63 ## TODO: dan: add edit comment here
97 ## TODO: dan: add edit comment here
64 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
98 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
65 %else:
99 %else:
66 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
100 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
67 %endif
101 %endif
68 %else:
102 %else:
69 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
103 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
70 %endif
104 %endif
71
105
72 %if not outdated_at_ver:
106 %if not outdated_at_ver:
73 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
107 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
74 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
108 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
75 %endif
109 %endif
76
110
77 </div>
111 </div>
78 </div>
112 </div>
79 <div class="text">
113 <div class="text">
80 ${comment.render(mentions=True)|n}
114 ${comment.render(mentions=True)|n}
81 </div>
115 </div>
82
116
83 </div>
117 </div>
84 </%def>
118 </%def>
85 ## generate main comments
119 ## generate main comments
86 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
120 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
87 <div id="comments">
121 <div id="comments">
88 %for comment in c.comments:
122 %for comment in c.comments:
89 <div id="comment-tr-${comment.comment_id}">
123 <div id="comment-tr-${comment.comment_id}">
90 ## only render comments that are not from pull request, or from
124 ## only render comments that are not from pull request, or from
91 ## pull request and a status change
125 ## pull request and a status change
92 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
126 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
93 ${comment_block(comment)}
127 ${comment_block(comment)}
94 %endif
128 %endif
95 </div>
129 </div>
96 %endfor
130 %endfor
97 ## to anchor ajax comments
131 ## to anchor ajax comments
98 <div id="injected_page_comments"></div>
132 <div id="injected_page_comments"></div>
99 </div>
133 </div>
100 </%def>
134 </%def>
101
135
102 ## MAIN COMMENT FORM
136 ## MAIN COMMENT FORM
103 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
137 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
104
138
105 %if is_compare:
139 %if is_compare:
106 <% form_id = "comments_form_compare" %>
140 <% form_id = "comments_form_compare" %>
107 %else:
141 %else:
108 <% form_id = "comments_form" %>
142 <% form_id = "comments_form" %>
109 %endif
143 %endif
110
144
111
145
112 %if is_pull_request:
146 %if is_pull_request:
113 <div class="pull-request-merge">
147 <div class="pull-request-merge">
114 %if c.allowed_to_merge:
148 %if c.allowed_to_merge:
115 <div class="pull-request-wrap">
149 <div class="pull-request-wrap">
116 <div class="pull-right">
150 <div class="pull-right">
117 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
151 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
118 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
152 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
119 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
153 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
120 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
154 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
121 ${h.end_form()}
155 ${h.end_form()}
122 </div>
156 </div>
123 </div>
157 </div>
124 %else:
158 %else:
125 <div class="pull-request-wrap">
159 <div class="pull-request-wrap">
126 <div class="pull-right">
160 <div class="pull-right">
127 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
161 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
128 </div>
162 </div>
129 </div>
163 </div>
130 %endif
164 %endif
131 </div>
165 </div>
132 %endif
166 %endif
133 <div class="comments">
167 <div class="comments">
134 <%
168 <%
135 if is_pull_request:
169 if is_pull_request:
136 placeholder = _('Leave a comment on this Pull Request.')
170 placeholder = _('Leave a comment on this Pull Request.')
137 elif is_compare:
171 elif is_compare:
138 placeholder = _('Leave a comment on all commits in this range.')
172 placeholder = _('Leave a comment on all commits in this range.')
139 else:
173 else:
140 placeholder = _('Leave a comment on this Commit.')
174 placeholder = _('Leave a comment on this Commit.')
141 %>
175 %>
142 % if c.rhodecode_user.username != h.DEFAULT_USER:
176 % if c.rhodecode_user.username != h.DEFAULT_USER:
143 <div class="comment-form ac">
177 <div class="comment-form ac">
144 ${h.secure_form(post_url, id_=form_id)}
178 ${h.secure_form(post_url, id_=form_id)}
145 <div class="comment-area">
179 <div class="comment-area">
146 <div class="comment-area-header">
180 <div class="comment-area-header">
147 <ul class="nav-links clearfix">
181 <ul class="nav-links clearfix">
148 <li class="active">
182 <li class="active">
149 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
183 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
150 </li>
184 </li>
151 <li class="">
185 <li class="">
152 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
186 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
153 </li>
187 </li>
154 </ul>
188 </ul>
155 </div>
189 </div>
156
190
157 <div class="comment-area-write" style="display: block;">
191 <div class="comment-area-write" style="display: block;">
158 <div id="edit-container">
192 <div id="edit-container">
159 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
193 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
160 </div>
194 </div>
161 <div id="preview-container" class="clearfix" style="display: none;">
195 <div id="preview-container" class="clearfix" style="display: none;">
162 <div id="preview-box" class="preview-box"></div>
196 <div id="preview-box" class="preview-box"></div>
163 </div>
197 </div>
164 </div>
198 </div>
165
199
166 <div class="comment-area-footer">
200 <div class="comment-area-footer">
167 <div class="toolbar">
201 <div class="toolbar">
168 <div class="toolbar-text">
202 <div class="toolbar-text">
169 ${(_('Comments parsed using %s syntax with %s support.') % (
203 ${(_('Comments parsed using %s syntax with %s support.') % (
170 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
204 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
171 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
205 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
172 )
206 )
173 )|n}
207 )|n}
174 </div>
208 </div>
175 </div>
209 </div>
176 </div>
210 </div>
177 </div>
211 </div>
178
212
179 <div id="comment_form_extras">
213 <div id="comment_form_extras">
180 %if form_extras and isinstance(form_extras, (list, tuple)):
214 %if form_extras and isinstance(form_extras, (list, tuple)):
181 % for form_ex_el in form_extras:
215 % for form_ex_el in form_extras:
182 ${form_ex_el|n}
216 ${form_ex_el|n}
183 % endfor
217 % endfor
184 %endif
218 %endif
185 </div>
219 </div>
186 <div class="comment-footer">
220 <div class="comment-footer">
187 %if change_status:
221 %if change_status:
188 <div class="status_box">
222 <div class="status_box">
189 <select id="change_status" name="changeset_status">
223 <select id="change_status" name="changeset_status">
190 <option></option> # Placeholder
224 <option></option> # Placeholder
191 %for status,lbl in c.commit_statuses:
225 %for status,lbl in c.commit_statuses:
192 <option value="${status}" data-status="${status}">${lbl}</option>
226 <option value="${status}" data-status="${status}">${lbl}</option>
193 %if is_pull_request and change_status and status in ('approved', 'rejected'):
227 %if is_pull_request and change_status and status in ('approved', 'rejected'):
194 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
228 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
195 %endif
229 %endif
196 %endfor
230 %endfor
197 </select>
231 </select>
198 </div>
232 </div>
199 %endif
233 %endif
200 <div class="action-buttons">
234 <div class="action-buttons">
201 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
235 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
202 </div>
236 </div>
203 </div>
237 </div>
204 ${h.end_form()}
238 ${h.end_form()}
205 </div>
239 </div>
206 % else:
240 % else:
207 <div class="comment-form ac">
241 <div class="comment-form ac">
208
242
209 <div class="comment-area">
243 <div class="comment-area">
210 <div class="comment-area-header">
244 <div class="comment-area-header">
211 <ul class="nav-links clearfix">
245 <ul class="nav-links clearfix">
212 <li class="active">
246 <li class="active">
213 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
247 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
214 </li>
248 </li>
215 <li class="">
249 <li class="">
216 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
250 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
217 </li>
251 </li>
218 </ul>
252 </ul>
219 </div>
253 </div>
220
254
221 <div class="comment-area-write" style="display: block;">
255 <div class="comment-area-write" style="display: block;">
222 <div id="edit-container">
256 <div id="edit-container">
223 <div style="padding: 40px 0">
257 <div style="padding: 40px 0">
224 ${_('You need to be logged in to leave comments.')}
258 ${_('You need to be logged in to leave comments.')}
225 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
259 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
226 </div>
260 </div>
227 </div>
261 </div>
228 <div id="preview-container" class="clearfix" style="display: none;">
262 <div id="preview-container" class="clearfix" style="display: none;">
229 <div id="preview-box" class="preview-box"></div>
263 <div id="preview-box" class="preview-box"></div>
230 </div>
264 </div>
231 </div>
265 </div>
232
266
233 <div class="comment-area-footer">
267 <div class="comment-area-footer">
234 <div class="toolbar">
268 <div class="toolbar">
235 <div class="toolbar-text">
269 <div class="toolbar-text">
236 </div>
270 </div>
237 </div>
271 </div>
238 </div>
272 </div>
239 </div>
273 </div>
240
274
241 <div class="comment-footer">
275 <div class="comment-footer">
242 </div>
276 </div>
243
277
244 </div>
278 </div>
245 % endif
279 % endif
246
280
247 </div>
281 </div>
248
282
249 <script>
283 <script>
250 // init active elements of commentForm
284 // init active elements of commentForm
251 var commitId = templateContext.commit_data.commit_id;
285 var commitId = templateContext.commit_data.commit_id;
252 var pullRequestId = templateContext.pull_request_data.pull_request_id;
286 var pullRequestId = templateContext.pull_request_data.pull_request_id;
253 var lineNo;
287 var lineNo;
254
288
255 var mainCommentForm = new CommentForm(
289 var mainCommentForm = new CommentForm(
256 "#${form_id}", commitId, pullRequestId, lineNo, true);
290 "#${form_id}", commitId, pullRequestId, lineNo, true);
257
291
258 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
292 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
259
293
260 mainCommentForm.initStatusChangeSelector();
294 mainCommentForm.initStatusChangeSelector();
261 bindToggleButtons();
295 bindToggleButtons();
262 </script>
296 </script>
263 </%def>
297 </%def>
@@ -1,711 +1,711 b''
1 <%def name="diff_line_anchor(filename, line, type)"><%
1 <%def name="diff_line_anchor(filename, line, type)"><%
2 return '%s_%s_%i' % (h.safeid(filename), type, line)
2 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 %></%def>
3 %></%def>
4
4
5 <%def name="action_class(action)">
5 <%def name="action_class(action)">
6 <%
6 <%
7 return {
7 return {
8 '-': 'cb-deletion',
8 '-': 'cb-deletion',
9 '+': 'cb-addition',
9 '+': 'cb-addition',
10 ' ': 'cb-context',
10 ' ': 'cb-context',
11 }.get(action, 'cb-empty')
11 }.get(action, 'cb-empty')
12 %>
12 %>
13 </%def>
13 </%def>
14
14
15 <%def name="op_class(op_id)">
15 <%def name="op_class(op_id)">
16 <%
16 <%
17 return {
17 return {
18 DEL_FILENODE: 'deletion', # file deleted
18 DEL_FILENODE: 'deletion', # file deleted
19 BIN_FILENODE: 'warning' # binary diff hidden
19 BIN_FILENODE: 'warning' # binary diff hidden
20 }.get(op_id, 'addition')
20 }.get(op_id, 'addition')
21 %>
21 %>
22 </%def>
22 </%def>
23
23
24 <%def name="link_for(**kw)">
24 <%def name="link_for(**kw)">
25 <%
25 <%
26 new_args = request.GET.mixed()
26 new_args = request.GET.mixed()
27 new_args.update(kw)
27 new_args.update(kw)
28 return h.url('', **new_args)
28 return h.url('', **new_args)
29 %>
29 %>
30 </%def>
30 </%def>
31
31
32 <%def name="render_diffset(diffset, commit=None,
32 <%def name="render_diffset(diffset, commit=None,
33
33
34 # collapse all file diff entries when there are more than this amount of files in the diff
34 # collapse all file diff entries when there are more than this amount of files in the diff
35 collapse_when_files_over=20,
35 collapse_when_files_over=20,
36
36
37 # collapse lines in the diff when more than this amount of lines changed in the file diff
37 # collapse lines in the diff when more than this amount of lines changed in the file diff
38 lines_changed_limit=500,
38 lines_changed_limit=500,
39
39
40 # add a ruler at to the output
40 # add a ruler at to the output
41 ruler_at_chars=0,
41 ruler_at_chars=0,
42
42
43 # show inline comments
43 # show inline comments
44 use_comments=False,
44 use_comments=False,
45
45
46 # disable new comments
46 # disable new comments
47 disable_new_comments=False,
47 disable_new_comments=False,
48
48
49 # special file-comments that were deleted in previous versions
49 # special file-comments that were deleted in previous versions
50 # it's used for showing outdated comments for deleted files in a PR
50 # it's used for showing outdated comments for deleted files in a PR
51 deleted_files_comments=None
51 deleted_files_comments=None
52
52
53 )">
53 )">
54
54
55 %if use_comments:
55 %if use_comments:
56 <div id="cb-comments-inline-container-template" class="js-template">
56 <div id="cb-comments-inline-container-template" class="js-template">
57 ${inline_comments_container([])}
57 ${inline_comments_container([])}
58 </div>
58 </div>
59 <div class="js-template" id="cb-comment-inline-form-template">
59 <div class="js-template" id="cb-comment-inline-form-template">
60 <div class="comment-inline-form ac">
60 <div class="comment-inline-form ac">
61
61
62 %if c.rhodecode_user.username != h.DEFAULT_USER:
62 %if c.rhodecode_user.username != h.DEFAULT_USER:
63 ${h.form('#', method='get')}
63 ${h.form('#', method='get')}
64 <div class="comment-area">
64 <div class="comment-area">
65 <div class="comment-area-header">
65 <div class="comment-area-header">
66 <ul class="nav-links clearfix">
66 <ul class="nav-links clearfix">
67 <li class="active">
67 <li class="active">
68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
69 </li>
69 </li>
70 <li class="">
70 <li class="">
71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
72 </li>
72 </li>
73 </ul>
73 </ul>
74 </div>
74 </div>
75
75
76 <div class="comment-area-write" style="display: block;">
76 <div class="comment-area-write" style="display: block;">
77 <div id="edit-container_{1}">
77 <div id="edit-container_{1}">
78 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
78 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
79 </div>
79 </div>
80 <div id="preview-container_{1}" class="clearfix" style="display: none;">
80 <div id="preview-container_{1}" class="clearfix" style="display: none;">
81 <div id="preview-box_{1}" class="preview-box"></div>
81 <div id="preview-box_{1}" class="preview-box"></div>
82 </div>
82 </div>
83 </div>
83 </div>
84
84
85 <div class="comment-area-footer">
85 <div class="comment-area-footer">
86 <div class="toolbar">
86 <div class="toolbar">
87 <div class="toolbar-text">
87 <div class="toolbar-text">
88 ${(_('Comments parsed using %s syntax with %s support.') % (
88 ${(_('Comments parsed using %s syntax with %s support.') % (
89 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
89 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
90 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
90 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
91 )
91 )
92 )|n}
92 )|n}
93 </div>
93 </div>
94 </div>
94 </div>
95 </div>
95 </div>
96 </div>
96 </div>
97
97
98 <div class="comment-footer">
98 <div class="comment-footer">
99 <div class="action-buttons">
99 <div class="action-buttons">
100 <input type="hidden" name="f_path" value="{0}">
100 <input type="hidden" name="f_path" value="{0}">
101 <input type="hidden" name="line" value="{1}">
101 <input type="hidden" name="line" value="{1}">
102 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
102 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
103 ${_('Cancel')}
103 ${_('Cancel')}
104 </button>
104 </button>
105 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
105 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
106 </div>
106 </div>
107 ${h.end_form()}
107 ${h.end_form()}
108 </div>
108 </div>
109 %else:
109 %else:
110 ${h.form('', class_='inline-form comment-form-login', method='get')}
110 ${h.form('', class_='inline-form comment-form-login', method='get')}
111 <div class="pull-left">
111 <div class="pull-left">
112 <div class="comment-help pull-right">
112 <div class="comment-help pull-right">
113 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
113 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
114 </div>
114 </div>
115 </div>
115 </div>
116 <div class="comment-button pull-right">
116 <div class="comment-button pull-right">
117 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
117 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
118 ${_('Cancel')}
118 ${_('Cancel')}
119 </button>
119 </button>
120 </div>
120 </div>
121 <div class="clearfix"></div>
121 <div class="clearfix"></div>
122 ${h.end_form()}
122 ${h.end_form()}
123 %endif
123 %endif
124 </div>
124 </div>
125 </div>
125 </div>
126
126
127 %endif
127 %endif
128 <%
128 <%
129 collapse_all = len(diffset.files) > collapse_when_files_over
129 collapse_all = len(diffset.files) > collapse_when_files_over
130 %>
130 %>
131
131
132 %if c.diffmode == 'sideside':
132 %if c.diffmode == 'sideside':
133 <style>
133 <style>
134 .wrapper {
134 .wrapper {
135 max-width: 1600px !important;
135 max-width: 1600px !important;
136 }
136 }
137 </style>
137 </style>
138 %endif
138 %endif
139
139
140 %if ruler_at_chars:
140 %if ruler_at_chars:
141 <style>
141 <style>
142 .diff table.cb .cb-content:after {
142 .diff table.cb .cb-content:after {
143 content: "";
143 content: "";
144 border-left: 1px solid blue;
144 border-left: 1px solid blue;
145 position: absolute;
145 position: absolute;
146 top: 0;
146 top: 0;
147 height: 18px;
147 height: 18px;
148 opacity: .2;
148 opacity: .2;
149 z-index: 10;
149 z-index: 10;
150 //## +5 to account for diff action (+/-)
150 //## +5 to account for diff action (+/-)
151 left: ${ruler_at_chars + 5}ch;
151 left: ${ruler_at_chars + 5}ch;
152 </style>
152 </style>
153 %endif
153 %endif
154
154
155 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
155 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
156 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
156 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
157 %if commit:
157 %if commit:
158 <div class="pull-right">
158 <div class="pull-right">
159 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
159 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
160 ${_('Browse Files')}
160 ${_('Browse Files')}
161 </a>
161 </a>
162 </div>
162 </div>
163 %endif
163 %endif
164 <h2 class="clearinner">
164 <h2 class="clearinner">
165 %if commit:
165 %if commit:
166 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
166 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
167 ${h.age_component(commit.date)} -
167 ${h.age_component(commit.date)} -
168 %endif
168 %endif
169 %if diffset.limited_diff:
169 %if diffset.limited_diff:
170 ${_('The requested commit is too big and content was truncated.')}
170 ${_('The requested commit is too big and content was truncated.')}
171
171
172 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
172 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
173 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
173 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
174 %else:
174 %else:
175 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
175 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
176 '%(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}}
176 '%(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}}
177 %endif
177 %endif
178
178
179 <% at_ver = getattr(c, 'at_version_num', None) %>
179 <% at_ver = getattr(c, 'at_version_pos', None) %>
180 % if at_ver:
180 % if at_ver:
181 <div class="pull-right">
181 <div class="pull-right">
182 ${_('Changes at version %d') % at_ver}
182 ${_('Showing changes at version %d') % at_ver}
183 </div>
183 </div>
184 % endif
184 % endif
185
185
186 </h2>
186 </h2>
187 </div>
187 </div>
188
188
189 %if not diffset.files:
189 %if not diffset.files:
190 <p class="empty_data">${_('No files')}</p>
190 <p class="empty_data">${_('No files')}</p>
191 %endif
191 %endif
192
192
193 <div class="filediffs">
193 <div class="filediffs">
194 %for i, filediff in enumerate(diffset.files):
194 %for i, filediff in enumerate(diffset.files):
195
195
196 <%
196 <%
197 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
197 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
198 over_lines_changed_limit = lines_changed > lines_changed_limit
198 over_lines_changed_limit = lines_changed > lines_changed_limit
199 %>
199 %>
200 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
200 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
201 <div
201 <div
202 class="filediff"
202 class="filediff"
203 data-f-path="${filediff['patch']['filename']}"
203 data-f-path="${filediff['patch']['filename']}"
204 id="a_${h.FID('', filediff['patch']['filename'])}">
204 id="a_${h.FID('', filediff['patch']['filename'])}">
205 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
205 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
206 <div class="filediff-collapse-indicator"></div>
206 <div class="filediff-collapse-indicator"></div>
207 ${diff_ops(filediff)}
207 ${diff_ops(filediff)}
208 </label>
208 </label>
209 ${diff_menu(filediff, use_comments=use_comments)}
209 ${diff_menu(filediff, use_comments=use_comments)}
210 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
210 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
211 %if not filediff.hunks:
211 %if not filediff.hunks:
212 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
212 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
213 <tr>
213 <tr>
214 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
214 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
215 %if op_id == DEL_FILENODE:
215 %if op_id == DEL_FILENODE:
216 ${_('File was deleted')}
216 ${_('File was deleted')}
217 %elif op_id == BIN_FILENODE:
217 %elif op_id == BIN_FILENODE:
218 ${_('Binary file hidden')}
218 ${_('Binary file hidden')}
219 %else:
219 %else:
220 ${op_text}
220 ${op_text}
221 %endif
221 %endif
222 </td>
222 </td>
223 </tr>
223 </tr>
224 %endfor
224 %endfor
225 %endif
225 %endif
226 %if filediff.patch['is_limited_diff']:
226 %if filediff.patch['is_limited_diff']:
227 <tr class="cb-warning cb-collapser">
227 <tr class="cb-warning cb-collapser">
228 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
228 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
229 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
229 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
230 </td>
230 </td>
231 </tr>
231 </tr>
232 %else:
232 %else:
233 %if over_lines_changed_limit:
233 %if over_lines_changed_limit:
234 <tr class="cb-warning cb-collapser">
234 <tr class="cb-warning cb-collapser">
235 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
235 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
236 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
236 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
237 <a href="#" class="cb-expand"
237 <a href="#" class="cb-expand"
238 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
238 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
239 </a>
239 </a>
240 <a href="#" class="cb-collapse"
240 <a href="#" class="cb-collapse"
241 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
241 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
242 </a>
242 </a>
243 </td>
243 </td>
244 </tr>
244 </tr>
245 %endif
245 %endif
246 %endif
246 %endif
247
247
248 %for hunk in filediff.hunks:
248 %for hunk in filediff.hunks:
249 <tr class="cb-hunk">
249 <tr class="cb-hunk">
250 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
250 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
251 ## TODO: dan: add ajax loading of more context here
251 ## TODO: dan: add ajax loading of more context here
252 ## <a href="#">
252 ## <a href="#">
253 <i class="icon-more"></i>
253 <i class="icon-more"></i>
254 ## </a>
254 ## </a>
255 </td>
255 </td>
256 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
256 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
257 @@
257 @@
258 -${hunk.source_start},${hunk.source_length}
258 -${hunk.source_start},${hunk.source_length}
259 +${hunk.target_start},${hunk.target_length}
259 +${hunk.target_start},${hunk.target_length}
260 ${hunk.section_header}
260 ${hunk.section_header}
261 </td>
261 </td>
262 </tr>
262 </tr>
263 %if c.diffmode == 'unified':
263 %if c.diffmode == 'unified':
264 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
264 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
265 %elif c.diffmode == 'sideside':
265 %elif c.diffmode == 'sideside':
266 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
266 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
267 %else:
267 %else:
268 <tr class="cb-line">
268 <tr class="cb-line">
269 <td>unknown diff mode</td>
269 <td>unknown diff mode</td>
270 </tr>
270 </tr>
271 %endif
271 %endif
272 %endfor
272 %endfor
273
273
274 ## outdated comments that do not fit into currently displayed lines
274 ## outdated comments that do not fit into currently displayed lines
275 % for lineno, comments in filediff.left_comments.items():
275 % for lineno, comments in filediff.left_comments.items():
276
276
277 %if c.diffmode == 'unified':
277 %if c.diffmode == 'unified':
278 <tr class="cb-line">
278 <tr class="cb-line">
279 <td class="cb-data cb-context"></td>
279 <td class="cb-data cb-context"></td>
280 <td class="cb-lineno cb-context"></td>
280 <td class="cb-lineno cb-context"></td>
281 <td class="cb-lineno cb-context"></td>
281 <td class="cb-lineno cb-context"></td>
282 <td class="cb-content cb-context">
282 <td class="cb-content cb-context">
283 ${inline_comments_container(comments)}
283 ${inline_comments_container(comments)}
284 </td>
284 </td>
285 </tr>
285 </tr>
286 %elif c.diffmode == 'sideside':
286 %elif c.diffmode == 'sideside':
287 <tr class="cb-line">
287 <tr class="cb-line">
288 <td class="cb-data cb-context"></td>
288 <td class="cb-data cb-context"></td>
289 <td class="cb-lineno cb-context"></td>
289 <td class="cb-lineno cb-context"></td>
290 <td class="cb-content cb-context"></td>
290 <td class="cb-content cb-context"></td>
291
291
292 <td class="cb-data cb-context"></td>
292 <td class="cb-data cb-context"></td>
293 <td class="cb-lineno cb-context"></td>
293 <td class="cb-lineno cb-context"></td>
294 <td class="cb-content cb-context">
294 <td class="cb-content cb-context">
295 ${inline_comments_container(comments)}
295 ${inline_comments_container(comments)}
296 </td>
296 </td>
297 </tr>
297 </tr>
298 %endif
298 %endif
299
299
300 % endfor
300 % endfor
301
301
302 </table>
302 </table>
303 </div>
303 </div>
304 %endfor
304 %endfor
305
305
306 ## outdated comments that are made for a file that has been deleted
306 ## outdated comments that are made for a file that has been deleted
307 % for filename, comments_dict in (deleted_files_comments or {}).items():
307 % for filename, comments_dict in (deleted_files_comments or {}).items():
308
308
309 <div class="filediffs filediff-outdated" style="display: none">
309 <div class="filediffs filediff-outdated" style="display: none">
310 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
310 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
311 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
311 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
312 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
312 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
313 <div class="filediff-collapse-indicator"></div>
313 <div class="filediff-collapse-indicator"></div>
314 <span class="pill">
314 <span class="pill">
315 ## file was deleted
315 ## file was deleted
316 <strong>${filename}</strong>
316 <strong>${filename}</strong>
317 </span>
317 </span>
318 <span class="pill-group" style="float: left">
318 <span class="pill-group" style="float: left">
319 ## file op, doesn't need translation
319 ## file op, doesn't need translation
320 <span class="pill" op="removed">removed in this version</span>
320 <span class="pill" op="removed">removed in this version</span>
321 </span>
321 </span>
322 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
322 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
323 <span class="pill-group" style="float: right">
323 <span class="pill-group" style="float: right">
324 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
324 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
325 </span>
325 </span>
326 </label>
326 </label>
327
327
328 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
328 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
329 <tr>
329 <tr>
330 % if c.diffmode == 'unified':
330 % if c.diffmode == 'unified':
331 <td></td>
331 <td></td>
332 %endif
332 %endif
333
333
334 <td></td>
334 <td></td>
335 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
335 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
336 ${_('File was deleted in this version, and outdated comments were made on it')}
336 ${_('File was deleted in this version, and outdated comments were made on it')}
337 </td>
337 </td>
338 </tr>
338 </tr>
339 %if c.diffmode == 'unified':
339 %if c.diffmode == 'unified':
340 <tr class="cb-line">
340 <tr class="cb-line">
341 <td class="cb-data cb-context"></td>
341 <td class="cb-data cb-context"></td>
342 <td class="cb-lineno cb-context"></td>
342 <td class="cb-lineno cb-context"></td>
343 <td class="cb-lineno cb-context"></td>
343 <td class="cb-lineno cb-context"></td>
344 <td class="cb-content cb-context">
344 <td class="cb-content cb-context">
345 ${inline_comments_container(comments_dict['comments'])}
345 ${inline_comments_container(comments_dict['comments'])}
346 </td>
346 </td>
347 </tr>
347 </tr>
348 %elif c.diffmode == 'sideside':
348 %elif c.diffmode == 'sideside':
349 <tr class="cb-line">
349 <tr class="cb-line">
350 <td class="cb-data cb-context"></td>
350 <td class="cb-data cb-context"></td>
351 <td class="cb-lineno cb-context"></td>
351 <td class="cb-lineno cb-context"></td>
352 <td class="cb-content cb-context"></td>
352 <td class="cb-content cb-context"></td>
353
353
354 <td class="cb-data cb-context"></td>
354 <td class="cb-data cb-context"></td>
355 <td class="cb-lineno cb-context"></td>
355 <td class="cb-lineno cb-context"></td>
356 <td class="cb-content cb-context">
356 <td class="cb-content cb-context">
357 ${inline_comments_container(comments_dict['comments'])}
357 ${inline_comments_container(comments_dict['comments'])}
358 </td>
358 </td>
359 </tr>
359 </tr>
360 %endif
360 %endif
361 </table>
361 </table>
362 </div>
362 </div>
363 </div>
363 </div>
364 % endfor
364 % endfor
365
365
366 </div>
366 </div>
367 </div>
367 </div>
368 </%def>
368 </%def>
369
369
370 <%def name="diff_ops(filediff)">
370 <%def name="diff_ops(filediff)">
371 <%
371 <%
372 stats = filediff['patch']['stats']
372 stats = filediff['patch']['stats']
373 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
373 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
374 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
374 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
375 %>
375 %>
376 <span class="pill">
376 <span class="pill">
377 %if filediff.source_file_path and filediff.target_file_path:
377 %if filediff.source_file_path and filediff.target_file_path:
378 %if filediff.source_file_path != filediff.target_file_path:
378 %if filediff.source_file_path != filediff.target_file_path:
379 ## file was renamed
379 ## file was renamed
380 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
380 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
381 %else:
381 %else:
382 ## file was modified
382 ## file was modified
383 <strong>${filediff.source_file_path}</strong>
383 <strong>${filediff.source_file_path}</strong>
384 %endif
384 %endif
385 %else:
385 %else:
386 %if filediff.source_file_path:
386 %if filediff.source_file_path:
387 ## file was deleted
387 ## file was deleted
388 <strong>${filediff.source_file_path}</strong>
388 <strong>${filediff.source_file_path}</strong>
389 %else:
389 %else:
390 ## file was added
390 ## file was added
391 <strong>${filediff.target_file_path}</strong>
391 <strong>${filediff.target_file_path}</strong>
392 %endif
392 %endif
393 %endif
393 %endif
394 </span>
394 </span>
395 <span class="pill-group" style="float: left">
395 <span class="pill-group" style="float: left">
396 %if filediff.patch['is_limited_diff']:
396 %if filediff.patch['is_limited_diff']:
397 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
398 %endif
398 %endif
399 %if RENAMED_FILENODE in stats['ops']:
399 %if RENAMED_FILENODE in stats['ops']:
400 <span class="pill" op="renamed">renamed</span>
400 <span class="pill" op="renamed">renamed</span>
401 %endif
401 %endif
402
402
403 %if NEW_FILENODE in stats['ops']:
403 %if NEW_FILENODE in stats['ops']:
404 <span class="pill" op="created">created</span>
404 <span class="pill" op="created">created</span>
405 %if filediff['target_mode'].startswith('120'):
405 %if filediff['target_mode'].startswith('120'):
406 <span class="pill" op="symlink">symlink</span>
406 <span class="pill" op="symlink">symlink</span>
407 %else:
407 %else:
408 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
408 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
409 %endif
409 %endif
410 %endif
410 %endif
411
411
412 %if DEL_FILENODE in stats['ops']:
412 %if DEL_FILENODE in stats['ops']:
413 <span class="pill" op="removed">removed</span>
413 <span class="pill" op="removed">removed</span>
414 %endif
414 %endif
415
415
416 %if CHMOD_FILENODE in stats['ops']:
416 %if CHMOD_FILENODE in stats['ops']:
417 <span class="pill" op="mode">
417 <span class="pill" op="mode">
418 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
418 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
419 </span>
419 </span>
420 %endif
420 %endif
421 </span>
421 </span>
422
422
423 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
423 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
424
424
425 <span class="pill-group" style="float: right">
425 <span class="pill-group" style="float: right">
426 %if BIN_FILENODE in stats['ops']:
426 %if BIN_FILENODE in stats['ops']:
427 <span class="pill" op="binary">binary</span>
427 <span class="pill" op="binary">binary</span>
428 %if MOD_FILENODE in stats['ops']:
428 %if MOD_FILENODE in 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 %if stats['added']:
432 %if stats['added']:
433 <span class="pill" op="added">+${stats['added']}</span>
433 <span class="pill" op="added">+${stats['added']}</span>
434 %endif
434 %endif
435 %if stats['deleted']:
435 %if stats['deleted']:
436 <span class="pill" op="deleted">-${stats['deleted']}</span>
436 <span class="pill" op="deleted">-${stats['deleted']}</span>
437 %endif
437 %endif
438 </span>
438 </span>
439
439
440 </%def>
440 </%def>
441
441
442 <%def name="nice_mode(filemode)">
442 <%def name="nice_mode(filemode)">
443 ${filemode.startswith('100') and filemode[3:] or filemode}
443 ${filemode.startswith('100') and filemode[3:] or filemode}
444 </%def>
444 </%def>
445
445
446 <%def name="diff_menu(filediff, use_comments=False)">
446 <%def name="diff_menu(filediff, use_comments=False)">
447 <div class="filediff-menu">
447 <div class="filediff-menu">
448 %if filediff.diffset.source_ref:
448 %if filediff.diffset.source_ref:
449 %if filediff.patch['operation'] in ['D', 'M']:
449 %if filediff.patch['operation'] in ['D', 'M']:
450 <a
450 <a
451 class="tooltip"
451 class="tooltip"
452 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
452 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
453 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
453 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
454 >
454 >
455 ${_('Show file before')}
455 ${_('Show file before')}
456 </a> |
456 </a> |
457 %else:
457 %else:
458 <span
458 <span
459 class="tooltip"
459 class="tooltip"
460 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
460 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 >
461 >
462 ${_('Show file before')}
462 ${_('Show file before')}
463 </span> |
463 </span> |
464 %endif
464 %endif
465 %if filediff.patch['operation'] in ['A', 'M']:
465 %if filediff.patch['operation'] in ['A', 'M']:
466 <a
466 <a
467 class="tooltip"
467 class="tooltip"
468 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
468 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
469 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
469 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
470 >
470 >
471 ${_('Show file after')}
471 ${_('Show file after')}
472 </a> |
472 </a> |
473 %else:
473 %else:
474 <span
474 <span
475 class="tooltip"
475 class="tooltip"
476 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
476 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
477 >
477 >
478 ${_('Show file after')}
478 ${_('Show file after')}
479 </span> |
479 </span> |
480 %endif
480 %endif
481 <a
481 <a
482 class="tooltip"
482 class="tooltip"
483 title="${h.tooltip(_('Raw diff'))}"
483 title="${h.tooltip(_('Raw diff'))}"
484 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
484 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
485 >
485 >
486 ${_('Raw diff')}
486 ${_('Raw diff')}
487 </a> |
487 </a> |
488 <a
488 <a
489 class="tooltip"
489 class="tooltip"
490 title="${h.tooltip(_('Download diff'))}"
490 title="${h.tooltip(_('Download diff'))}"
491 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
491 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
492 >
492 >
493 ${_('Download diff')}
493 ${_('Download diff')}
494 </a>
494 </a>
495 % if use_comments:
495 % if use_comments:
496 |
496 |
497 % endif
497 % endif
498
498
499 ## 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)
499 ## 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)
500 %if hasattr(c, 'ignorews_url'):
500 %if hasattr(c, 'ignorews_url'):
501 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
501 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
502 %endif
502 %endif
503 %if hasattr(c, 'context_url'):
503 %if hasattr(c, 'context_url'):
504 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
504 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
505 %endif
505 %endif
506
506
507 %if use_comments:
507 %if use_comments:
508 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
508 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
509 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
509 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
510 </a>
510 </a>
511 %endif
511 %endif
512 %endif
512 %endif
513 </div>
513 </div>
514 </%def>
514 </%def>
515
515
516
516
517 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
517 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
518 <%def name="inline_comments_container(comments)">
518 <%def name="inline_comments_container(comments)">
519 <div class="inline-comments">
519 <div class="inline-comments">
520 %for comment in comments:
520 %for comment in comments:
521 ${commentblock.comment_block(comment, inline=True)}
521 ${commentblock.comment_block(comment, inline=True)}
522 %endfor
522 %endfor
523
523
524 % if comments and comments[-1].outdated:
524 % if comments and comments[-1].outdated:
525 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
525 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
526 style="display: none;}">
526 style="display: none;}">
527 ${_('Add another comment')}
527 ${_('Add another comment')}
528 </span>
528 </span>
529 % else:
529 % else:
530 <span onclick="return Rhodecode.comments.createComment(this)"
530 <span onclick="return Rhodecode.comments.createComment(this)"
531 class="btn btn-secondary cb-comment-add-button">
531 class="btn btn-secondary cb-comment-add-button">
532 ${_('Add another comment')}
532 ${_('Add another comment')}
533 </span>
533 </span>
534 % endif
534 % endif
535
535
536 </div>
536 </div>
537 </%def>
537 </%def>
538
538
539
539
540 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
540 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
541 %for i, line in enumerate(hunk.sideside):
541 %for i, line in enumerate(hunk.sideside):
542 <%
542 <%
543 old_line_anchor, new_line_anchor = None, None
543 old_line_anchor, new_line_anchor = None, None
544 if line.original.lineno:
544 if line.original.lineno:
545 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
545 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
546 if line.modified.lineno:
546 if line.modified.lineno:
547 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
547 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
548 %>
548 %>
549
549
550 <tr class="cb-line">
550 <tr class="cb-line">
551 <td class="cb-data ${action_class(line.original.action)}"
551 <td class="cb-data ${action_class(line.original.action)}"
552 data-line-number="${line.original.lineno}"
552 data-line-number="${line.original.lineno}"
553 >
553 >
554 <div>
554 <div>
555 %if line.original.comments:
555 %if line.original.comments:
556 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
556 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
557 %endif
557 %endif
558 </div>
558 </div>
559 </td>
559 </td>
560 <td class="cb-lineno ${action_class(line.original.action)}"
560 <td class="cb-lineno ${action_class(line.original.action)}"
561 data-line-number="${line.original.lineno}"
561 data-line-number="${line.original.lineno}"
562 %if old_line_anchor:
562 %if old_line_anchor:
563 id="${old_line_anchor}"
563 id="${old_line_anchor}"
564 %endif
564 %endif
565 >
565 >
566 %if line.original.lineno:
566 %if line.original.lineno:
567 <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>
568 %endif
568 %endif
569 </td>
569 </td>
570 <td class="cb-content ${action_class(line.original.action)}"
570 <td class="cb-content ${action_class(line.original.action)}"
571 data-line-number="o${line.original.lineno}"
571 data-line-number="o${line.original.lineno}"
572 >
572 >
573 %if use_comments and line.original.lineno:
573 %if use_comments and line.original.lineno:
574 ${render_add_comment_button()}
574 ${render_add_comment_button()}
575 %endif
575 %endif
576 <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>
577 %if use_comments and line.original.lineno and line.original.comments:
577 %if use_comments and line.original.lineno and line.original.comments:
578 ${inline_comments_container(line.original.comments)}
578 ${inline_comments_container(line.original.comments)}
579 %endif
579 %endif
580 </td>
580 </td>
581 <td class="cb-data ${action_class(line.modified.action)}"
581 <td class="cb-data ${action_class(line.modified.action)}"
582 data-line-number="${line.modified.lineno}"
582 data-line-number="${line.modified.lineno}"
583 >
583 >
584 <div>
584 <div>
585 %if line.modified.comments:
585 %if line.modified.comments:
586 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
586 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
587 %endif
587 %endif
588 </div>
588 </div>
589 </td>
589 </td>
590 <td class="cb-lineno ${action_class(line.modified.action)}"
590 <td class="cb-lineno ${action_class(line.modified.action)}"
591 data-line-number="${line.modified.lineno}"
591 data-line-number="${line.modified.lineno}"
592 %if new_line_anchor:
592 %if new_line_anchor:
593 id="${new_line_anchor}"
593 id="${new_line_anchor}"
594 %endif
594 %endif
595 >
595 >
596 %if line.modified.lineno:
596 %if line.modified.lineno:
597 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
597 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
598 %endif
598 %endif
599 </td>
599 </td>
600 <td class="cb-content ${action_class(line.modified.action)}"
600 <td class="cb-content ${action_class(line.modified.action)}"
601 data-line-number="n${line.modified.lineno}"
601 data-line-number="n${line.modified.lineno}"
602 >
602 >
603 %if use_comments and line.modified.lineno:
603 %if use_comments and line.modified.lineno:
604 ${render_add_comment_button()}
604 ${render_add_comment_button()}
605 %endif
605 %endif
606 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
606 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
607 %if use_comments and line.modified.lineno and line.modified.comments:
607 %if use_comments and line.modified.lineno and line.modified.comments:
608 ${inline_comments_container(line.modified.comments)}
608 ${inline_comments_container(line.modified.comments)}
609 %endif
609 %endif
610 </td>
610 </td>
611 </tr>
611 </tr>
612 %endfor
612 %endfor
613 </%def>
613 </%def>
614
614
615
615
616 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
616 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
617 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
617 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
618 <%
618 <%
619 old_line_anchor, new_line_anchor = None, None
619 old_line_anchor, new_line_anchor = None, None
620 if old_line_no:
620 if old_line_no:
621 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
621 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
622 if new_line_no:
622 if new_line_no:
623 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
623 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
624 %>
624 %>
625 <tr class="cb-line">
625 <tr class="cb-line">
626 <td class="cb-data ${action_class(action)}">
626 <td class="cb-data ${action_class(action)}">
627 <div>
627 <div>
628 %if comments:
628 %if comments:
629 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
629 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
630 %endif
630 %endif
631 </div>
631 </div>
632 </td>
632 </td>
633 <td class="cb-lineno ${action_class(action)}"
633 <td class="cb-lineno ${action_class(action)}"
634 data-line-number="${old_line_no}"
634 data-line-number="${old_line_no}"
635 %if old_line_anchor:
635 %if old_line_anchor:
636 id="${old_line_anchor}"
636 id="${old_line_anchor}"
637 %endif
637 %endif
638 >
638 >
639 %if old_line_anchor:
639 %if old_line_anchor:
640 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
640 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
641 %endif
641 %endif
642 </td>
642 </td>
643 <td class="cb-lineno ${action_class(action)}"
643 <td class="cb-lineno ${action_class(action)}"
644 data-line-number="${new_line_no}"
644 data-line-number="${new_line_no}"
645 %if new_line_anchor:
645 %if new_line_anchor:
646 id="${new_line_anchor}"
646 id="${new_line_anchor}"
647 %endif
647 %endif
648 >
648 >
649 %if new_line_anchor:
649 %if new_line_anchor:
650 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
650 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
651 %endif
651 %endif
652 </td>
652 </td>
653 <td class="cb-content ${action_class(action)}"
653 <td class="cb-content ${action_class(action)}"
654 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
654 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
655 >
655 >
656 %if use_comments:
656 %if use_comments:
657 ${render_add_comment_button()}
657 ${render_add_comment_button()}
658 %endif
658 %endif
659 <span class="cb-code">${action} ${content or '' | n}</span>
659 <span class="cb-code">${action} ${content or '' | n}</span>
660 %if use_comments and comments:
660 %if use_comments and comments:
661 ${inline_comments_container(comments)}
661 ${inline_comments_container(comments)}
662 %endif
662 %endif
663 </td>
663 </td>
664 </tr>
664 </tr>
665 %endfor
665 %endfor
666 </%def>
666 </%def>
667
667
668 <%def name="render_add_comment_button()">
668 <%def name="render_add_comment_button()">
669 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
669 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
670 <span><i class="icon-comment"></i></span>
670 <span><i class="icon-comment"></i></span>
671 </button>
671 </button>
672 </%def>
672 </%def>
673
673
674 <%def name="render_diffset_menu()">
674 <%def name="render_diffset_menu()">
675
675
676 <div class="diffset-menu clearinner">
676 <div class="diffset-menu clearinner">
677 <div class="pull-right">
677 <div class="pull-right">
678 <div class="btn-group">
678 <div class="btn-group">
679
679
680 <a
680 <a
681 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
681 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
682 title="${_('View side by side')}"
682 title="${_('View side by side')}"
683 href="${h.url_replace(diffmode='sideside')}">
683 href="${h.url_replace(diffmode='sideside')}">
684 <span>${_('Side by Side')}</span>
684 <span>${_('Side by Side')}</span>
685 </a>
685 </a>
686 <a
686 <a
687 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
687 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
688 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
688 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
689 <span>${_('Unified')}</span>
689 <span>${_('Unified')}</span>
690 </a>
690 </a>
691 </div>
691 </div>
692 </div>
692 </div>
693
693
694 <div class="pull-left">
694 <div class="pull-left">
695 <div class="btn-group">
695 <div class="btn-group">
696 <a
696 <a
697 class="btn"
697 class="btn"
698 href="#"
698 href="#"
699 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
699 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
700 <a
700 <a
701 class="btn"
701 class="btn"
702 href="#"
702 href="#"
703 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
703 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
704 <a
704 <a
705 class="btn"
705 class="btn"
706 href="#"
706 href="#"
707 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
707 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
708 </div>
708 </div>
709 </div>
709 </div>
710 </div>
710 </div>
711 </%def>
711 </%def>
@@ -1,638 +1,643 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 <span id="pr-title">
11 <span id="pr-title">
12 ${c.pull_request.title}
12 ${c.pull_request.title}
13 %if c.pull_request.is_closed():
13 %if c.pull_request.is_closed():
14 (${_('Closed')})
14 (${_('Closed')})
15 %endif
15 %endif
16 </span>
16 </span>
17 <div id="pr-title-edit" class="input" style="display: none;">
17 <div id="pr-title-edit" class="input" style="display: none;">
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 </div>
19 </div>
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_nav()">
22 <%def name="menu_bar_nav()">
23 ${self.menu_items(active='repositories')}
23 ${self.menu_items(active='repositories')}
24 </%def>
24 </%def>
25
25
26 <%def name="menu_bar_subnav()">
26 <%def name="menu_bar_subnav()">
27 ${self.repo_menu(active='showpullrequest')}
27 ${self.repo_menu(active='showpullrequest')}
28 </%def>
28 </%def>
29
29
30 <%def name="main()">
30 <%def name="main()">
31
31
32 <script type="text/javascript">
32 <script type="text/javascript">
33 // TODO: marcink switch this to pyroutes
33 // TODO: marcink switch this to pyroutes
34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 </script>
36 </script>
37 <div class="box">
37 <div class="box">
38 <div class="title">
38 <div class="title">
39 ${self.repo_page_title(c.rhodecode_db_repo)}
39 ${self.repo_page_title(c.rhodecode_db_repo)}
40 </div>
40 </div>
41
41
42 ${self.breadcrumbs()}
42 ${self.breadcrumbs()}
43
43
44 <div class="box pr-summary">
44 <div class="box pr-summary">
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 % if c.allowed_to_delete:
51 % if c.allowed_to_delete:
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 ${h.end_form()}
55 ${h.end_form()}
56 % else:
56 % else:
57 ${_('Delete')}
57 ${_('Delete')}
58 % endif
58 % endif
59 </div>
59 </div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 %endif
62 %endif
63 </div>
63 </div>
64
64
65 <div id="summary" class="fields pr-details-content">
65 <div id="summary" class="fields pr-details-content">
66 <div class="field">
66 <div class="field">
67 <div class="label-summary">
67 <div class="label-summary">
68 <label>${_('Origin')}:</label>
68 <label>${_('Origin')}:</label>
69 </div>
69 </div>
70 <div class="input">
70 <div class="input">
71 <div class="pr-origininfo">
71 <div class="pr-origininfo">
72 ## branch link is only valid if it is a branch
72 ## branch link is only valid if it is a branch
73 <span class="tag">
73 <span class="tag">
74 %if c.pull_request.source_ref_parts.type == 'branch':
74 %if c.pull_request.source_ref_parts.type == 'branch':
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 %else:
76 %else:
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 %endif
78 %endif
79 </span>
79 </span>
80 <span class="clone-url">
80 <span class="clone-url">
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 </span>
82 </span>
83 </div>
83 </div>
84 <div class="pr-pullinfo">
84 <div class="pr-pullinfo">
85 %if h.is_hg(c.pull_request.source_repo):
85 %if h.is_hg(c.pull_request.source_repo):
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
87 %elif h.is_git(c.pull_request.source_repo):
87 %elif h.is_git(c.pull_request.source_repo):
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
89 %endif
89 %endif
90 </div>
90 </div>
91 </div>
91 </div>
92 </div>
92 </div>
93 <div class="field">
93 <div class="field">
94 <div class="label-summary">
94 <div class="label-summary">
95 <label>${_('Target')}:</label>
95 <label>${_('Target')}:</label>
96 </div>
96 </div>
97 <div class="input">
97 <div class="input">
98 <div class="pr-targetinfo">
98 <div class="pr-targetinfo">
99 ## branch link is only valid if it is a branch
99 ## branch link is only valid if it is a branch
100 <span class="tag">
100 <span class="tag">
101 %if c.pull_request.target_ref_parts.type == 'branch':
101 %if c.pull_request.target_ref_parts.type == 'branch':
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
103 %else:
103 %else:
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
105 %endif
105 %endif
106 </span>
106 </span>
107 <span class="clone-url">
107 <span class="clone-url">
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
109 </span>
109 </span>
110 </div>
110 </div>
111 </div>
111 </div>
112 </div>
112 </div>
113
113
114 ## Link to the shadow repository.
114 ## Link to the shadow repository.
115 <div class="field">
115 <div class="field">
116 <div class="label-summary">
116 <div class="label-summary">
117 <label>${_('Merge')}:</label>
117 <label>${_('Merge')}:</label>
118 </div>
118 </div>
119 <div class="input">
119 <div class="input">
120 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
120 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
121 <div class="pr-mergeinfo">
121 <div class="pr-mergeinfo">
122 %if h.is_hg(c.pull_request.target_repo):
122 %if h.is_hg(c.pull_request.target_repo):
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
124 %elif h.is_git(c.pull_request.target_repo):
124 %elif h.is_git(c.pull_request.target_repo):
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 %endif
126 %endif
127 </div>
127 </div>
128 % else:
128 % else:
129 <div class="">
129 <div class="">
130 ${_('Shadow repository data not available')}.
130 ${_('Shadow repository data not available')}.
131 </div>
131 </div>
132 % endif
132 % endif
133 </div>
133 </div>
134 </div>
134 </div>
135
135
136 <div class="field">
136 <div class="field">
137 <div class="label-summary">
137 <div class="label-summary">
138 <label>${_('Review')}:</label>
138 <label>${_('Review')}:</label>
139 </div>
139 </div>
140 <div class="input">
140 <div class="input">
141 %if c.pull_request_review_status:
141 %if c.pull_request_review_status:
142 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
142 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
143 <span class="changeset-status-lbl tooltip">
143 <span class="changeset-status-lbl tooltip">
144 %if c.pull_request.is_closed():
144 %if c.pull_request.is_closed():
145 ${_('Closed')},
145 ${_('Closed')},
146 %endif
146 %endif
147 ${h.commit_status_lbl(c.pull_request_review_status)}
147 ${h.commit_status_lbl(c.pull_request_review_status)}
148 </span>
148 </span>
149 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
149 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
150 %endif
150 %endif
151 </div>
151 </div>
152 </div>
152 </div>
153 <div class="field">
153 <div class="field">
154 <div class="pr-description-label label-summary">
154 <div class="pr-description-label label-summary">
155 <label>${_('Description')}:</label>
155 <label>${_('Description')}:</label>
156 </div>
156 </div>
157 <div id="pr-desc" class="input">
157 <div id="pr-desc" class="input">
158 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
158 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
159 </div>
159 </div>
160 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
160 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
161 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
161 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
162 </div>
162 </div>
163 </div>
163 </div>
164
164
165 <div class="field">
165 <div class="field">
166 <div class="label-summary">
166 <div class="label-summary">
167 <label>${_('Versions')} (${len(c.versions)+1}):</label>
167 <label>${_('Versions')} (${len(c.versions)+1}):</label>
168 </div>
168 </div>
169
169
170 <div class="pr-versions">
170 <div class="pr-versions">
171 % if c.show_version_changes:
171 % if c.show_version_changes:
172 <table>
172 <table>
173 ## CURRENTLY SELECT PR VERSION
173 ## CURRENTLY SELECT PR VERSION
174 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
174 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
175 <td>
175 <td>
176 % if c.at_version in [None, 'latest']:
176 % if c.at_version in [None, 'latest']:
177 <i class="icon-ok link"></i>
177 <i class="icon-ok link"></i>
178 % else:
178 % else:
179 <i class="icon-comment"></i> <code>${len(c.inline_versions[None])}</code>
179 <i class="icon-comment"></i> <code>${len(c.inline_versions[None])}</code>
180 % endif
180 % endif
181 </td>
181 </td>
182 <td>
182 <td>
183 <code>
183 <code>
184 % if c.versions:
184 % if c.versions:
185 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
185 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
186 % else:
186 % else:
187 ${_('initial')}
187 ${_('initial')}
188 % endif
188 % endif
189 </code>
189 </code>
190 </td>
190 </td>
191 <td>
191 <td>
192 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
192 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
193 </td>
193 </td>
194 <td>
194 <td>
195 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
195 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
196 </td>
196 </td>
197 <td align="right">
197 <td align="right">
198 % if c.versions and c.at_version_num in [None, 'latest']:
198 % if c.versions and c.at_version_num in [None, 'latest']:
199 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
199 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
200 % endif
200 % endif
201 </td>
201 </td>
202 </tr>
202 </tr>
203
203
204 ## SHOW ALL VERSIONS OF PR
204 ## SHOW ALL VERSIONS OF PR
205 <% ver_pr = None %>
205 <% ver_pr = None %>
206 % for ver in reversed(c.pull_request.versions()):
206 % for data in reversed(list(enumerate(c.versions, 1))):
207 <% ver_pos = data[0] %>
208 <% ver = data[1] %>
207 <% ver_pr = ver.pull_request_version_id %>
209 <% ver_pr = ver.pull_request_version_id %>
210
208 <tr class="version-pr" style="display: ${'' if c.at_version == ver_pr else 'none'}">
211 <tr class="version-pr" style="display: ${'' if c.at_version == ver_pr else 'none'}">
209 <td>
212 <td>
210 % if c.at_version == ver_pr:
213 % if c.at_version == ver_pr:
211 <i class="icon-ok link"></i>
214 <i class="icon-ok link"></i>
212 % else:
215 % else:
213 <i class="icon-comment"></i> <code>${len(c.inline_versions[ver_pr])}</code>
216 <i class="icon-comment"></i> <code>${len(c.inline_versions[ver_pr])}</code>
214 % endif
217 % endif
215 </td>
218 </td>
216 <td>
219 <td>
217 <code><a href="${h.url.current(version=ver_pr)}">version ${ver_pr}</a></code>
220 <code class="tooltip" title="${_('Comment from pull request version {0}').format(ver_pos)}">
221 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
222 </code>
218 </td>
223 </td>
219 <td>
224 <td>
220 <code>${ver.source_ref_parts.commit_id[:6]}</code>
225 <code>${ver.source_ref_parts.commit_id[:6]}</code>
221 </td>
226 </td>
222 <td>
227 <td>
223 ${_('created')} ${h.age_component(ver.updated_on)}
228 ${_('created')} ${h.age_component(ver.updated_on)}
224 </td>
229 </td>
225 <td align="right">
230 <td align="right">
226 % if c.at_version == ver_pr:
231 % if c.at_version == ver_pr:
227 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
232 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
228 % endif
233 % endif
229 </td>
234 </td>
230 </tr>
235 </tr>
231 % endfor
236 % endfor
232
237
233 ## show comment/inline comments summary
238 ## show comment/inline comments summary
234 <tr>
239 <tr>
235 <td>
240 <td>
236 </td>
241 </td>
237
242
238 <% inline_comm_count_ver = len(c.inline_versions[ver_pr])%>
243 <% inline_comm_count_ver = len(c.inline_versions[ver_pr])%>
239 <td colspan="4" style="border-top: 1px dashed #dbd9da">
244 <td colspan="4" style="border-top: 1px dashed #dbd9da">
240 ${_('Comments for this version')}:
245 ${_('Comments for this version')}:
241 %if c.comments:
246 %if c.comments:
242 <a href="#comments">${_("%d General ") % len(c.comments)}</a>
247 <a href="#comments">${_("%d General ") % len(c.comments)}</a>
243 %else:
248 %else:
244 ${_("%d General ") % len(c.comments)}
249 ${_("%d General ") % len(c.comments)}
245 %endif
250 %endif
246
251
247 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num])%>
252 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num])%>
248 %if inline_comm_count_ver:
253 %if inline_comm_count_ver:
249 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
254 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
250 %else:
255 %else:
251 , ${_("%d Inline") % inline_comm_count_ver}
256 , ${_("%d Inline") % inline_comm_count_ver}
252 %endif
257 %endif
253
258
254 %if c.outdated_cnt:
259 %if c.outdated_cnt:
255 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % c.outdated_cnt}</a>
260 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % c.outdated_cnt}</a>
256 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
261 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
257 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
262 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
258 %else:
263 %else:
259 , ${_("%d Outdated") % c.outdated_cnt}
264 , ${_("%d Outdated") % c.outdated_cnt}
260 %endif
265 %endif
261 </td>
266 </td>
262 </tr>
267 </tr>
263
268
264 <tr>
269 <tr>
265 <td></td>
270 <td></td>
266 <td colspan="4">
271 <td colspan="4">
267 % if c.at_version:
272 % if c.at_version:
268 <pre>
273 <pre>
269 Changed commits:
274 Changed commits:
270 * added: ${len(c.changes.added)}
275 * added: ${len(c.changes.added)}
271 * removed: ${len(c.changes.removed)}
276 * removed: ${len(c.changes.removed)}
272
277
273 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
278 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
274 No file changes found
279 No file changes found
275 % else:
280 % else:
276 Changed files:
281 Changed files:
277 %for file_name in c.file_changes.added:
282 %for file_name in c.file_changes.added:
278 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
283 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
279 %endfor
284 %endfor
280 %for file_name in c.file_changes.modified:
285 %for file_name in c.file_changes.modified:
281 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
286 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
282 %endfor
287 %endfor
283 %for file_name in c.file_changes.removed:
288 %for file_name in c.file_changes.removed:
284 * R ${file_name}
289 * R ${file_name}
285 %endfor
290 %endfor
286 % endif
291 % endif
287 </pre>
292 </pre>
288 % endif
293 % endif
289 </td>
294 </td>
290 </tr>
295 </tr>
291 </table>
296 </table>
292 % else:
297 % else:
293 ${_('Pull request versions not available')}.
298 ${_('Pull request versions not available')}.
294 % endif
299 % endif
295 </div>
300 </div>
296 </div>
301 </div>
297
302
298 <div id="pr-save" class="field" style="display: none;">
303 <div id="pr-save" class="field" style="display: none;">
299 <div class="label-summary"></div>
304 <div class="label-summary"></div>
300 <div class="input">
305 <div class="input">
301 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
306 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
302 </div>
307 </div>
303 </div>
308 </div>
304 </div>
309 </div>
305 </div>
310 </div>
306 <div>
311 <div>
307 ## AUTHOR
312 ## AUTHOR
308 <div class="reviewers-title block-right">
313 <div class="reviewers-title block-right">
309 <div class="pr-details-title">
314 <div class="pr-details-title">
310 ${_('Author')}
315 ${_('Author')}
311 </div>
316 </div>
312 </div>
317 </div>
313 <div class="block-right pr-details-content reviewers">
318 <div class="block-right pr-details-content reviewers">
314 <ul class="group_members">
319 <ul class="group_members">
315 <li>
320 <li>
316 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
317 </li>
322 </li>
318 </ul>
323 </ul>
319 </div>
324 </div>
320 ## REVIEWERS
325 ## REVIEWERS
321 <div class="reviewers-title block-right">
326 <div class="reviewers-title block-right">
322 <div class="pr-details-title">
327 <div class="pr-details-title">
323 ${_('Pull request reviewers')}
328 ${_('Pull request reviewers')}
324 %if c.allowed_to_update:
329 %if c.allowed_to_update:
325 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
330 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
326 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
331 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
327 %endif
332 %endif
328 </div>
333 </div>
329 </div>
334 </div>
330 <div id="reviewers" class="block-right pr-details-content reviewers">
335 <div id="reviewers" class="block-right pr-details-content reviewers">
331 ## members goes here !
336 ## members goes here !
332 <input type="hidden" name="__start__" value="review_members:sequence">
337 <input type="hidden" name="__start__" value="review_members:sequence">
333 <ul id="review_members" class="group_members">
338 <ul id="review_members" class="group_members">
334 %for member,reasons,status in c.pull_request_reviewers:
339 %for member,reasons,status in c.pull_request_reviewers:
335 <li id="reviewer_${member.user_id}">
340 <li id="reviewer_${member.user_id}">
336 <div class="reviewers_member">
341 <div class="reviewers_member">
337 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
342 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
338 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
343 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
339 </div>
344 </div>
340 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
345 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
341 ${self.gravatar_with_user(member.email, 16)}
346 ${self.gravatar_with_user(member.email, 16)}
342 </div>
347 </div>
343 <input type="hidden" name="__start__" value="reviewer:mapping">
348 <input type="hidden" name="__start__" value="reviewer:mapping">
344 <input type="hidden" name="__start__" value="reasons:sequence">
349 <input type="hidden" name="__start__" value="reasons:sequence">
345 %for reason in reasons:
350 %for reason in reasons:
346 <div class="reviewer_reason">- ${reason}</div>
351 <div class="reviewer_reason">- ${reason}</div>
347 <input type="hidden" name="reason" value="${reason}">
352 <input type="hidden" name="reason" value="${reason}">
348
353
349 %endfor
354 %endfor
350 <input type="hidden" name="__end__" value="reasons:sequence">
355 <input type="hidden" name="__end__" value="reasons:sequence">
351 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
356 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
352 <input type="hidden" name="__end__" value="reviewer:mapping">
357 <input type="hidden" name="__end__" value="reviewer:mapping">
353 %if c.allowed_to_update:
358 %if c.allowed_to_update:
354 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
359 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
355 <i class="icon-remove-sign" ></i>
360 <i class="icon-remove-sign" ></i>
356 </div>
361 </div>
357 %endif
362 %endif
358 </div>
363 </div>
359 </li>
364 </li>
360 %endfor
365 %endfor
361 </ul>
366 </ul>
362 <input type="hidden" name="__end__" value="review_members:sequence">
367 <input type="hidden" name="__end__" value="review_members:sequence">
363 %if not c.pull_request.is_closed():
368 %if not c.pull_request.is_closed():
364 <div id="add_reviewer_input" class='ac' style="display: none;">
369 <div id="add_reviewer_input" class='ac' style="display: none;">
365 %if c.allowed_to_update:
370 %if c.allowed_to_update:
366 <div class="reviewer_ac">
371 <div class="reviewer_ac">
367 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
372 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
368 <div id="reviewers_container"></div>
373 <div id="reviewers_container"></div>
369 </div>
374 </div>
370 <div>
375 <div>
371 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
376 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
372 </div>
377 </div>
373 %endif
378 %endif
374 </div>
379 </div>
375 %endif
380 %endif
376 </div>
381 </div>
377 </div>
382 </div>
378 </div>
383 </div>
379 <div class="box">
384 <div class="box">
380 ##DIFF
385 ##DIFF
381 <div class="table" >
386 <div class="table" >
382 <div id="changeset_compare_view_content">
387 <div id="changeset_compare_view_content">
383 ##CS
388 ##CS
384 % if c.missing_requirements:
389 % if c.missing_requirements:
385 <div class="box">
390 <div class="box">
386 <div class="alert alert-warning">
391 <div class="alert alert-warning">
387 <div>
392 <div>
388 <strong>${_('Missing requirements:')}</strong>
393 <strong>${_('Missing requirements:')}</strong>
389 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
394 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
390 </div>
395 </div>
391 </div>
396 </div>
392 </div>
397 </div>
393 % elif c.missing_commits:
398 % elif c.missing_commits:
394 <div class="box">
399 <div class="box">
395 <div class="alert alert-warning">
400 <div class="alert alert-warning">
396 <div>
401 <div>
397 <strong>${_('Missing commits')}:</strong>
402 <strong>${_('Missing commits')}:</strong>
398 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
403 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
399 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
404 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
400 </div>
405 </div>
401 </div>
406 </div>
402 </div>
407 </div>
403 % endif
408 % endif
404 <div class="compare_view_commits_title">
409 <div class="compare_view_commits_title">
405
410
406 <div class="pull-left">
411 <div class="pull-left">
407 <div class="btn-group">
412 <div class="btn-group">
408 <a
413 <a
409 class="btn"
414 class="btn"
410 href="#"
415 href="#"
411 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
416 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
412 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
417 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
413 </a>
418 </a>
414 <a
419 <a
415 class="btn"
420 class="btn"
416 href="#"
421 href="#"
417 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
422 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
418 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
423 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
419 </a>
424 </a>
420 </div>
425 </div>
421 </div>
426 </div>
422
427
423 <div class="pull-right">
428 <div class="pull-right">
424 % if c.allowed_to_update and not c.pull_request.is_closed():
429 % if c.allowed_to_update and not c.pull_request.is_closed():
425 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
430 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
426 % else:
431 % else:
427 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
432 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
428 % endif
433 % endif
429
434
430 </div>
435 </div>
431
436
432 </div>
437 </div>
433 % if not c.missing_commits:
438 % if not c.missing_commits:
434 <%include file="/compare/compare_commits.mako" />
439 <%include file="/compare/compare_commits.mako" />
435 <div class="cs_files">
440 <div class="cs_files">
436 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
441 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
437 ${cbdiffs.render_diffset_menu()}
442 ${cbdiffs.render_diffset_menu()}
438 ${cbdiffs.render_diffset(
443 ${cbdiffs.render_diffset(
439 c.diffset, use_comments=True,
444 c.diffset, use_comments=True,
440 collapse_when_files_over=30,
445 collapse_when_files_over=30,
441 disable_new_comments=not c.allowed_to_comment,
446 disable_new_comments=not c.allowed_to_comment,
442 deleted_files_comments=c.deleted_files_comments)}
447 deleted_files_comments=c.deleted_files_comments)}
443
448
444 </div>
449 </div>
445 % endif
450 % endif
446 </div>
451 </div>
447 </div>
452 </div>
448
453
449 ## template for inline comment form
454 ## template for inline comment form
450 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
455 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
451
456
452 ## render general comments
457 ## render general comments
453 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
458 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
454
459
455 % if not c.pull_request.is_closed():
460 % if not c.pull_request.is_closed():
456 ## main comment form and it status
461 ## main comment form and it status
457 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
462 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
458 pull_request_id=c.pull_request.pull_request_id),
463 pull_request_id=c.pull_request.pull_request_id),
459 c.pull_request_review_status,
464 c.pull_request_review_status,
460 is_pull_request=True, change_status=c.allowed_to_change_status)}
465 is_pull_request=True, change_status=c.allowed_to_change_status)}
461 %endif
466 %endif
462
467
463 <script type="text/javascript">
468 <script type="text/javascript">
464 if (location.hash) {
469 if (location.hash) {
465 var result = splitDelimitedHash(location.hash);
470 var result = splitDelimitedHash(location.hash);
466 var line = $('html').find(result.loc);
471 var line = $('html').find(result.loc);
467 if (line.length > 0){
472 if (line.length > 0){
468 offsetScroll(line, 70);
473 offsetScroll(line, 70);
469 }
474 }
470 }
475 }
471 $(function(){
476 $(function(){
472 ReviewerAutoComplete('user');
477 ReviewerAutoComplete('user');
473 // custom code mirror
478 // custom code mirror
474 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
479 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
475
480
476 var PRDetails = {
481 var PRDetails = {
477 editButton: $('#open_edit_pullrequest'),
482 editButton: $('#open_edit_pullrequest'),
478 closeButton: $('#close_edit_pullrequest'),
483 closeButton: $('#close_edit_pullrequest'),
479 deleteButton: $('#delete_pullrequest'),
484 deleteButton: $('#delete_pullrequest'),
480 viewFields: $('#pr-desc, #pr-title'),
485 viewFields: $('#pr-desc, #pr-title'),
481 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
486 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
482
487
483 init: function() {
488 init: function() {
484 var that = this;
489 var that = this;
485 this.editButton.on('click', function(e) { that.edit(); });
490 this.editButton.on('click', function(e) { that.edit(); });
486 this.closeButton.on('click', function(e) { that.view(); });
491 this.closeButton.on('click', function(e) { that.view(); });
487 },
492 },
488
493
489 edit: function(event) {
494 edit: function(event) {
490 this.viewFields.hide();
495 this.viewFields.hide();
491 this.editButton.hide();
496 this.editButton.hide();
492 this.deleteButton.hide();
497 this.deleteButton.hide();
493 this.closeButton.show();
498 this.closeButton.show();
494 this.editFields.show();
499 this.editFields.show();
495 codeMirrorInstance.refresh();
500 codeMirrorInstance.refresh();
496 },
501 },
497
502
498 view: function(event) {
503 view: function(event) {
499 this.editButton.show();
504 this.editButton.show();
500 this.deleteButton.show();
505 this.deleteButton.show();
501 this.editFields.hide();
506 this.editFields.hide();
502 this.closeButton.hide();
507 this.closeButton.hide();
503 this.viewFields.show();
508 this.viewFields.show();
504 }
509 }
505 };
510 };
506
511
507 var ReviewersPanel = {
512 var ReviewersPanel = {
508 editButton: $('#open_edit_reviewers'),
513 editButton: $('#open_edit_reviewers'),
509 closeButton: $('#close_edit_reviewers'),
514 closeButton: $('#close_edit_reviewers'),
510 addButton: $('#add_reviewer_input'),
515 addButton: $('#add_reviewer_input'),
511 removeButtons: $('.reviewer_member_remove'),
516 removeButtons: $('.reviewer_member_remove'),
512
517
513 init: function() {
518 init: function() {
514 var that = this;
519 var that = this;
515 this.editButton.on('click', function(e) { that.edit(); });
520 this.editButton.on('click', function(e) { that.edit(); });
516 this.closeButton.on('click', function(e) { that.close(); });
521 this.closeButton.on('click', function(e) { that.close(); });
517 },
522 },
518
523
519 edit: function(event) {
524 edit: function(event) {
520 this.editButton.hide();
525 this.editButton.hide();
521 this.closeButton.show();
526 this.closeButton.show();
522 this.addButton.show();
527 this.addButton.show();
523 this.removeButtons.css('visibility', 'visible');
528 this.removeButtons.css('visibility', 'visible');
524 },
529 },
525
530
526 close: function(event) {
531 close: function(event) {
527 this.editButton.show();
532 this.editButton.show();
528 this.closeButton.hide();
533 this.closeButton.hide();
529 this.addButton.hide();
534 this.addButton.hide();
530 this.removeButtons.css('visibility', 'hidden');
535 this.removeButtons.css('visibility', 'hidden');
531 }
536 }
532 };
537 };
533
538
534 PRDetails.init();
539 PRDetails.init();
535 ReviewersPanel.init();
540 ReviewersPanel.init();
536
541
537 showOutdated = function(self){
542 showOutdated = function(self){
538 $('.comment-outdated').show();
543 $('.comment-outdated').show();
539 $('.filediff-outdated').show();
544 $('.filediff-outdated').show();
540 $('.showOutdatedComments').hide();
545 $('.showOutdatedComments').hide();
541 $('.hideOutdatedComments').show();
546 $('.hideOutdatedComments').show();
542
547
543 };
548 };
544
549
545 hideOutdated = function(self){
550 hideOutdated = function(self){
546 $('.comment-outdated').hide();
551 $('.comment-outdated').hide();
547 $('.filediff-outdated').hide();
552 $('.filediff-outdated').hide();
548 $('.hideOutdatedComments').hide();
553 $('.hideOutdatedComments').hide();
549 $('.showOutdatedComments').show();
554 $('.showOutdatedComments').show();
550 };
555 };
551
556
552 $('#show-outdated-comments').on('click', function(e){
557 $('#show-outdated-comments').on('click', function(e){
553 var button = $(this);
558 var button = $(this);
554 var outdated = $('.comment-outdated');
559 var outdated = $('.comment-outdated');
555
560
556 if (button.html() === "(Show)") {
561 if (button.html() === "(Show)") {
557 button.html("(Hide)");
562 button.html("(Hide)");
558 outdated.show();
563 outdated.show();
559 } else {
564 } else {
560 button.html("(Show)");
565 button.html("(Show)");
561 outdated.hide();
566 outdated.hide();
562 }
567 }
563 });
568 });
564
569
565 $('.show-inline-comments').on('change', function(e){
570 $('.show-inline-comments').on('change', function(e){
566 var show = 'none';
571 var show = 'none';
567 var target = e.currentTarget;
572 var target = e.currentTarget;
568 if(target.checked){
573 if(target.checked){
569 show = ''
574 show = ''
570 }
575 }
571 var boxid = $(target).attr('id_for');
576 var boxid = $(target).attr('id_for');
572 var comments = $('#{0} .inline-comments'.format(boxid));
577 var comments = $('#{0} .inline-comments'.format(boxid));
573 var fn_display = function(idx){
578 var fn_display = function(idx){
574 $(this).css('display', show);
579 $(this).css('display', show);
575 };
580 };
576 $(comments).each(fn_display);
581 $(comments).each(fn_display);
577 var btns = $('#{0} .inline-comments-button'.format(boxid));
582 var btns = $('#{0} .inline-comments-button'.format(boxid));
578 $(btns).each(fn_display);
583 $(btns).each(fn_display);
579 });
584 });
580
585
581 $('#merge_pull_request_form').submit(function() {
586 $('#merge_pull_request_form').submit(function() {
582 if (!$('#merge_pull_request').attr('disabled')) {
587 if (!$('#merge_pull_request').attr('disabled')) {
583 $('#merge_pull_request').attr('disabled', 'disabled');
588 $('#merge_pull_request').attr('disabled', 'disabled');
584 }
589 }
585 return true;
590 return true;
586 });
591 });
587
592
588 $('#edit_pull_request').on('click', function(e){
593 $('#edit_pull_request').on('click', function(e){
589 var title = $('#pr-title-input').val();
594 var title = $('#pr-title-input').val();
590 var description = codeMirrorInstance.getValue();
595 var description = codeMirrorInstance.getValue();
591 editPullRequest(
596 editPullRequest(
592 "${c.repo_name}", "${c.pull_request.pull_request_id}",
597 "${c.repo_name}", "${c.pull_request.pull_request_id}",
593 title, description);
598 title, description);
594 });
599 });
595
600
596 $('#update_pull_request').on('click', function(e){
601 $('#update_pull_request').on('click', function(e){
597 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
602 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
598 });
603 });
599
604
600 $('#update_commits').on('click', function(e){
605 $('#update_commits').on('click', function(e){
601 var isDisabled = !$(e.currentTarget).attr('disabled');
606 var isDisabled = !$(e.currentTarget).attr('disabled');
602 $(e.currentTarget).text(_gettext('Updating...'));
607 $(e.currentTarget).text(_gettext('Updating...'));
603 $(e.currentTarget).attr('disabled', 'disabled');
608 $(e.currentTarget).attr('disabled', 'disabled');
604 if(isDisabled){
609 if(isDisabled){
605 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
610 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
606 }
611 }
607
612
608 });
613 });
609 // fixing issue with caches on firefox
614 // fixing issue with caches on firefox
610 $('#update_commits').removeAttr("disabled");
615 $('#update_commits').removeAttr("disabled");
611
616
612 $('#close_pull_request').on('click', function(e){
617 $('#close_pull_request').on('click', function(e){
613 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
618 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
614 });
619 });
615
620
616 $('.show-inline-comments').on('click', function(e){
621 $('.show-inline-comments').on('click', function(e){
617 var boxid = $(this).attr('data-comment-id');
622 var boxid = $(this).attr('data-comment-id');
618 var button = $(this);
623 var button = $(this);
619
624
620 if(button.hasClass("comments-visible")) {
625 if(button.hasClass("comments-visible")) {
621 $('#{0} .inline-comments'.format(boxid)).each(function(index){
626 $('#{0} .inline-comments'.format(boxid)).each(function(index){
622 $(this).hide();
627 $(this).hide();
623 });
628 });
624 button.removeClass("comments-visible");
629 button.removeClass("comments-visible");
625 } else {
630 } else {
626 $('#{0} .inline-comments'.format(boxid)).each(function(index){
631 $('#{0} .inline-comments'.format(boxid)).each(function(index){
627 $(this).show();
632 $(this).show();
628 });
633 });
629 button.addClass("comments-visible");
634 button.addClass("comments-visible");
630 }
635 }
631 });
636 });
632 })
637 })
633 </script>
638 </script>
634
639
635 </div>
640 </div>
636 </div>
641 </div>
637
642
638 </%def>
643 </%def>
General Comments 0
You need to be logged in to leave comments. Login now