##// END OF EJS Templates
comments: enabled resolution for general comments, and finalized how general comment is build
marcink -
r1326:e70e0f00 default
parent child Browse files
Show More
@@ -1,473 +1,475 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 CommentsModel
49 from rhodecode.model.comment import CommentsModel
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 = CommentsModel().get_comments(
213 c.comments = CommentsModel().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 = CommentsModel().get_inline_comments(
258 inline_comments = CommentsModel().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 = CommentsModel().get_inline_comments_count(
260 c.inline_cnt = CommentsModel().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 comment_type = request.POST.get('comment_type')
337 comment_type = request.POST.get('comment_type')
338 resolves_comment_id = request.POST.get('resolves_comment_id', None)
338
339
339 if status:
340 if status:
340 text = text or (_('Status change %(transition_icon)s %(status)s')
341 text = text or (_('Status change %(transition_icon)s %(status)s')
341 % {'transition_icon': '>',
342 % {'transition_icon': '>',
342 'status': ChangesetStatus.get_status_lbl(status)})
343 'status': ChangesetStatus.get_status_lbl(status)})
343
344
344 multi_commit_ids = filter(
345 multi_commit_ids = filter(
345 lambda s: s not in ['', None],
346 lambda s: s not in ['', None],
346 request.POST.get('commit_ids', '').split(','),)
347 request.POST.get('commit_ids', '').split(','),)
347
348
348 commit_ids = multi_commit_ids or [commit_id]
349 commit_ids = multi_commit_ids or [commit_id]
349 comment = None
350 comment = None
350 for current_id in filter(None, commit_ids):
351 for current_id in filter(None, commit_ids):
351 c.co = comment = CommentsModel().create(
352 c.co = comment = CommentsModel().create(
352 text=text,
353 text=text,
353 repo=c.rhodecode_db_repo.repo_id,
354 repo=c.rhodecode_db_repo.repo_id,
354 user=c.rhodecode_user.user_id,
355 user=c.rhodecode_user.user_id,
355 commit_id=current_id,
356 commit_id=current_id,
356 f_path=request.POST.get('f_path'),
357 f_path=request.POST.get('f_path'),
357 line_no=request.POST.get('line'),
358 line_no=request.POST.get('line'),
358 status_change=(ChangesetStatus.get_status_lbl(status)
359 status_change=(ChangesetStatus.get_status_lbl(status)
359 if status else None),
360 if status else None),
360 status_change_type=status,
361 status_change_type=status,
361 comment_type=comment_type
362 comment_type=comment_type,
363 resolves_comment_id=resolves_comment_id
362 )
364 )
363 c.inline_comment = True if comment.line_no else False
365 c.inline_comment = True if comment.line_no else False
364
366
365 # get status if set !
367 # get status if set !
366 if status:
368 if status:
367 # if latest status was from pull request and it's closed
369 # if latest status was from pull request and it's closed
368 # disallow changing status !
370 # disallow changing status !
369 # dont_allow_on_closed_pull_request = True !
371 # dont_allow_on_closed_pull_request = True !
370
372
371 try:
373 try:
372 ChangesetStatusModel().set_status(
374 ChangesetStatusModel().set_status(
373 c.rhodecode_db_repo.repo_id,
375 c.rhodecode_db_repo.repo_id,
374 status,
376 status,
375 c.rhodecode_user.user_id,
377 c.rhodecode_user.user_id,
376 comment,
378 comment,
377 revision=current_id,
379 revision=current_id,
378 dont_allow_on_closed_pull_request=True
380 dont_allow_on_closed_pull_request=True
379 )
381 )
380 except StatusChangeOnClosedPullRequestError:
382 except StatusChangeOnClosedPullRequestError:
381 msg = _('Changing the status of a commit associated with '
383 msg = _('Changing the status of a commit associated with '
382 'a closed pull request is not allowed')
384 'a closed pull request is not allowed')
383 log.exception(msg)
385 log.exception(msg)
384 h.flash(msg, category='warning')
386 h.flash(msg, category='warning')
385 return redirect(h.url(
387 return redirect(h.url(
386 'changeset_home', repo_name=repo_name,
388 'changeset_home', repo_name=repo_name,
387 revision=current_id))
389 revision=current_id))
388
390
389 # finalize, commit and redirect
391 # finalize, commit and redirect
390 Session().commit()
392 Session().commit()
391
393
392 data = {
394 data = {
393 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
395 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
394 }
396 }
395 if comment:
397 if comment:
396 data.update(comment.get_dict())
398 data.update(comment.get_dict())
397 data.update({'rendered_text':
399 data.update({'rendered_text':
398 render('changeset/changeset_comment_block.mako')})
400 render('changeset/changeset_comment_block.mako')})
399
401
400 return data
402 return data
401
403
402 @LoginRequired()
404 @LoginRequired()
403 @NotAnonymous()
405 @NotAnonymous()
404 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
406 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
405 'repository.admin')
407 'repository.admin')
406 @auth.CSRFRequired()
408 @auth.CSRFRequired()
407 def preview_comment(self):
409 def preview_comment(self):
408 # Technically a CSRF token is not needed as no state changes with this
410 # Technically a CSRF token is not needed as no state changes with this
409 # call. However, as this is a POST is better to have it, so automated
411 # call. However, as this is a POST is better to have it, so automated
410 # tools don't flag it as potential CSRF.
412 # tools don't flag it as potential CSRF.
411 # Post is required because the payload could be bigger than the maximum
413 # Post is required because the payload could be bigger than the maximum
412 # allowed by GET.
414 # allowed by GET.
413 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
415 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
414 raise HTTPBadRequest()
416 raise HTTPBadRequest()
415 text = request.POST.get('text')
417 text = request.POST.get('text')
416 renderer = request.POST.get('renderer') or 'rst'
418 renderer = request.POST.get('renderer') or 'rst'
417 if text:
419 if text:
418 return h.render(text, renderer=renderer, mentions=True)
420 return h.render(text, renderer=renderer, mentions=True)
419 return ''
421 return ''
420
422
421 @LoginRequired()
423 @LoginRequired()
422 @NotAnonymous()
424 @NotAnonymous()
423 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
425 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
424 'repository.admin')
426 'repository.admin')
425 @auth.CSRFRequired()
427 @auth.CSRFRequired()
426 @jsonify
428 @jsonify
427 def delete_comment(self, repo_name, comment_id):
429 def delete_comment(self, repo_name, comment_id):
428 comment = ChangesetComment.get(comment_id)
430 comment = ChangesetComment.get(comment_id)
429 owner = (comment.author.user_id == c.rhodecode_user.user_id)
431 owner = (comment.author.user_id == c.rhodecode_user.user_id)
430 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
432 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
431 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
433 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
432 CommentsModel().delete(comment=comment)
434 CommentsModel().delete(comment=comment)
433 Session().commit()
435 Session().commit()
434 return True
436 return True
435 else:
437 else:
436 raise HTTPForbidden()
438 raise HTTPForbidden()
437
439
438 @LoginRequired()
440 @LoginRequired()
439 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
441 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
440 'repository.admin')
442 'repository.admin')
441 @jsonify
443 @jsonify
442 def changeset_info(self, repo_name, revision):
444 def changeset_info(self, repo_name, revision):
443 if request.is_xhr:
445 if request.is_xhr:
444 try:
446 try:
445 return c.rhodecode_repo.get_commit(commit_id=revision)
447 return c.rhodecode_repo.get_commit(commit_id=revision)
446 except CommitDoesNotExistError as e:
448 except CommitDoesNotExistError as e:
447 return EmptyCommit(message=str(e))
449 return EmptyCommit(message=str(e))
448 else:
450 else:
449 raise HTTPBadRequest()
451 raise HTTPBadRequest()
450
452
451 @LoginRequired()
453 @LoginRequired()
452 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
454 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
453 'repository.admin')
455 'repository.admin')
454 @jsonify
456 @jsonify
455 def changeset_children(self, repo_name, revision):
457 def changeset_children(self, repo_name, revision):
456 if request.is_xhr:
458 if request.is_xhr:
457 commit = c.rhodecode_repo.get_commit(commit_id=revision)
459 commit = c.rhodecode_repo.get_commit(commit_id=revision)
458 result = {"results": commit.children}
460 result = {"results": commit.children}
459 return result
461 return result
460 else:
462 else:
461 raise HTTPBadRequest()
463 raise HTTPBadRequest()
462
464
463 @LoginRequired()
465 @LoginRequired()
464 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
465 'repository.admin')
467 'repository.admin')
466 @jsonify
468 @jsonify
467 def changeset_parents(self, repo_name, revision):
469 def changeset_parents(self, repo_name, revision):
468 if request.is_xhr:
470 if request.is_xhr:
469 commit = c.rhodecode_repo.get_commit(commit_id=revision)
471 commit = c.rhodecode_repo.get_commit(commit_id=revision)
470 result = {"results": commit.parents}
472 result = {"results": commit.parents}
471 return result
473 return result
472 else:
474 else:
473 raise HTTPBadRequest()
475 raise HTTPBadRequest()
@@ -1,1029 +1,1029 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 CommentsModel
58 from rhodecode.model.comment import CommentsModel
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 = CommentsModel().get_all_comments(
261 comments = CommentsModel().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 = CommentsModel()
799 cc_model = CommentsModel()
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.versions = pull_request_display_obj.versions()
818 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(
819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 c.at_version_num, c.versions)
820 c.at_version_num, c.versions)
821
821
822 is_outdated = lambda co: \
822 is_outdated = lambda co: \
823 not c.at_version_num \
823 not c.at_version_num \
824 or co.pull_request_version_id <= c.at_version_num
824 or co.pull_request_version_id <= c.at_version_num
825
825
826 # inline_comments_until_version
826 # inline_comments_until_version
827 if c.at_version_num:
827 if c.at_version_num:
828 # if we use version, then do not show later comments
828 # if we use version, then do not show later comments
829 # than current version
829 # than current version
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
831 for fname, per_line_comments in inline_comments.iteritems():
831 for fname, per_line_comments in inline_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
833 for co in comments:
833 for co in comments:
834 if co.pull_request_version_id and is_outdated(co):
834 if co.pull_request_version_id and is_outdated(co):
835 paths[co.f_path][co.line_no].append(co)
835 paths[co.f_path][co.line_no].append(co)
836 inline_comments = paths
836 inline_comments = paths
837
837
838 # outdated comments
838 # outdated comments
839 c.outdated_cnt = 0
839 c.outdated_cnt = 0
840 if CommentsModel.use_outdated_comments(pull_request_latest):
840 if CommentsModel.use_outdated_comments(pull_request_latest):
841 outdated_comments = cc_model.get_outdated_comments(
841 outdated_comments = cc_model.get_outdated_comments(
842 c.rhodecode_db_repo.repo_id,
842 c.rhodecode_db_repo.repo_id,
843 pull_request=pull_request_at_ver)
843 pull_request=pull_request_at_ver)
844
844
845 # Count outdated comments and check for deleted files
845 # Count outdated comments and check for deleted files
846 is_outdated = lambda co: \
846 is_outdated = lambda co: \
847 not c.at_version_num \
847 not c.at_version_num \
848 or co.pull_request_version_id < c.at_version_num
848 or co.pull_request_version_id < c.at_version_num
849 for file_name, lines in outdated_comments.iteritems():
849 for file_name, lines in outdated_comments.iteritems():
850 for comments in lines.values():
850 for comments in lines.values():
851 comments = [comm for comm in comments if is_outdated(comm)]
851 comments = [comm for comm in comments if is_outdated(comm)]
852 c.outdated_cnt += len(comments)
852 c.outdated_cnt += len(comments)
853
853
854 # load compare data into template context
854 # load compare data into template context
855 self._load_compare_data(pull_request_at_ver, inline_comments)
855 self._load_compare_data(pull_request_at_ver, inline_comments)
856
856
857 # 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
858 # compare view and others uses different notation, and
858 # compare view and others uses different notation, and
859 # compare_commits.mako renders links based on the target_repo.
859 # compare_commits.mako renders links based on the target_repo.
860 # 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
861 c.target_repo = c.source_repo
861 c.target_repo = c.source_repo
862
862
863 # general comments
863 # general comments
864 c.comments = cc_model.get_comments(
864 c.comments = cc_model.get_comments(
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866
866
867 if c.allowed_to_update:
867 if c.allowed_to_update:
868 force_close = ('forced_closed', _('Close Pull Request'))
868 force_close = ('forced_closed', _('Close Pull Request'))
869 statuses = ChangesetStatus.STATUSES + [force_close]
869 statuses = ChangesetStatus.STATUSES + [force_close]
870 else:
870 else:
871 statuses = ChangesetStatus.STATUSES
871 statuses = ChangesetStatus.STATUSES
872 c.commit_statuses = statuses
872 c.commit_statuses = statuses
873
873
874 c.ancestor = None # TODO: add ancestor here
874 c.ancestor = None # TODO: add ancestor here
875 c.pull_request = pull_request_display_obj
875 c.pull_request = pull_request_display_obj
876 c.pull_request_latest = pull_request_latest
876 c.pull_request_latest = pull_request_latest
877 c.at_version = at_version
877 c.at_version = at_version
878
878
879 c.changes = None
879 c.changes = None
880 c.file_changes = None
880 c.file_changes = None
881
881
882 c.show_version_changes = 1 # control flag, not used yet
882 c.show_version_changes = 1 # control flag, not used yet
883
883
884 if at_version and c.show_version_changes:
884 if at_version and c.show_version_changes:
885 c.changes, c.file_changes = self._get_pr_version_changes(
885 c.changes, c.file_changes = self._get_pr_version_changes(
886 version, pull_request_latest)
886 version, pull_request_latest)
887
887
888 return render('/pullrequests/pullrequest_show.mako')
888 return render('/pullrequests/pullrequest_show.mako')
889
889
890 @LoginRequired()
890 @LoginRequired()
891 @NotAnonymous()
891 @NotAnonymous()
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 'repository.admin')
893 'repository.admin')
894 @auth.CSRFRequired()
894 @auth.CSRFRequired()
895 @jsonify
895 @jsonify
896 def comment(self, repo_name, pull_request_id):
896 def comment(self, repo_name, pull_request_id):
897 pull_request_id = safe_int(pull_request_id)
897 pull_request_id = safe_int(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
899 if pull_request.is_closed():
899 if pull_request.is_closed():
900 raise HTTPForbidden()
900 raise HTTPForbidden()
901
901
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 # 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.
904 status = request.POST.get('changeset_status', None)
904 status = request.POST.get('changeset_status', None)
905 text = request.POST.get('text')
905 text = request.POST.get('text')
906 comment_type = request.POST.get('comment_type')
906 comment_type = request.POST.get('comment_type')
907 resolves_comment_id = request.POST.get('resolves_comment_id')
907 resolves_comment_id = request.POST.get('resolves_comment_id', None)
908
908
909 if status and '_closed' in status:
909 if status and '_closed' in status:
910 close_pr = True
910 close_pr = True
911 status = status.replace('_closed', '')
911 status = status.replace('_closed', '')
912 else:
912 else:
913 close_pr = False
913 close_pr = False
914
914
915 forced = (status == 'forced')
915 forced = (status == 'forced')
916 if forced:
916 if forced:
917 status = 'rejected'
917 status = 'rejected'
918
918
919 allowed_to_change_status = PullRequestModel().check_user_change_status(
919 allowed_to_change_status = PullRequestModel().check_user_change_status(
920 pull_request, c.rhodecode_user)
920 pull_request, c.rhodecode_user)
921
921
922 if status and allowed_to_change_status:
922 if status and allowed_to_change_status:
923 message = (_('Status change %(transition_icon)s %(status)s')
923 message = (_('Status change %(transition_icon)s %(status)s')
924 % {'transition_icon': '>',
924 % {'transition_icon': '>',
925 'status': ChangesetStatus.get_status_lbl(status)})
925 'status': ChangesetStatus.get_status_lbl(status)})
926 if close_pr:
926 if close_pr:
927 message = _('Closing with') + ' ' + message
927 message = _('Closing with') + ' ' + message
928 text = text or message
928 text = text or message
929 comm = CommentsModel().create(
929 comm = CommentsModel().create(
930 text=text,
930 text=text,
931 repo=c.rhodecode_db_repo.repo_id,
931 repo=c.rhodecode_db_repo.repo_id,
932 user=c.rhodecode_user.user_id,
932 user=c.rhodecode_user.user_id,
933 pull_request=pull_request_id,
933 pull_request=pull_request_id,
934 f_path=request.POST.get('f_path'),
934 f_path=request.POST.get('f_path'),
935 line_no=request.POST.get('line'),
935 line_no=request.POST.get('line'),
936 status_change=(ChangesetStatus.get_status_lbl(status)
936 status_change=(ChangesetStatus.get_status_lbl(status)
937 if status and allowed_to_change_status else None),
937 if status and allowed_to_change_status else None),
938 status_change_type=(status
938 status_change_type=(status
939 if status and allowed_to_change_status else None),
939 if status and allowed_to_change_status else None),
940 closing_pr=close_pr,
940 closing_pr=close_pr,
941 comment_type=comment_type,
941 comment_type=comment_type,
942 resolves_comment_id=resolves_comment_id
942 resolves_comment_id=resolves_comment_id
943 )
943 )
944
944
945 if allowed_to_change_status:
945 if allowed_to_change_status:
946 old_calculated_status = pull_request.calculated_review_status()
946 old_calculated_status = pull_request.calculated_review_status()
947 # get status if set !
947 # get status if set !
948 if status:
948 if status:
949 ChangesetStatusModel().set_status(
949 ChangesetStatusModel().set_status(
950 c.rhodecode_db_repo.repo_id,
950 c.rhodecode_db_repo.repo_id,
951 status,
951 status,
952 c.rhodecode_user.user_id,
952 c.rhodecode_user.user_id,
953 comm,
953 comm,
954 pull_request=pull_request_id
954 pull_request=pull_request_id
955 )
955 )
956
956
957 Session().flush()
957 Session().flush()
958 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
958 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
959 # we now calculate the status of pull request, and based on that
959 # we now calculate the status of pull request, and based on that
960 # calculation we set the commits status
960 # calculation we set the commits status
961 calculated_status = pull_request.calculated_review_status()
961 calculated_status = pull_request.calculated_review_status()
962 if old_calculated_status != calculated_status:
962 if old_calculated_status != calculated_status:
963 PullRequestModel()._trigger_pull_request_hook(
963 PullRequestModel()._trigger_pull_request_hook(
964 pull_request, c.rhodecode_user, 'review_status_change')
964 pull_request, c.rhodecode_user, 'review_status_change')
965
965
966 calculated_status_lbl = ChangesetStatus.get_status_lbl(
966 calculated_status_lbl = ChangesetStatus.get_status_lbl(
967 calculated_status)
967 calculated_status)
968
968
969 if close_pr:
969 if close_pr:
970 status_completed = (
970 status_completed = (
971 calculated_status in [ChangesetStatus.STATUS_APPROVED,
971 calculated_status in [ChangesetStatus.STATUS_APPROVED,
972 ChangesetStatus.STATUS_REJECTED])
972 ChangesetStatus.STATUS_REJECTED])
973 if forced or status_completed:
973 if forced or status_completed:
974 PullRequestModel().close_pull_request(
974 PullRequestModel().close_pull_request(
975 pull_request_id, c.rhodecode_user)
975 pull_request_id, c.rhodecode_user)
976 else:
976 else:
977 h.flash(_('Closing pull request on other statuses than '
977 h.flash(_('Closing pull request on other statuses than '
978 'rejected or approved is forbidden. '
978 'rejected or approved is forbidden. '
979 'Calculated status from all reviewers '
979 'Calculated status from all reviewers '
980 'is currently: %s') % calculated_status_lbl,
980 'is currently: %s') % calculated_status_lbl,
981 category='warning')
981 category='warning')
982
982
983 Session().commit()
983 Session().commit()
984
984
985 if not request.is_xhr:
985 if not request.is_xhr:
986 return redirect(h.url('pullrequest_show', repo_name=repo_name,
986 return redirect(h.url('pullrequest_show', repo_name=repo_name,
987 pull_request_id=pull_request_id))
987 pull_request_id=pull_request_id))
988
988
989 data = {
989 data = {
990 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
990 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
991 }
991 }
992 if comm:
992 if comm:
993 c.co = comm
993 c.co = comm
994 c.inline_comment = True if comm.line_no else False
994 c.inline_comment = True if comm.line_no else False
995 data.update(comm.get_dict())
995 data.update(comm.get_dict())
996 data.update({'rendered_text':
996 data.update({'rendered_text':
997 render('changeset/changeset_comment_block.mako')})
997 render('changeset/changeset_comment_block.mako')})
998
998
999 return data
999 return data
1000
1000
1001 @LoginRequired()
1001 @LoginRequired()
1002 @NotAnonymous()
1002 @NotAnonymous()
1003 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1003 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1004 'repository.admin')
1004 'repository.admin')
1005 @auth.CSRFRequired()
1005 @auth.CSRFRequired()
1006 @jsonify
1006 @jsonify
1007 def delete_comment(self, repo_name, comment_id):
1007 def delete_comment(self, repo_name, comment_id):
1008 return self._delete_comment(comment_id)
1008 return self._delete_comment(comment_id)
1009
1009
1010 def _delete_comment(self, comment_id):
1010 def _delete_comment(self, comment_id):
1011 comment_id = safe_int(comment_id)
1011 comment_id = safe_int(comment_id)
1012 co = ChangesetComment.get_or_404(comment_id)
1012 co = ChangesetComment.get_or_404(comment_id)
1013 if co.pull_request.is_closed():
1013 if co.pull_request.is_closed():
1014 # don't allow deleting comments on closed pull request
1014 # don't allow deleting comments on closed pull request
1015 raise HTTPForbidden()
1015 raise HTTPForbidden()
1016
1016
1017 is_owner = co.author.user_id == c.rhodecode_user.user_id
1017 is_owner = co.author.user_id == c.rhodecode_user.user_id
1018 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1018 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1019 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1019 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1020 old_calculated_status = co.pull_request.calculated_review_status()
1020 old_calculated_status = co.pull_request.calculated_review_status()
1021 CommentsModel().delete(comment=co)
1021 CommentsModel().delete(comment=co)
1022 Session().commit()
1022 Session().commit()
1023 calculated_status = co.pull_request.calculated_review_status()
1023 calculated_status = co.pull_request.calculated_review_status()
1024 if old_calculated_status != calculated_status:
1024 if old_calculated_status != calculated_status:
1025 PullRequestModel()._trigger_pull_request_hook(
1025 PullRequestModel()._trigger_pull_request_hook(
1026 co.pull_request, c.rhodecode_user, 'review_status_change')
1026 co.pull_request, c.rhodecode_user, 'review_status_change')
1027 return True
1027 return True
1028 else:
1028 else:
1029 raise HTTPForbidden()
1029 raise HTTPForbidden()
@@ -1,528 +1,547 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-label {
50 .comment-label {
51 float: left;
51 float: left;
52
52
53 padding: 0.4em 0.4em;
53 padding: 0.4em 0.4em;
54 margin: 2px 5px 0px -10px;
54 margin: 2px 5px 0px -10px;
55 display: inline-block;
55 display: inline-block;
56 min-height: 0;
56 min-height: 0;
57
57
58 text-align: center;
58 text-align: center;
59 font-size: 10px;
59 font-size: 10px;
60 line-height: .8em;
60 line-height: .8em;
61
61
62 font-family: @text-italic;
62 font-family: @text-italic;
63 background: #fff none;
63 background: #fff none;
64 color: @grey4;
64 color: @grey4;
65 border: 1px solid @grey4;
65 border: 1px solid @grey4;
66 white-space: nowrap;
66 white-space: nowrap;
67
67
68 text-transform: uppercase;
68 text-transform: uppercase;
69 min-width: 40px;
69 min-width: 40px;
70
70
71 &.todo {
71 &.todo {
72 color: @color5;
72 color: @color5;
73 font-family: @text-bold-italic;
73 font-family: @text-bold-italic;
74 }
74 }
75
76 .resolve {
77 cursor: pointer;
78 text-decoration: underline;
79 }
80
81 .resolved {
82 text-decoration: line-through;
83 color: @color1;
84 }
85 .resolved a {
86 text-decoration: line-through;
87 color: @color1;
88 }
89 .resolve-text {
90 color: @color1;
91 margin: 2px 8px;
92 font-family: @text-italic;
93 }
94
75 }
95 }
76
96
77
97
78 .comment {
98 .comment {
79
99
80 &.comment-general {
100 &.comment-general {
81 border: 1px solid @grey5;
101 border: 1px solid @grey5;
82 padding: 5px 5px 5px 5px;
102 padding: 5px 5px 5px 5px;
83 }
103 }
84
104
85 margin: @padding 0;
105 margin: @padding 0;
86 padding: 4px 0 0 0;
106 padding: 4px 0 0 0;
87 line-height: 1em;
107 line-height: 1em;
88
108
89 .rc-user {
109 .rc-user {
90 min-width: 0;
110 min-width: 0;
91 margin: 0px .5em 0 0;
111 margin: 0px .5em 0 0;
92
112
93 .user {
113 .user {
94 display: inline;
114 display: inline;
95 }
115 }
96 }
116 }
97
117
98 .meta {
118 .meta {
99 position: relative;
119 position: relative;
100 width: 100%;
120 width: 100%;
101 border-bottom: 1px solid @grey5;
121 border-bottom: 1px solid @grey5;
102 margin: -5px 0px;
122 margin: -5px 0px;
103 line-height: 24px;
123 line-height: 24px;
104
124
105 &:hover .permalink {
125 &:hover .permalink {
106 visibility: visible;
126 visibility: visible;
107 color: @rcblue;
127 color: @rcblue;
108 }
128 }
109 }
129 }
110
130
111 .author,
131 .author,
112 .date {
132 .date {
113 display: inline;
133 display: inline;
114
134
115 &:after {
135 &:after {
116 content: ' | ';
136 content: ' | ';
117 color: @grey5;
137 color: @grey5;
118 }
138 }
119 }
139 }
120
140
121 .author-general img {
141 .author-general img {
122 top: 3px;
142 top: 3px;
123 }
143 }
124 .author-inline img {
144 .author-inline img {
125 top: 3px;
145 top: 3px;
126 }
146 }
127
147
128 .status-change,
148 .status-change,
129 .permalink,
149 .permalink,
130 .changeset-status-lbl {
150 .changeset-status-lbl {
131 display: inline;
151 display: inline;
132 }
152 }
133
153
134 .permalink {
154 .permalink {
135 visibility: hidden;
155 visibility: hidden;
136 }
156 }
137
157
138 .comment-links-divider {
158 .comment-links-divider {
139 display: inline;
159 display: inline;
140 }
160 }
141
161
142 .comment-links-block {
162 .comment-links-block {
143 float:right;
163 float:right;
144 text-align: right;
164 text-align: right;
145 min-width: 85px;
165 min-width: 85px;
146
166
147 [class^="icon-"]:before,
167 [class^="icon-"]:before,
148 [class*=" icon-"]:before {
168 [class*=" icon-"]:before {
149 margin-left: 0;
169 margin-left: 0;
150 margin-right: 0;
170 margin-right: 0;
151 }
171 }
152 }
172 }
153
173
154 .comment-previous-link {
174 .comment-previous-link {
155 display: inline-block;
175 display: inline-block;
156
176
157 .arrow_comment_link{
177 .arrow_comment_link{
158 cursor: pointer;
178 cursor: pointer;
159 i {
179 i {
160 font-size:10px;
180 font-size:10px;
161 }
181 }
162 }
182 }
163 .arrow_comment_link.disabled {
183 .arrow_comment_link.disabled {
164 cursor: default;
184 cursor: default;
165 color: @grey5;
185 color: @grey5;
166 }
186 }
167 }
187 }
168
188
169 .comment-next-link {
189 .comment-next-link {
170 display: inline-block;
190 display: inline-block;
171
191
172 .arrow_comment_link{
192 .arrow_comment_link{
173 cursor: pointer;
193 cursor: pointer;
174 i {
194 i {
175 font-size:10px;
195 font-size:10px;
176 }
196 }
177 }
197 }
178 .arrow_comment_link.disabled {
198 .arrow_comment_link.disabled {
179 cursor: default;
199 cursor: default;
180 color: @grey5;
200 color: @grey5;
181 }
201 }
182 }
202 }
183
203
184 .flag_status {
204 .flag_status {
185 display: inline-block;
205 display: inline-block;
186 margin: -2px .5em 0 .25em
206 margin: -2px .5em 0 .25em
187 }
207 }
188
208
189 .delete-comment {
209 .delete-comment {
190 display: inline-block;
210 display: inline-block;
191 color: @rcblue;
211 color: @rcblue;
192
212
193 &:hover {
213 &:hover {
194 cursor: pointer;
214 cursor: pointer;
195 }
215 }
196 }
216 }
197
217
198
199 .text {
218 .text {
200 clear: both;
219 clear: both;
201 .border-radius(@border-radius);
220 .border-radius(@border-radius);
202 .box-sizing(border-box);
221 .box-sizing(border-box);
203
222
204 .markdown-block p,
223 .markdown-block p,
205 .rst-block p {
224 .rst-block p {
206 margin: .5em 0 !important;
225 margin: .5em 0 !important;
207 // TODO: lisa: This is needed because of other rst !important rules :[
226 // TODO: lisa: This is needed because of other rst !important rules :[
208 }
227 }
209 }
228 }
210
229
211 .pr-version {
230 .pr-version {
212 float: left;
231 float: left;
213 margin: 0px 4px;
232 margin: 0px 4px;
214 }
233 }
215 .pr-version-inline {
234 .pr-version-inline {
216 float: left;
235 float: left;
217 margin: 0px 4px;
236 margin: 0px 4px;
218 }
237 }
219 .pr-version-num {
238 .pr-version-num {
220 font-size: 10px;
239 font-size: 10px;
221 }
240 }
222
241
223 }
242 }
224
243
225 @comment-padding: 5px;
244 @comment-padding: 5px;
226
245
227 .inline-comments {
246 .inline-comments {
228 border-radius: @border-radius;
247 border-radius: @border-radius;
229 .comment {
248 .comment {
230 margin: 0;
249 margin: 0;
231 border-radius: @border-radius;
250 border-radius: @border-radius;
232 }
251 }
233 .comment-outdated {
252 .comment-outdated {
234 opacity: 0.5;
253 opacity: 0.5;
235 }
254 }
236
255
237 .comment-inline {
256 .comment-inline {
238 background: white;
257 background: white;
239 padding: @comment-padding @comment-padding;
258 padding: @comment-padding @comment-padding;
240 border: @comment-padding solid @grey6;
259 border: @comment-padding solid @grey6;
241
260
242 .text {
261 .text {
243 border: none;
262 border: none;
244 }
263 }
245 .meta {
264 .meta {
246 border-bottom: 1px solid @grey6;
265 border-bottom: 1px solid @grey6;
247 margin: -5px 0px;
266 margin: -5px 0px;
248 line-height: 24px;
267 line-height: 24px;
249 }
268 }
250 }
269 }
251 .comment-selected {
270 .comment-selected {
252 border-left: 6px solid @comment-highlight-color;
271 border-left: 6px solid @comment-highlight-color;
253 }
272 }
254 .comment-inline-form {
273 .comment-inline-form {
255 padding: @comment-padding;
274 padding: @comment-padding;
256 display: none;
275 display: none;
257 }
276 }
258 .cb-comment-add-button {
277 .cb-comment-add-button {
259 margin: @comment-padding;
278 margin: @comment-padding;
260 }
279 }
261 /* hide add comment button when form is open */
280 /* hide add comment button when form is open */
262 .comment-inline-form-open ~ .cb-comment-add-button {
281 .comment-inline-form-open ~ .cb-comment-add-button {
263 display: none;
282 display: none;
264 }
283 }
265 .comment-inline-form-open {
284 .comment-inline-form-open {
266 display: block;
285 display: block;
267 }
286 }
268 /* hide add comment button when form but no comments */
287 /* hide add comment button when form but no comments */
269 .comment-inline-form:first-child + .cb-comment-add-button {
288 .comment-inline-form:first-child + .cb-comment-add-button {
270 display: none;
289 display: none;
271 }
290 }
272 /* hide add comment button when no comments or form */
291 /* hide add comment button when no comments or form */
273 .cb-comment-add-button:first-child {
292 .cb-comment-add-button:first-child {
274 display: none;
293 display: none;
275 }
294 }
276 /* hide add comment button when only comment is being deleted */
295 /* hide add comment button when only comment is being deleted */
277 .comment-deleting:first-child + .cb-comment-add-button {
296 .comment-deleting:first-child + .cb-comment-add-button {
278 display: none;
297 display: none;
279 }
298 }
280 }
299 }
281
300
282
301
283 .show-outdated-comments {
302 .show-outdated-comments {
284 display: inline;
303 display: inline;
285 color: @rcblue;
304 color: @rcblue;
286 }
305 }
287
306
288 // Comment Form
307 // Comment Form
289 div.comment-form {
308 div.comment-form {
290 margin-top: 20px;
309 margin-top: 20px;
291 }
310 }
292
311
293 .comment-form strong {
312 .comment-form strong {
294 display: block;
313 display: block;
295 margin-bottom: 15px;
314 margin-bottom: 15px;
296 }
315 }
297
316
298 .comment-form textarea {
317 .comment-form textarea {
299 width: 100%;
318 width: 100%;
300 height: 100px;
319 height: 100px;
301 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
320 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
302 }
321 }
303
322
304 form.comment-form {
323 form.comment-form {
305 margin-top: 10px;
324 margin-top: 10px;
306 margin-left: 10px;
325 margin-left: 10px;
307 }
326 }
308
327
309 .comment-inline-form .comment-block-ta,
328 .comment-inline-form .comment-block-ta,
310 .comment-form .comment-block-ta,
329 .comment-form .comment-block-ta,
311 .comment-form .preview-box {
330 .comment-form .preview-box {
312 .border-radius(@border-radius);
331 .border-radius(@border-radius);
313 .box-sizing(border-box);
332 .box-sizing(border-box);
314 background-color: white;
333 background-color: white;
315 }
334 }
316
335
317 .comment-form-submit {
336 .comment-form-submit {
318 margin-top: 5px;
337 margin-top: 5px;
319 margin-left: 525px;
338 margin-left: 525px;
320 }
339 }
321
340
322 .file-comments {
341 .file-comments {
323 display: none;
342 display: none;
324 }
343 }
325
344
326 .comment-form .preview-box.unloaded,
345 .comment-form .preview-box.unloaded,
327 .comment-inline-form .preview-box.unloaded {
346 .comment-inline-form .preview-box.unloaded {
328 height: 50px;
347 height: 50px;
329 text-align: center;
348 text-align: center;
330 padding: 20px;
349 padding: 20px;
331 background-color: white;
350 background-color: white;
332 }
351 }
333
352
334 .comment-footer {
353 .comment-footer {
335 position: relative;
354 position: relative;
336 width: 100%;
355 width: 100%;
337 min-height: 42px;
356 min-height: 42px;
338
357
339 .status_box,
358 .status_box,
340 .cancel-button {
359 .cancel-button {
341 float: left;
360 float: left;
342 display: inline-block;
361 display: inline-block;
343 }
362 }
344
363
345 .action-buttons {
364 .action-buttons {
346 float: right;
365 float: right;
347 display: inline-block;
366 display: inline-block;
348 }
367 }
349 }
368 }
350
369
351 .comment-form {
370 .comment-form {
352
371
353 .comment {
372 .comment {
354 margin-left: 10px;
373 margin-left: 10px;
355 }
374 }
356
375
357 .comment-help {
376 .comment-help {
358 color: @grey4;
377 color: @grey4;
359 padding: 5px 0 5px 0;
378 padding: 5px 0 5px 0;
360 }
379 }
361
380
362 .comment-title {
381 .comment-title {
363 padding: 5px 0 5px 0;
382 padding: 5px 0 5px 0;
364 }
383 }
365
384
366 .comment-button {
385 .comment-button {
367 display: inline-block;
386 display: inline-block;
368 }
387 }
369
388
370 .comment-button-input {
389 .comment-button-input {
371 margin-right: 0;
390 margin-right: 0;
372 }
391 }
373
392
374 .comment-footer {
393 .comment-footer {
375 margin-bottom: 110px;
394 margin-bottom: 110px;
376 margin-top: 10px;
395 margin-top: 10px;
377 }
396 }
378 }
397 }
379
398
380
399
381 .comment-form-login {
400 .comment-form-login {
382 .comment-help {
401 .comment-help {
383 padding: 0.9em; //same as the button
402 padding: 0.9em; //same as the button
384 }
403 }
385
404
386 div.clearfix {
405 div.clearfix {
387 clear: both;
406 clear: both;
388 width: 100%;
407 width: 100%;
389 display: block;
408 display: block;
390 }
409 }
391 }
410 }
392
411
393 .comment-type {
412 .comment-type {
394 margin: 0px;
413 margin: 0px;
395 border-radius: inherit;
414 border-radius: inherit;
396 border-color: @grey6;
415 border-color: @grey6;
397 }
416 }
398
417
399 .preview-box {
418 .preview-box {
400 min-height: 105px;
419 min-height: 105px;
401 margin-bottom: 15px;
420 margin-bottom: 15px;
402 background-color: white;
421 background-color: white;
403 .border-radius(@border-radius);
422 .border-radius(@border-radius);
404 .box-sizing(border-box);
423 .box-sizing(border-box);
405 }
424 }
406
425
407 .add-another-button {
426 .add-another-button {
408 margin-left: 10px;
427 margin-left: 10px;
409 margin-top: 10px;
428 margin-top: 10px;
410 margin-bottom: 10px;
429 margin-bottom: 10px;
411 }
430 }
412
431
413 .comment .buttons {
432 .comment .buttons {
414 float: right;
433 float: right;
415 margin: -1px 0px 0px 0px;
434 margin: -1px 0px 0px 0px;
416 }
435 }
417
436
418 // Inline Comment Form
437 // Inline Comment Form
419 .injected_diff .comment-inline-form,
438 .injected_diff .comment-inline-form,
420 .comment-inline-form {
439 .comment-inline-form {
421 background-color: white;
440 background-color: white;
422 margin-top: 10px;
441 margin-top: 10px;
423 margin-bottom: 20px;
442 margin-bottom: 20px;
424 }
443 }
425
444
426 .inline-form {
445 .inline-form {
427 padding: 10px 7px;
446 padding: 10px 7px;
428 }
447 }
429
448
430 .inline-form div {
449 .inline-form div {
431 max-width: 100%;
450 max-width: 100%;
432 }
451 }
433
452
434 .overlay {
453 .overlay {
435 display: none;
454 display: none;
436 position: absolute;
455 position: absolute;
437 width: 100%;
456 width: 100%;
438 text-align: center;
457 text-align: center;
439 vertical-align: middle;
458 vertical-align: middle;
440 font-size: 16px;
459 font-size: 16px;
441 background: none repeat scroll 0 0 white;
460 background: none repeat scroll 0 0 white;
442
461
443 &.submitting {
462 &.submitting {
444 display: block;
463 display: block;
445 opacity: 0.5;
464 opacity: 0.5;
446 z-index: 100;
465 z-index: 100;
447 }
466 }
448 }
467 }
449 .comment-inline-form .overlay.submitting .overlay-text {
468 .comment-inline-form .overlay.submitting .overlay-text {
450 margin-top: 5%;
469 margin-top: 5%;
451 }
470 }
452
471
453 .comment-inline-form .clearfix,
472 .comment-inline-form .clearfix,
454 .comment-form .clearfix {
473 .comment-form .clearfix {
455 .border-radius(@border-radius);
474 .border-radius(@border-radius);
456 margin: 0px;
475 margin: 0px;
457 }
476 }
458
477
459 .comment-inline-form .comment-footer {
478 .comment-inline-form .comment-footer {
460 margin: 10px 0px 0px 0px;
479 margin: 10px 0px 0px 0px;
461 }
480 }
462
481
463 .hide-inline-form-button {
482 .hide-inline-form-button {
464 margin-left: 5px;
483 margin-left: 5px;
465 }
484 }
466 .comment-button .hide-inline-form {
485 .comment-button .hide-inline-form {
467 background: white;
486 background: white;
468 }
487 }
469
488
470 .comment-area {
489 .comment-area {
471 padding: 8px 12px;
490 padding: 8px 12px;
472 border: 1px solid @grey5;
491 border: 1px solid @grey5;
473 .border-radius(@border-radius);
492 .border-radius(@border-radius);
474 }
493 }
475
494
476 .comment-area-header .nav-links {
495 .comment-area-header .nav-links {
477 display: flex;
496 display: flex;
478 flex-flow: row wrap;
497 flex-flow: row wrap;
479 -webkit-flex-flow: row wrap;
498 -webkit-flex-flow: row wrap;
480 width: 100%;
499 width: 100%;
481 }
500 }
482
501
483 .comment-area-footer {
502 .comment-area-footer {
484 display: flex;
503 display: flex;
485 }
504 }
486
505
487 .comment-footer .toolbar {
506 .comment-footer .toolbar {
488
507
489 }
508 }
490
509
491 .nav-links {
510 .nav-links {
492 padding: 0;
511 padding: 0;
493 margin: 0;
512 margin: 0;
494 list-style: none;
513 list-style: none;
495 height: auto;
514 height: auto;
496 border-bottom: 1px solid @grey5;
515 border-bottom: 1px solid @grey5;
497 }
516 }
498 .nav-links li {
517 .nav-links li {
499 display: inline-block;
518 display: inline-block;
500 }
519 }
501 .nav-links li:before {
520 .nav-links li:before {
502 content: "";
521 content: "";
503 }
522 }
504 .nav-links li a.disabled {
523 .nav-links li a.disabled {
505 cursor: not-allowed;
524 cursor: not-allowed;
506 }
525 }
507
526
508 .nav-links li.active a {
527 .nav-links li.active a {
509 border-bottom: 2px solid @rcblue;
528 border-bottom: 2px solid @rcblue;
510 color: #000;
529 color: #000;
511 font-weight: 600;
530 font-weight: 600;
512 }
531 }
513 .nav-links li a {
532 .nav-links li a {
514 display: inline-block;
533 display: inline-block;
515 padding: 0px 10px 5px 10px;
534 padding: 0px 10px 5px 10px;
516 margin-bottom: -1px;
535 margin-bottom: -1px;
517 font-size: 14px;
536 font-size: 14px;
518 line-height: 28px;
537 line-height: 28px;
519 color: #8f8f8f;
538 color: #8f8f8f;
520 border-bottom: 2px solid transparent;
539 border-bottom: 2px solid transparent;
521 }
540 }
522
541
523 .toolbar-text {
542 .toolbar-text {
524 float: left;
543 float: left;
525 margin: -5px 0px 0px 0px;
544 margin: -5px 0px 0px 0px;
526 font-size: 12px;
545 font-size: 12px;
527 }
546 }
528
547
@@ -1,530 +1,530 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 * Code Mirror
20 * Code Mirror
21 */
21 */
22 // global code-mirror logger;, to enable run
22 // global code-mirror logger;, to enable run
23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
24
24
25 cmLog = Logger.get('CodeMirror');
25 cmLog = Logger.get('CodeMirror');
26 cmLog.setLevel(Logger.OFF);
26 cmLog.setLevel(Logger.OFF);
27
27
28
28
29 //global cache for inline forms
29 //global cache for inline forms
30 var userHintsCache = {};
30 var userHintsCache = {};
31
31
32 // global timer, used to cancel async loading
32 // global timer, used to cancel async loading
33 var CodeMirrorLoadUserHintTimer;
33 var CodeMirrorLoadUserHintTimer;
34
34
35 var escapeRegExChars = function(value) {
35 var escapeRegExChars = function(value) {
36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
37 };
37 };
38
38
39 /**
39 /**
40 * Load hints from external source returns an array of objects in a format
40 * Load hints from external source returns an array of objects in a format
41 * that hinting lib requires
41 * that hinting lib requires
42 * @returns {Array}
42 * @returns {Array}
43 */
43 */
44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
45 cmLog.debug('Loading mentions users via AJAX');
45 cmLog.debug('Loading mentions users via AJAX');
46 var _users = [];
46 var _users = [];
47 $.ajax({
47 $.ajax({
48 type: 'GET',
48 type: 'GET',
49 data: {query: query},
49 data: {query: query},
50 url: pyroutes.url('user_autocomplete_data'),
50 url: pyroutes.url('user_autocomplete_data'),
51 headers: {'X-PARTIAL-XHR': true},
51 headers: {'X-PARTIAL-XHR': true},
52 async: true
52 async: true
53 })
53 })
54 .done(function(data) {
54 .done(function(data) {
55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
56 $.each(data.suggestions, function(i) {
56 $.each(data.suggestions, function(i) {
57 var userObj = data.suggestions[i];
57 var userObj = data.suggestions[i];
58
58
59 if (userObj.username !== "default") {
59 if (userObj.username !== "default") {
60 _users.push({
60 _users.push({
61 text: userObj.username + " ",
61 text: userObj.username + " ",
62 org_text: userObj.username,
62 org_text: userObj.username,
63 displayText: userObj.value_display, // search that field
63 displayText: userObj.value_display, // search that field
64 // internal caches
64 // internal caches
65 _icon_link: userObj.icon_link,
65 _icon_link: userObj.icon_link,
66 _text: userObj.value_display,
66 _text: userObj.value_display,
67
67
68 render: function(elt, data, completion) {
68 render: function(elt, data, completion) {
69 var el = document.createElement('div');
69 var el = document.createElement('div');
70 el.className = "CodeMirror-hint-entry";
70 el.className = "CodeMirror-hint-entry";
71 el.innerHTML = tmpl.format(
71 el.innerHTML = tmpl.format(
72 completion._icon_link, completion._text);
72 completion._icon_link, completion._text);
73 elt.appendChild(el);
73 elt.appendChild(el);
74 }
74 }
75 });
75 });
76 }
76 }
77 });
77 });
78 cmLog.debug('Mention users loaded');
78 cmLog.debug('Mention users loaded');
79 // set to global cache
79 // set to global cache
80 userHintsCache[query] = _users;
80 userHintsCache[query] = _users;
81 triggerHints(userHintsCache[query]);
81 triggerHints(userHintsCache[query]);
82 })
82 })
83 .fail(function(data, textStatus, xhr) {
83 .fail(function(data, textStatus, xhr) {
84 alert("error processing request: " + textStatus);
84 alert("error processing request: " + textStatus);
85 });
85 });
86 };
86 };
87
87
88 /**
88 /**
89 * filters the results based on the current context
89 * filters the results based on the current context
90 * @param users
90 * @param users
91 * @param context
91 * @param context
92 * @returns {Array}
92 * @returns {Array}
93 */
93 */
94 var CodeMirrorFilterUsers = function(users, context) {
94 var CodeMirrorFilterUsers = function(users, context) {
95 var MAX_LIMIT = 10;
95 var MAX_LIMIT = 10;
96 var filtered_users = [];
96 var filtered_users = [];
97 var curWord = context.string;
97 var curWord = context.string;
98
98
99 cmLog.debug('Filtering users based on query:', curWord);
99 cmLog.debug('Filtering users based on query:', curWord);
100 $.each(users, function(i) {
100 $.each(users, function(i) {
101 var match = users[i];
101 var match = users[i];
102 var searchText = match.displayText;
102 var searchText = match.displayText;
103
103
104 if (!curWord ||
104 if (!curWord ||
105 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
105 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
106 // reset state
106 // reset state
107 match._text = match.displayText;
107 match._text = match.displayText;
108 if (curWord) {
108 if (curWord) {
109 // do highlighting
109 // do highlighting
110 var pattern = '(' + escapeRegExChars(curWord) + ')';
110 var pattern = '(' + escapeRegExChars(curWord) + ')';
111 match._text = searchText.replace(
111 match._text = searchText.replace(
112 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
112 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
113 }
113 }
114
114
115 filtered_users.push(match);
115 filtered_users.push(match);
116 }
116 }
117 // to not return to many results, use limit of filtered results
117 // to not return to many results, use limit of filtered results
118 if (filtered_users.length > MAX_LIMIT) {
118 if (filtered_users.length > MAX_LIMIT) {
119 return false;
119 return false;
120 }
120 }
121 });
121 });
122
122
123 return filtered_users;
123 return filtered_users;
124 };
124 };
125
125
126 var CodeMirrorMentionHint = function(editor, callback, options) {
126 var CodeMirrorMentionHint = function(editor, callback, options) {
127 var cur = editor.getCursor();
127 var cur = editor.getCursor();
128 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
128 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
129
129
130 // match on @ +1char
130 // match on @ +1char
131 var tokenMatch = new RegExp(
131 var tokenMatch = new RegExp(
132 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
132 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
133
133
134 var tokenStr = '';
134 var tokenStr = '';
135 if (tokenMatch !== null && tokenMatch.length > 0){
135 if (tokenMatch !== null && tokenMatch.length > 0){
136 tokenStr = tokenMatch[0].strip();
136 tokenStr = tokenMatch[0].strip();
137 } else {
137 } else {
138 // skip if we didn't match our token
138 // skip if we didn't match our token
139 return;
139 return;
140 }
140 }
141
141
142 var context = {
142 var context = {
143 start: (cur.ch - tokenStr.length) + 1,
143 start: (cur.ch - tokenStr.length) + 1,
144 end: cur.ch,
144 end: cur.ch,
145 string: tokenStr.slice(1),
145 string: tokenStr.slice(1),
146 type: null
146 type: null
147 };
147 };
148
148
149 // case when we put the @sign in fron of a string,
149 // case when we put the @sign in fron of a string,
150 // eg <@ we put it here>sometext then we need to prepend to text
150 // eg <@ we put it here>sometext then we need to prepend to text
151 if (context.end > cur.ch) {
151 if (context.end > cur.ch) {
152 context.start = context.start + 1; // we add to the @ sign
152 context.start = context.start + 1; // we add to the @ sign
153 context.end = cur.ch; // don't eat front part just append
153 context.end = cur.ch; // don't eat front part just append
154 context.string = context.string.slice(1, cur.ch - context.start);
154 context.string = context.string.slice(1, cur.ch - context.start);
155 }
155 }
156
156
157 cmLog.debug('Mention context', context);
157 cmLog.debug('Mention context', context);
158
158
159 var triggerHints = function(userHints){
159 var triggerHints = function(userHints){
160 return callback({
160 return callback({
161 list: CodeMirrorFilterUsers(userHints, context),
161 list: CodeMirrorFilterUsers(userHints, context),
162 from: CodeMirror.Pos(cur.line, context.start),
162 from: CodeMirror.Pos(cur.line, context.start),
163 to: CodeMirror.Pos(cur.line, context.end)
163 to: CodeMirror.Pos(cur.line, context.end)
164 });
164 });
165 };
165 };
166
166
167 var queryBasedHintsCache = undefined;
167 var queryBasedHintsCache = undefined;
168 // if we have something in the cache, try to fetch the query based cache
168 // if we have something in the cache, try to fetch the query based cache
169 if (userHintsCache !== {}){
169 if (userHintsCache !== {}){
170 queryBasedHintsCache = userHintsCache[context.string];
170 queryBasedHintsCache = userHintsCache[context.string];
171 }
171 }
172
172
173 if (queryBasedHintsCache !== undefined) {
173 if (queryBasedHintsCache !== undefined) {
174 cmLog.debug('Users loaded from cache');
174 cmLog.debug('Users loaded from cache');
175 triggerHints(queryBasedHintsCache);
175 triggerHints(queryBasedHintsCache);
176 } else {
176 } else {
177 // this takes care for async loading, and then displaying results
177 // this takes care for async loading, and then displaying results
178 // and also propagates the userHintsCache
178 // and also propagates the userHintsCache
179 window.clearTimeout(CodeMirrorLoadUserHintTimer);
179 window.clearTimeout(CodeMirrorLoadUserHintTimer);
180 CodeMirrorLoadUserHintTimer = setTimeout(function() {
180 CodeMirrorLoadUserHintTimer = setTimeout(function() {
181 CodeMirrorLoadUserHints(context.string, triggerHints);
181 CodeMirrorLoadUserHints(context.string, triggerHints);
182 }, 300);
182 }, 300);
183 }
183 }
184 };
184 };
185
185
186 var CodeMirrorCompleteAfter = function(cm, pred) {
186 var CodeMirrorCompleteAfter = function(cm, pred) {
187 var options = {
187 var options = {
188 completeSingle: false,
188 completeSingle: false,
189 async: true,
189 async: true,
190 closeOnUnfocus: true
190 closeOnUnfocus: true
191 };
191 };
192 var cur = cm.getCursor();
192 var cur = cm.getCursor();
193 setTimeout(function() {
193 setTimeout(function() {
194 if (!cm.state.completionActive) {
194 if (!cm.state.completionActive) {
195 cmLog.debug('Trigger mentions hinting');
195 cmLog.debug('Trigger mentions hinting');
196 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
196 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
197 }
197 }
198 }, 100);
198 }, 100);
199
199
200 // tell CodeMirror we didn't handle the key
200 // tell CodeMirror we didn't handle the key
201 // trick to trigger on a char but still complete it
201 // trick to trigger on a char but still complete it
202 return CodeMirror.Pass;
202 return CodeMirror.Pass;
203 };
203 };
204
204
205 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
205 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
206 var ta = $('#' + textAreadId).get(0);
206 var ta = $('#' + textAreadId).get(0);
207 if (focus === undefined) {
207 if (focus === undefined) {
208 focus = true;
208 focus = true;
209 }
209 }
210
210
211 // default options
211 // default options
212 var codeMirrorOptions = {
212 var codeMirrorOptions = {
213 mode: "null",
213 mode: "null",
214 lineNumbers: true,
214 lineNumbers: true,
215 indentUnit: 4,
215 indentUnit: 4,
216 autofocus: focus
216 autofocus: focus
217 };
217 };
218
218
219 if (options !== undefined) {
219 if (options !== undefined) {
220 // extend with custom options
220 // extend with custom options
221 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
221 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
222 }
222 }
223
223
224 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
224 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
225
225
226 $('#reset').on('click', function(e) {
226 $('#reset').on('click', function(e) {
227 window.location = resetUrl;
227 window.location = resetUrl;
228 });
228 });
229
229
230 return myCodeMirror;
230 return myCodeMirror;
231 };
231 };
232
232
233 var initCommentBoxCodeMirror = function(textAreaId, triggerActions){
233 var initCommentBoxCodeMirror = function(textAreaId, triggerActions){
234 var initialHeight = 100;
234 var initialHeight = 100;
235
235
236 if (typeof userHintsCache === "undefined") {
236 if (typeof userHintsCache === "undefined") {
237 userHintsCache = {};
237 userHintsCache = {};
238 cmLog.debug('Init empty cache for mentions');
238 cmLog.debug('Init empty cache for mentions');
239 }
239 }
240 if (!$(textAreaId).get(0)) {
240 if (!$(textAreaId).get(0)) {
241 cmLog.debug('Element for textarea not found', textAreaId);
241 cmLog.debug('Element for textarea not found', textAreaId);
242 return;
242 return;
243 }
243 }
244 /**
244 /**
245 * Filter action based on typed in text
245 * Filter action based on typed in text
246 * @param actions
246 * @param actions
247 * @param context
247 * @param context
248 * @returns {Array}
248 * @returns {Array}
249 */
249 */
250
250
251 var filterActions = function(actions, context){
251 var filterActions = function(actions, context){
252 var MAX_LIMIT = 10;
252 var MAX_LIMIT = 10;
253 var filtered_actions= [];
253 var filtered_actions= [];
254 var curWord = context.string;
254 var curWord = context.string;
255
255
256 cmLog.debug('Filtering actions based on query:', curWord);
256 cmLog.debug('Filtering actions based on query:', curWord);
257 $.each(actions, function(i) {
257 $.each(actions, function(i) {
258 var match = actions[i];
258 var match = actions[i];
259 var searchText = match.displayText;
259 var searchText = match.displayText;
260
260
261 if (!curWord ||
261 if (!curWord ||
262 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
262 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
263 // reset state
263 // reset state
264 match._text = match.displayText;
264 match._text = match.displayText;
265 if (curWord) {
265 if (curWord) {
266 // do highlighting
266 // do highlighting
267 var pattern = '(' + escapeRegExChars(curWord) + ')';
267 var pattern = '(' + escapeRegExChars(curWord) + ')';
268 match._text = searchText.replace(
268 match._text = searchText.replace(
269 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
269 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
270 }
270 }
271
271
272 filtered_actions.push(match);
272 filtered_actions.push(match);
273 }
273 }
274 // to not return to many results, use limit of filtered results
274 // to not return to many results, use limit of filtered results
275 if (filtered_actions.length > MAX_LIMIT) {
275 if (filtered_actions.length > MAX_LIMIT) {
276 return false;
276 return false;
277 }
277 }
278 });
278 });
279 return filtered_actions;
279 return filtered_actions;
280 };
280 };
281
281
282 var submitForm = function(cm, pred) {
282 var submitForm = function(cm, pred) {
283 $(cm.display.input.textarea.form).submit();
283 $(cm.display.input.textarea.form).submit();
284 return CodeMirror.Pass;
284 return CodeMirror.Pass;
285 };
285 };
286
286
287 var completeActions = function(cm, pred) {
287 var completeActions = function(cm, pred) {
288 var cur = cm.getCursor();
288 var cur = cm.getCursor();
289 var options = {
289 var options = {
290 closeOnUnfocus: true
290 closeOnUnfocus: true
291 };
291 };
292 setTimeout(function() {
292 setTimeout(function() {
293 if (!cm.state.completionActive) {
293 if (!cm.state.completionActive) {
294 cmLog.debug('Trigger actions hinting');
294 cmLog.debug('Trigger actions hinting');
295 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
295 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
296 }
296 }
297 }, 100);
297 }, 100);
298 };
298 };
299
299
300 var extraKeys = {
300 var extraKeys = {
301 "'@'": CodeMirrorCompleteAfter,
301 "'@'": CodeMirrorCompleteAfter,
302 Tab: function(cm) {
302 Tab: function(cm) {
303 // space indent instead of TABS
303 // space indent instead of TABS
304 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
304 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
305 cm.replaceSelection(spaces);
305 cm.replaceSelection(spaces);
306 }
306 }
307 };
307 };
308 // submit form on Meta-Enter
308 // submit form on Meta-Enter
309 if (OSType === "mac") {
309 if (OSType === "mac") {
310 extraKeys["Cmd-Enter"] = submitForm;
310 extraKeys["Cmd-Enter"] = submitForm;
311 }
311 }
312 else {
312 else {
313 extraKeys["Ctrl-Enter"] = submitForm;
313 extraKeys["Ctrl-Enter"] = submitForm;
314 }
314 }
315
315
316 if (triggerActions) {
316 if (triggerActions) {
317 extraKeys["Ctrl-Space"] = completeActions;
317 extraKeys["Ctrl-Space"] = completeActions;
318 }
318 }
319
319
320 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
320 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
321 lineNumbers: false,
321 lineNumbers: false,
322 indentUnit: 4,
322 indentUnit: 4,
323 viewportMargin: 30,
323 viewportMargin: 30,
324 // this is a trick to trigger some logic behind codemirror placeholder
324 // this is a trick to trigger some logic behind codemirror placeholder
325 // it influences styling and behaviour.
325 // it influences styling and behaviour.
326 placeholder: " ",
326 placeholder: " ",
327 extraKeys: extraKeys,
327 extraKeys: extraKeys,
328 lineWrapping: true
328 lineWrapping: true
329 });
329 });
330
330
331 cm.setSize(null, initialHeight);
331 cm.setSize(null, initialHeight);
332 cm.setOption("mode", DEFAULT_RENDERER);
332 cm.setOption("mode", DEFAULT_RENDERER);
333 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
333 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
334 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
334 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
335 // start listening on changes to make auto-expanded editor
335 // start listening on changes to make auto-expanded editor
336 cm.on("change", function(self) {
336 cm.on("change", function(self) {
337 var height = initialHeight;
337 var height = initialHeight;
338 var lines = self.lineCount();
338 var lines = self.lineCount();
339 if ( lines > 6 && lines < 20) {
339 if ( lines > 6 && lines < 20) {
340 height = "auto";
340 height = "auto";
341 }
341 }
342 else if (lines >= 20){
342 else if (lines >= 20){
343 zheight = 20*15;
343 zheight = 20*15;
344 }
344 }
345 self.setSize(null, height);
345 self.setSize(null, height);
346 });
346 });
347
347
348 var actionHint = function(editor, options) {
348 var actionHint = function(editor, options) {
349 var cur = editor.getCursor();
349 var cur = editor.getCursor();
350 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
350 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
351
351
352 var tokenMatch = new RegExp('[a-zA-Z]{1}[a-zA-Z]*$').exec(curLine);
352 var tokenMatch = new RegExp('[a-zA-Z]{1}[a-zA-Z]*$').exec(curLine);
353
353
354 var tokenStr = '';
354 var tokenStr = '';
355 if (tokenMatch !== null && tokenMatch.length > 0){
355 if (tokenMatch !== null && tokenMatch.length > 0){
356 tokenStr = tokenMatch[0].strip();
356 tokenStr = tokenMatch[0].strip();
357 }
357 }
358
358
359 var context = {
359 var context = {
360 start: cur.ch - tokenStr.length,
360 start: cur.ch - tokenStr.length,
361 end: cur.ch,
361 end: cur.ch,
362 string: tokenStr,
362 string: tokenStr,
363 type: null
363 type: null
364 };
364 };
365
365
366 var actions = [
366 var actions = [
367 {
367 {
368 text: "approve",
368 text: "approve",
369 displayText: _gettext('Set status to Approved'),
369 displayText: _gettext('Set status to Approved'),
370 hint: function(CodeMirror, data, completion) {
370 hint: function(CodeMirror, data, completion) {
371 CodeMirror.replaceRange("", completion.from || data.from,
371 CodeMirror.replaceRange("", completion.from || data.from,
372 completion.to || data.to, "complete");
372 completion.to || data.to, "complete");
373 $('#change_status').select2("val", 'approved').trigger('change');
373 $('#change_status_general').select2("val", 'approved').trigger('change');
374 },
374 },
375 render: function(elt, data, completion) {
375 render: function(elt, data, completion) {
376 var el = document.createElement('div');
376 var el = document.createElement('div');
377 el.className = "flag_status flag_status_comment_box approved pull-left";
377 el.className = "flag_status flag_status_comment_box approved pull-left";
378 elt.appendChild(el);
378 elt.appendChild(el);
379
379
380 el = document.createElement('span');
380 el = document.createElement('span');
381 el.innerHTML = completion.displayText;
381 el.innerHTML = completion.displayText;
382 elt.appendChild(el);
382 elt.appendChild(el);
383 }
383 }
384 },
384 },
385 {
385 {
386 text: "reject",
386 text: "reject",
387 displayText: _gettext('Set status to Rejected'),
387 displayText: _gettext('Set status to Rejected'),
388 hint: function(CodeMirror, data, completion) {
388 hint: function(CodeMirror, data, completion) {
389 CodeMirror.replaceRange("", completion.from || data.from,
389 CodeMirror.replaceRange("", completion.from || data.from,
390 completion.to || data.to, "complete");
390 completion.to || data.to, "complete");
391 $('#change_status').select2("val", 'rejected').trigger('change');
391 $('#change_status_general').select2("val", 'rejected').trigger('change');
392 },
392 },
393 render: function(elt, data, completion) {
393 render: function(elt, data, completion) {
394 var el = document.createElement('div');
394 var el = document.createElement('div');
395 el.className = "flag_status flag_status_comment_box rejected pull-left";
395 el.className = "flag_status flag_status_comment_box rejected pull-left";
396 elt.appendChild(el);
396 elt.appendChild(el);
397
397
398 el = document.createElement('span');
398 el = document.createElement('span');
399 el.innerHTML = completion.displayText;
399 el.innerHTML = completion.displayText;
400 elt.appendChild(el);
400 elt.appendChild(el);
401 }
401 }
402 }
402 }
403 ];
403 ];
404
404
405 return {
405 return {
406 list: filterActions(actions, context),
406 list: filterActions(actions, context),
407 from: CodeMirror.Pos(cur.line, context.start),
407 from: CodeMirror.Pos(cur.line, context.start),
408 to: CodeMirror.Pos(cur.line, context.end)
408 to: CodeMirror.Pos(cur.line, context.end)
409 };
409 };
410 };
410 };
411 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
411 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
412 CodeMirror.registerHelper("hint", "actions", actionHint);
412 CodeMirror.registerHelper("hint", "actions", actionHint);
413 return cm;
413 return cm;
414 };
414 };
415
415
416 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
416 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
417 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
417 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
418 codeMirrorInstance.setOption("mode", mode);
418 codeMirrorInstance.setOption("mode", mode);
419 };
419 };
420
420
421 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
421 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
422 codeMirrorInstance.setOption("lineWrapping", line_wrap);
422 codeMirrorInstance.setOption("lineWrapping", line_wrap);
423 };
423 };
424
424
425 var setCodeMirrorModeFromSelect = function(
425 var setCodeMirrorModeFromSelect = function(
426 targetSelect, targetFileInput, codeMirrorInstance, callback){
426 targetSelect, targetFileInput, codeMirrorInstance, callback){
427
427
428 $(targetSelect).on('change', function(e) {
428 $(targetSelect).on('change', function(e) {
429 cmLog.debug('codemirror select2 mode change event !');
429 cmLog.debug('codemirror select2 mode change event !');
430 var selected = e.currentTarget;
430 var selected = e.currentTarget;
431 var node = selected.options[selected.selectedIndex];
431 var node = selected.options[selected.selectedIndex];
432 var mimetype = node.value;
432 var mimetype = node.value;
433 cmLog.debug('picked mimetype', mimetype);
433 cmLog.debug('picked mimetype', mimetype);
434 var new_mode = $(node).attr('mode');
434 var new_mode = $(node).attr('mode');
435 setCodeMirrorMode(codeMirrorInstance, new_mode);
435 setCodeMirrorMode(codeMirrorInstance, new_mode);
436 cmLog.debug('set new mode', new_mode);
436 cmLog.debug('set new mode', new_mode);
437
437
438 //propose filename from picked mode
438 //propose filename from picked mode
439 cmLog.debug('setting mimetype', mimetype);
439 cmLog.debug('setting mimetype', mimetype);
440 var proposed_ext = getExtFromMimeType(mimetype);
440 var proposed_ext = getExtFromMimeType(mimetype);
441 cmLog.debug('file input', $(targetFileInput).val());
441 cmLog.debug('file input', $(targetFileInput).val());
442 var file_data = getFilenameAndExt($(targetFileInput).val());
442 var file_data = getFilenameAndExt($(targetFileInput).val());
443 var filename = file_data.filename || 'filename1';
443 var filename = file_data.filename || 'filename1';
444 $(targetFileInput).val(filename + proposed_ext);
444 $(targetFileInput).val(filename + proposed_ext);
445 cmLog.debug('proposed file', filename + proposed_ext);
445 cmLog.debug('proposed file', filename + proposed_ext);
446
446
447
447
448 if (typeof(callback) === 'function') {
448 if (typeof(callback) === 'function') {
449 try {
449 try {
450 cmLog.debug('running callback', callback);
450 cmLog.debug('running callback', callback);
451 callback(filename, mimetype, new_mode);
451 callback(filename, mimetype, new_mode);
452 } catch (err) {
452 } catch (err) {
453 console.log('failed to run callback', callback, err);
453 console.log('failed to run callback', callback, err);
454 }
454 }
455 }
455 }
456 cmLog.debug('finish iteration...');
456 cmLog.debug('finish iteration...');
457 });
457 });
458 };
458 };
459
459
460 var setCodeMirrorModeFromInput = function(
460 var setCodeMirrorModeFromInput = function(
461 targetSelect, targetFileInput, codeMirrorInstance, callback) {
461 targetSelect, targetFileInput, codeMirrorInstance, callback) {
462
462
463 // on type the new filename set mode
463 // on type the new filename set mode
464 $(targetFileInput).on('keyup', function(e) {
464 $(targetFileInput).on('keyup', function(e) {
465 var file_data = getFilenameAndExt(this.value);
465 var file_data = getFilenameAndExt(this.value);
466 if (file_data.ext === null) {
466 if (file_data.ext === null) {
467 return;
467 return;
468 }
468 }
469
469
470 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
470 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
471 cmLog.debug('mimetype from file', file_data, mimetypes);
471 cmLog.debug('mimetype from file', file_data, mimetypes);
472 var detected_mode;
472 var detected_mode;
473 var detected_option;
473 var detected_option;
474 for (var i in mimetypes) {
474 for (var i in mimetypes) {
475 var mt = mimetypes[i];
475 var mt = mimetypes[i];
476 if (!detected_mode) {
476 if (!detected_mode) {
477 detected_mode = detectCodeMirrorMode(this.value, mt);
477 detected_mode = detectCodeMirrorMode(this.value, mt);
478 }
478 }
479
479
480 if (!detected_option) {
480 if (!detected_option) {
481 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
481 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
482 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
482 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
483 detected_option = mt;
483 detected_option = mt;
484 }
484 }
485 }
485 }
486 }
486 }
487
487
488 cmLog.debug('detected mode', detected_mode);
488 cmLog.debug('detected mode', detected_mode);
489 cmLog.debug('detected option', detected_option);
489 cmLog.debug('detected option', detected_option);
490 if (detected_mode && detected_option){
490 if (detected_mode && detected_option){
491
491
492 $(targetSelect).select2("val", detected_option);
492 $(targetSelect).select2("val", detected_option);
493 setCodeMirrorMode(codeMirrorInstance, detected_mode);
493 setCodeMirrorMode(codeMirrorInstance, detected_mode);
494
494
495 if(typeof(callback) === 'function'){
495 if(typeof(callback) === 'function'){
496 try{
496 try{
497 cmLog.debug('running callback', callback);
497 cmLog.debug('running callback', callback);
498 var filename = file_data.filename + "." + file_data.ext;
498 var filename = file_data.filename + "." + file_data.ext;
499 callback(filename, detected_option, detected_mode);
499 callback(filename, detected_option, detected_mode);
500 }catch (err){
500 }catch (err){
501 console.log('failed to run callback', callback, err);
501 console.log('failed to run callback', callback, err);
502 }
502 }
503 }
503 }
504 }
504 }
505
505
506 });
506 });
507 };
507 };
508
508
509 var fillCodeMirrorOptions = function(targetSelect) {
509 var fillCodeMirrorOptions = function(targetSelect) {
510 //inject new modes, based on codeMirrors modeInfo object
510 //inject new modes, based on codeMirrors modeInfo object
511 var modes_select = $(targetSelect);
511 var modes_select = $(targetSelect);
512 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
512 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
513 var m = CodeMirror.modeInfo[i];
513 var m = CodeMirror.modeInfo[i];
514 var opt = new Option(m.name, m.mime);
514 var opt = new Option(m.name, m.mime);
515 $(opt).attr('mode', m.mode);
515 $(opt).attr('mode', m.mode);
516 modes_select.append(opt);
516 modes_select.append(opt);
517 }
517 }
518 };
518 };
519
519
520 var CodeMirrorPreviewEnable = function(edit_mode) {
520 var CodeMirrorPreviewEnable = function(edit_mode) {
521 // in case it a preview enabled mode enable the button
521 // in case it a preview enabled mode enable the button
522 if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) {
522 if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) {
523 $('#render_preview').removeClass('hidden');
523 $('#render_preview').removeClass('hidden');
524 }
524 }
525 else {
525 else {
526 if (!$('#render_preview').hasClass('hidden')) {
526 if (!$('#render_preview').hasClass('hidden')) {
527 $('#render_preview').addClass('hidden');
527 $('#render_preview').addClass('hidden');
528 }
528 }
529 }
529 }
530 };
530 };
@@ -1,798 +1,842 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 // returns a node from given html;
28 // returns a node from given html;
29 var fromHTML = function(html){
29 var fromHTML = function(html){
30 var _html = document.createElement('element');
30 var _html = document.createElement('element');
31 _html.innerHTML = html;
31 _html.innerHTML = html;
32 return _html;
32 return _html;
33 };
33 };
34
34
35 var tableTr = function(cls, body){
35 var tableTr = function(cls, body){
36 var _el = document.createElement('div');
36 var _el = document.createElement('div');
37 var _body = $(body).attr('id');
37 var _body = $(body).attr('id');
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 var id = 'comment-tr-{0}'.format(comment_id);
39 var id = 'comment-tr-{0}'.format(comment_id);
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 '<td></td>'+
42 '<td></td>'+
43 '<td></td>'+
43 '<td></td>'+
44 '<td></td>'+
44 '<td></td>'+
45 '<td>{2}</td>'+
45 '<td>{2}</td>'+
46 '</tr></tbody></table>').format(id, cls, body);
46 '</tr></tbody></table>').format(id, cls, body);
47 $(_el).html(_html);
47 $(_el).html(_html);
48 return _el.children[0].children[0].children[0];
48 return _el.children[0].children[0].children[0];
49 };
49 };
50
50
51 function bindDeleteCommentButtons() {
51 function bindDeleteCommentButtons() {
52 $('.delete-comment').one('click', function() {
52 $('.delete-comment').one('click', function() {
53 var comment_id = $(this).data("comment-id");
53 var comment_id = $(this).data("comment-id");
54
54
55 if (comment_id){
55 if (comment_id){
56 deleteComment(comment_id);
56 deleteComment(comment_id);
57 }
57 }
58 });
58 });
59 }
59 }
60
60
61 var deleteComment = function(comment_id) {
61 var deleteComment = function(comment_id) {
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
63 var postData = {
63 var postData = {
64 '_method': 'delete',
64 '_method': 'delete',
65 'csrf_token': CSRF_TOKEN
65 'csrf_token': CSRF_TOKEN
66 };
66 };
67
67
68 var success = function(o) {
68 var success = function(o) {
69 window.location.reload();
69 window.location.reload();
70 };
70 };
71 ajaxPOST(url, postData, success);
71 ajaxPOST(url, postData, success);
72 };
72 };
73
73
74
74
75 var bindToggleButtons = function() {
75 var bindToggleButtons = function() {
76 $('.comment-toggle').on('click', function() {
76 $('.comment-toggle').on('click', function() {
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
78 });
78 });
79 };
79 };
80
80
81 var linkifyComments = function(comments) {
81 var linkifyComments = function(comments) {
82 /* TODO: marcink: remove this - it should no longer needed */
82 /* TODO: marcink: remove this - it should no longer needed */
83 for (var i = 0; i < comments.length; i++) {
83 for (var i = 0; i < comments.length; i++) {
84 var comment_id = $(comments[i]).data('comment-id');
84 var comment_id = $(comments[i]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
87
87
88 // place next/prev links
88 // place next/prev links
89 if (prev_comment_id) {
89 if (prev_comment_id) {
90 $('#prev_c_' + comment_id).show();
90 $('#prev_c_' + comment_id).show();
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
93 }
93 }
94 if (next_comment_id) {
94 if (next_comment_id) {
95 $('#next_c_' + comment_id).show();
95 $('#next_c_' + comment_id).show();
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
98 }
98 }
99 /* TODO(marcink): end removal here */
99 /* TODO(marcink): end removal here */
100
100
101 // place a first link to the total counter
101 // place a first link to the total counter
102 if (i === 0) {
102 if (i === 0) {
103 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
103 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
104 }
104 }
105 }
105 }
106
106
107 };
107 };
108
108
109 var bindToggleButtons = function() {
110 $('.comment-toggle').on('click', function() {
111 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
112 });
113 };
109
114
110 /* Comment form for main and inline comments */
115 /* Comment form for main and inline comments */
111
116
112 (function(mod) {
117 (function(mod) {
113 if (typeof exports == "object" && typeof module == "object") // CommonJS
118 if (typeof exports == "object" && typeof module == "object") // CommonJS
114 module.exports = mod();
119 module.exports = mod();
115 else // Plain browser env
120 else // Plain browser env
116 (this || window).CommentForm = mod();
121 (this || window).CommentForm = mod();
117
122
118 })(function() {
123 })(function() {
119 "use strict";
124 "use strict";
120
125
121 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
126 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
122 if (!(this instanceof CommentForm)) {
127 if (!(this instanceof CommentForm)) {
123 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
128 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
124 }
129 }
125
130
126 // bind the element instance to our Form
131 // bind the element instance to our Form
127 $(formElement).get(0).CommentForm = this;
132 $(formElement).get(0).CommentForm = this;
128
133
129 this.withLineNo = function(selector) {
134 this.withLineNo = function(selector) {
130 var lineNo = this.lineNo;
135 var lineNo = this.lineNo;
131 if (lineNo === undefined) {
136 if (lineNo === undefined) {
132 return selector
137 return selector
133 } else {
138 } else {
134 return selector + '_' + lineNo;
139 return selector + '_' + lineNo;
135 }
140 }
136 };
141 };
137
142
138 this.commitId = commitId;
143 this.commitId = commitId;
139 this.pullRequestId = pullRequestId;
144 this.pullRequestId = pullRequestId;
140 this.lineNo = lineNo;
145 this.lineNo = lineNo;
141 this.initAutocompleteActions = initAutocompleteActions;
146 this.initAutocompleteActions = initAutocompleteActions;
142
147
143 this.previewButton = this.withLineNo('#preview-btn');
148 this.previewButton = this.withLineNo('#preview-btn');
144 this.previewContainer = this.withLineNo('#preview-container');
149 this.previewContainer = this.withLineNo('#preview-container');
145
150
146 this.previewBoxSelector = this.withLineNo('#preview-box');
151 this.previewBoxSelector = this.withLineNo('#preview-box');
147
152
148 this.editButton = this.withLineNo('#edit-btn');
153 this.editButton = this.withLineNo('#edit-btn');
149 this.editContainer = this.withLineNo('#edit-container');
154 this.editContainer = this.withLineNo('#edit-container');
150 this.cancelButton = this.withLineNo('#cancel-btn');
155 this.cancelButton = this.withLineNo('#cancel-btn');
151 this.commentType = this.withLineNo('#comment_type');
156 this.commentType = this.withLineNo('#comment_type');
152
157
153 this.resolvesId = null;
158 this.resolvesId = null;
154 this.resolvesActionId = null;
159 this.resolvesActionId = null;
155
160
156 this.cmBox = this.withLineNo('#text');
161 this.cmBox = this.withLineNo('#text');
157 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
162 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
158
163
159 this.statusChange = '#change_status';
164 this.statusChange = this.withLineNo('#change_status');
160
165
161 this.submitForm = formElement;
166 this.submitForm = formElement;
162 this.submitButton = $(this.submitForm).find('input[type="submit"]');
167 this.submitButton = $(this.submitForm).find('input[type="submit"]');
163 this.submitButtonText = this.submitButton.val();
168 this.submitButtonText = this.submitButton.val();
164
169
165 this.previewUrl = pyroutes.url('changeset_comment_preview',
170 this.previewUrl = pyroutes.url('changeset_comment_preview',
166 {'repo_name': templateContext.repo_name});
171 {'repo_name': templateContext.repo_name});
167
172
168 if (resolvesCommentId){
173 if (resolvesCommentId){
169 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
174 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
170 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
175 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
171 $(this.commentType).prop('disabled', true);
176 $(this.commentType).prop('disabled', true);
172 $(this.commentType).addClass('disabled');
177 $(this.commentType).addClass('disabled');
173
178
179 // disable select
180 setTimeout(function() {
181 $(self.statusChange).select2('readonly', true);
182 }, 10);
183
184
174 var resolvedInfo = (
185 var resolvedInfo = (
175 '<li class="">' +
186 '<li class="">' +
176 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
187 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
177 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
188 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
178 '</li>'
189 '</li>'
179 ).format(resolvesCommentId, _gettext('resolve comment'));
190 ).format(resolvesCommentId, _gettext('resolve comment'));
180 $(resolvedInfo).insertAfter($(this.commentType).parent());
191 $(resolvedInfo).insertAfter($(this.commentType).parent());
181 }
192 }
182
193
183 // based on commitId, or pullRequestId decide where do we submit
194 // based on commitId, or pullRequestId decide where do we submit
184 // out data
195 // out data
185 if (this.commitId){
196 if (this.commitId){
186 this.submitUrl = pyroutes.url('changeset_comment',
197 this.submitUrl = pyroutes.url('changeset_comment',
187 {'repo_name': templateContext.repo_name,
198 {'repo_name': templateContext.repo_name,
188 'revision': this.commitId});
199 'revision': this.commitId});
189 this.selfUrl = pyroutes.url('changeset_home',
200 this.selfUrl = pyroutes.url('changeset_home',
190 {'repo_name': templateContext.repo_name,
201 {'repo_name': templateContext.repo_name,
191 'revision': this.commitId});
202 'revision': this.commitId});
192
203
193 } else if (this.pullRequestId) {
204 } else if (this.pullRequestId) {
194 this.submitUrl = pyroutes.url('pullrequest_comment',
205 this.submitUrl = pyroutes.url('pullrequest_comment',
195 {'repo_name': templateContext.repo_name,
206 {'repo_name': templateContext.repo_name,
196 'pull_request_id': this.pullRequestId});
207 'pull_request_id': this.pullRequestId});
197 this.selfUrl = pyroutes.url('pullrequest_show',
208 this.selfUrl = pyroutes.url('pullrequest_show',
198 {'repo_name': templateContext.repo_name,
209 {'repo_name': templateContext.repo_name,
199 'pull_request_id': this.pullRequestId});
210 'pull_request_id': this.pullRequestId});
200
211
201 } else {
212 } else {
202 throw new Error(
213 throw new Error(
203 'CommentForm requires pullRequestId, or commitId to be specified.')
214 'CommentForm requires pullRequestId, or commitId to be specified.')
204 }
215 }
205
216
217 // FUNCTIONS and helpers
218 var self = this;
219
220 this.isInline = function(){
221 return this.lineNo && this.lineNo != 'general';
222 };
223
206 this.getCmInstance = function(){
224 this.getCmInstance = function(){
207 return this.cm
225 return this.cm
208 };
226 };
209
227
210 this.setPlaceholder = function(placeholder) {
228 this.setPlaceholder = function(placeholder) {
211 var cm = this.getCmInstance();
229 var cm = this.getCmInstance();
212 if (cm){
230 if (cm){
213 cm.setOption('placeholder', placeholder);
231 cm.setOption('placeholder', placeholder);
214 }
232 }
215 };
233 };
216
234
217 var self = this;
218
219 this.getCommentStatus = function() {
235 this.getCommentStatus = function() {
220 return $(this.submitForm).find(this.statusChange).val();
236 return $(this.submitForm).find(this.statusChange).val();
221 };
237 };
222 this.getCommentType = function() {
238 this.getCommentType = function() {
223 return $(this.submitForm).find(this.commentType).val();
239 return $(this.submitForm).find(this.commentType).val();
224 };
240 };
225
241
226 this.getResolvesId = function() {
242 this.getResolvesId = function() {
227 return $(this.submitForm).find(this.resolvesId).val() || null;
243 return $(this.submitForm).find(this.resolvesId).val() || null;
228 };
244 };
229 this.markCommentResolved = function(resolvedCommentId){
245 this.markCommentResolved = function(resolvedCommentId){
230 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
246 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
247 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
232 };
248 };
233
249
234 this.isAllowedToSubmit = function() {
250 this.isAllowedToSubmit = function() {
235 return !$(this.submitButton).prop('disabled');
251 return !$(this.submitButton).prop('disabled');
236 };
252 };
237
253
238 this.initStatusChangeSelector = function(){
254 this.initStatusChangeSelector = function(){
239 var formatChangeStatus = function(state, escapeMarkup) {
255 var formatChangeStatus = function(state, escapeMarkup) {
240 var originalOption = state.element;
256 var originalOption = state.element;
241 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
257 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
242 '<span>' + escapeMarkup(state.text) + '</span>';
258 '<span>' + escapeMarkup(state.text) + '</span>';
243 };
259 };
244 var formatResult = function(result, container, query, escapeMarkup) {
260 var formatResult = function(result, container, query, escapeMarkup) {
245 return formatChangeStatus(result, escapeMarkup);
261 return formatChangeStatus(result, escapeMarkup);
246 };
262 };
247
263
248 var formatSelection = function(data, container, escapeMarkup) {
264 var formatSelection = function(data, container, escapeMarkup) {
249 return formatChangeStatus(data, escapeMarkup);
265 return formatChangeStatus(data, escapeMarkup);
250 };
266 };
251
267
252 $(this.submitForm).find(this.statusChange).select2({
268 $(this.submitForm).find(this.statusChange).select2({
253 placeholder: _gettext('Status Review'),
269 placeholder: _gettext('Status Review'),
254 formatResult: formatResult,
270 formatResult: formatResult,
255 formatSelection: formatSelection,
271 formatSelection: formatSelection,
256 containerCssClass: "drop-menu status_box_menu",
272 containerCssClass: "drop-menu status_box_menu",
257 dropdownCssClass: "drop-menu-dropdown",
273 dropdownCssClass: "drop-menu-dropdown",
258 dropdownAutoWidth: true,
274 dropdownAutoWidth: true,
259 minimumResultsForSearch: -1
275 minimumResultsForSearch: -1
260 });
276 });
261 $(this.submitForm).find(this.statusChange).on('change', function() {
277 $(this.submitForm).find(this.statusChange).on('change', function() {
262 var status = self.getCommentStatus();
278 var status = self.getCommentStatus();
263 if (status && self.lineNo == 'general') {
279 if (status && !self.isInline()) {
264 $(self.submitButton).prop('disabled', false);
280 $(self.submitButton).prop('disabled', false);
265 }
281 }
266
282
267 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
283 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
268 self.setPlaceholder(placeholderText)
284 self.setPlaceholder(placeholderText)
269 })
285 })
270 };
286 };
271
287
272 // reset the comment form into it's original state
288 // reset the comment form into it's original state
273 this.resetCommentFormState = function(content) {
289 this.resetCommentFormState = function(content) {
274 content = content || '';
290 content = content || '';
275
291
276 $(this.editContainer).show();
292 $(this.editContainer).show();
277 $(this.editButton).parent().addClass('active');
293 $(this.editButton).parent().addClass('active');
278
294
279 $(this.previewContainer).hide();
295 $(this.previewContainer).hide();
280 $(this.previewButton).parent().removeClass('active');
296 $(this.previewButton).parent().removeClass('active');
281
297
282 this.setActionButtonsDisabled(true);
298 this.setActionButtonsDisabled(true);
283 self.cm.setValue(content);
299 self.cm.setValue(content);
284 self.cm.setOption("readOnly", false);
300 self.cm.setOption("readOnly", false);
285 };
301 };
286
302
287 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
303 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
288 failHandler = failHandler || function() {};
304 failHandler = failHandler || function() {};
289 var postData = toQueryString(postData);
305 var postData = toQueryString(postData);
290 var request = $.ajax({
306 var request = $.ajax({
291 url: url,
307 url: url,
292 type: 'POST',
308 type: 'POST',
293 data: postData,
309 data: postData,
294 headers: {'X-PARTIAL-XHR': true}
310 headers: {'X-PARTIAL-XHR': true}
295 })
311 })
296 .done(function(data) {
312 .done(function(data) {
297 successHandler(data);
313 successHandler(data);
298 })
314 })
299 .fail(function(data, textStatus, errorThrown){
315 .fail(function(data, textStatus, errorThrown){
300 alert(
316 alert(
301 "Error while submitting comment.\n" +
317 "Error while submitting comment.\n" +
302 "Error code {0} ({1}).".format(data.status, data.statusText));
318 "Error code {0} ({1}).".format(data.status, data.statusText));
303 failHandler()
319 failHandler()
304 });
320 });
305 return request;
321 return request;
306 };
322 };
307
323
308 // overwrite a submitHandler, we need to do it for inline comments
324 // overwrite a submitHandler, we need to do it for inline comments
309 this.setHandleFormSubmit = function(callback) {
325 this.setHandleFormSubmit = function(callback) {
310 this.handleFormSubmit = callback;
326 this.handleFormSubmit = callback;
311 };
327 };
312
328
313 // default handler for for submit for main comments
329 // default handler for for submit for main comments
314 this.handleFormSubmit = function() {
330 this.handleFormSubmit = function() {
315 var text = self.cm.getValue();
331 var text = self.cm.getValue();
316 var status = self.getCommentStatus();
332 var status = self.getCommentStatus();
317 var commentType = self.getCommentType();
333 var commentType = self.getCommentType();
318 var resolvesCommentId = self.getResolvesId();
334 var resolvesCommentId = self.getResolvesId();
319
335
320 if (text === "" && !status) {
336 if (text === "" && !status) {
321 return;
337 return;
322 }
338 }
323
339
324 var excludeCancelBtn = false;
340 var excludeCancelBtn = false;
325 var submitEvent = true;
341 var submitEvent = true;
326 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
342 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
327 self.cm.setOption("readOnly", true);
343 self.cm.setOption("readOnly", true);
344
328 var postData = {
345 var postData = {
329 'text': text,
346 'text': text,
330 'changeset_status': status,
347 'changeset_status': status,
331 'comment_type': commentType,
348 'comment_type': commentType,
332 'csrf_token': CSRF_TOKEN
349 'csrf_token': CSRF_TOKEN
333 };
350 };
334 if (resolvesCommentId){
351 if (resolvesCommentId){
335 postData['resolves_comment_id'] = resolvesCommentId;
352 postData['resolves_comment_id'] = resolvesCommentId;
336 }
353 }
337 var submitSuccessCallback = function(o) {
354 var submitSuccessCallback = function(o) {
338 if (status) {
355 if (status) {
339 location.reload(true);
356 location.reload(true);
340 } else {
357 } else {
341 $('#injected_page_comments').append(o.rendered_text);
358 $('#injected_page_comments').append(o.rendered_text);
342 self.resetCommentFormState();
359 self.resetCommentFormState();
343 bindDeleteCommentButtons();
360 bindDeleteCommentButtons();
344 timeagoActivate();
361 timeagoActivate();
345
362
346 //mark visually which comment was resolved
363 // mark visually which comment was resolved
347 if (resolvesCommentId) {
364 if (resolvesCommentId) {
348 this.markCommentResolved(resolvesCommentId);
365 self.markCommentResolved(resolvesCommentId);
349 }
366 }
350 }
367 }
351 };
368 };
352 var submitFailCallback = function(){
369 var submitFailCallback = function(){
353 self.resetCommentFormState(text);
370 self.resetCommentFormState(text);
354 };
371 };
355 self.submitAjaxPOST(
372 self.submitAjaxPOST(
356 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
373 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
357 };
374 };
358
375
359 this.previewSuccessCallback = function(o) {
376 this.previewSuccessCallback = function(o) {
360 $(self.previewBoxSelector).html(o);
377 $(self.previewBoxSelector).html(o);
361 $(self.previewBoxSelector).removeClass('unloaded');
378 $(self.previewBoxSelector).removeClass('unloaded');
362
379
363 // swap buttons, making preview active
380 // swap buttons, making preview active
364 $(self.previewButton).parent().addClass('active');
381 $(self.previewButton).parent().addClass('active');
365 $(self.editButton).parent().removeClass('active');
382 $(self.editButton).parent().removeClass('active');
366
383
367 // unlock buttons
384 // unlock buttons
368 self.setActionButtonsDisabled(false);
385 self.setActionButtonsDisabled(false);
369 };
386 };
370
387
371 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
388 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
372 excludeCancelBtn = excludeCancelBtn || false;
389 excludeCancelBtn = excludeCancelBtn || false;
373 submitEvent = submitEvent || false;
390 submitEvent = submitEvent || false;
374
391
375 $(this.editButton).prop('disabled', state);
392 $(this.editButton).prop('disabled', state);
376 $(this.previewButton).prop('disabled', state);
393 $(this.previewButton).prop('disabled', state);
377
394
378 if (!excludeCancelBtn) {
395 if (!excludeCancelBtn) {
379 $(this.cancelButton).prop('disabled', state);
396 $(this.cancelButton).prop('disabled', state);
380 }
397 }
381
398
382 var submitState = state;
399 var submitState = state;
383 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
400 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
384 // if the value of commit review status is set, we allow
401 // if the value of commit review status is set, we allow
385 // submit button, but only on Main form, lineNo means inline
402 // submit button, but only on Main form, lineNo means inline
386 submitState = false
403 submitState = false
387 }
404 }
388 $(this.submitButton).prop('disabled', submitState);
405 $(this.submitButton).prop('disabled', submitState);
389 if (submitEvent) {
406 if (submitEvent) {
390 $(this.submitButton).val(_gettext('Submitting...'));
407 $(this.submitButton).val(_gettext('Submitting...'));
391 } else {
408 } else {
392 $(this.submitButton).val(this.submitButtonText);
409 $(this.submitButton).val(this.submitButtonText);
393 }
410 }
394
411
395 };
412 };
396
413
397 // lock preview/edit/submit buttons on load, but exclude cancel button
414 // lock preview/edit/submit buttons on load, but exclude cancel button
398 var excludeCancelBtn = true;
415 var excludeCancelBtn = true;
399 this.setActionButtonsDisabled(true, excludeCancelBtn);
416 this.setActionButtonsDisabled(true, excludeCancelBtn);
400
417
401 // anonymous users don't have access to initialized CM instance
418 // anonymous users don't have access to initialized CM instance
402 if (this.cm !== undefined){
419 if (this.cm !== undefined){
403 this.cm.on('change', function(cMirror) {
420 this.cm.on('change', function(cMirror) {
404 if (cMirror.getValue() === "") {
421 if (cMirror.getValue() === "") {
405 self.setActionButtonsDisabled(true, excludeCancelBtn)
422 self.setActionButtonsDisabled(true, excludeCancelBtn)
406 } else {
423 } else {
407 self.setActionButtonsDisabled(false, excludeCancelBtn)
424 self.setActionButtonsDisabled(false, excludeCancelBtn)
408 }
425 }
409 });
426 });
410 }
427 }
411
428
412 $(this.editButton).on('click', function(e) {
429 $(this.editButton).on('click', function(e) {
413 e.preventDefault();
430 e.preventDefault();
414
431
415 $(self.previewButton).parent().removeClass('active');
432 $(self.previewButton).parent().removeClass('active');
416 $(self.previewContainer).hide();
433 $(self.previewContainer).hide();
417
434
418 $(self.editButton).parent().addClass('active');
435 $(self.editButton).parent().addClass('active');
419 $(self.editContainer).show();
436 $(self.editContainer).show();
420
437
421 });
438 });
422
439
423 $(this.previewButton).on('click', function(e) {
440 $(this.previewButton).on('click', function(e) {
424 e.preventDefault();
441 e.preventDefault();
425 var text = self.cm.getValue();
442 var text = self.cm.getValue();
426
443
427 if (text === "") {
444 if (text === "") {
428 return;
445 return;
429 }
446 }
430
447
431 var postData = {
448 var postData = {
432 'text': text,
449 'text': text,
433 'renderer': templateContext.visual.default_renderer,
450 'renderer': templateContext.visual.default_renderer,
434 'csrf_token': CSRF_TOKEN
451 'csrf_token': CSRF_TOKEN
435 };
452 };
436
453
437 // lock ALL buttons on preview
454 // lock ALL buttons on preview
438 self.setActionButtonsDisabled(true);
455 self.setActionButtonsDisabled(true);
439
456
440 $(self.previewBoxSelector).addClass('unloaded');
457 $(self.previewBoxSelector).addClass('unloaded');
441 $(self.previewBoxSelector).html(_gettext('Loading ...'));
458 $(self.previewBoxSelector).html(_gettext('Loading ...'));
442
459
443 $(self.editContainer).hide();
460 $(self.editContainer).hide();
444 $(self.previewContainer).show();
461 $(self.previewContainer).show();
445
462
446 // by default we reset state of comment preserving the text
463 // by default we reset state of comment preserving the text
447 var previewFailCallback = function(){
464 var previewFailCallback = function(){
448 self.resetCommentFormState(text)
465 self.resetCommentFormState(text)
449 };
466 };
450 self.submitAjaxPOST(
467 self.submitAjaxPOST(
451 self.previewUrl, postData, self.previewSuccessCallback,
468 self.previewUrl, postData, self.previewSuccessCallback,
452 previewFailCallback);
469 previewFailCallback);
453
470
454 $(self.previewButton).parent().addClass('active');
471 $(self.previewButton).parent().addClass('active');
455 $(self.editButton).parent().removeClass('active');
472 $(self.editButton).parent().removeClass('active');
456 });
473 });
457
474
458 $(this.submitForm).submit(function(e) {
475 $(this.submitForm).submit(function(e) {
459 e.preventDefault();
476 e.preventDefault();
460 var allowedToSubmit = self.isAllowedToSubmit();
477 var allowedToSubmit = self.isAllowedToSubmit();
461 if (!allowedToSubmit){
478 if (!allowedToSubmit){
462 return false;
479 return false;
463 }
480 }
464 self.handleFormSubmit();
481 self.handleFormSubmit();
465 });
482 });
466
483
467 }
484 }
468
485
469 return CommentForm;
486 return CommentForm;
470 });
487 });
471
488
472 /* comments controller */
489 /* comments controller */
473 var CommentsController = function() {
490 var CommentsController = function() {
474 var mainComment = '#text';
491 var mainComment = '#text';
475 var self = this;
492 var self = this;
476
493
477 this.cancelComment = function(node) {
494 this.cancelComment = function(node) {
478 var $node = $(node);
495 var $node = $(node);
479 var $td = $node.closest('td');
496 var $td = $node.closest('td');
480 $node.closest('.comment-inline-form').remove();
497 $node.closest('.comment-inline-form').remove();
481 return false;
498 return false;
482 };
499 };
483
500
484 this.getLineNumber = function(node) {
501 this.getLineNumber = function(node) {
485 var $node = $(node);
502 var $node = $(node);
486 return $node.closest('td').attr('data-line-number');
503 return $node.closest('td').attr('data-line-number');
487 };
504 };
488
505
489 this.scrollToComment = function(node, offset, outdated) {
506 this.scrollToComment = function(node, offset, outdated) {
490 var outdated = outdated || false;
507 var outdated = outdated || false;
491 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
508 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
492
509
493 if (!node) {
510 if (!node) {
494 node = $('.comment-selected');
511 node = $('.comment-selected');
495 if (!node.length) {
512 if (!node.length) {
496 node = $('comment-current')
513 node = $('comment-current')
497 }
514 }
498 }
515 }
499 $comment = $(node).closest(klass);
516 $comment = $(node).closest(klass);
500 $comments = $(klass);
517 $comments = $(klass);
501
518
502 $('.comment-selected').removeClass('comment-selected');
519 $('.comment-selected').removeClass('comment-selected');
503
520
504 var nextIdx = $(klass).index($comment) + offset;
521 var nextIdx = $(klass).index($comment) + offset;
505 if (nextIdx >= $comments.length) {
522 if (nextIdx >= $comments.length) {
506 nextIdx = 0;
523 nextIdx = 0;
507 }
524 }
508 var $next = $(klass).eq(nextIdx);
525 var $next = $(klass).eq(nextIdx);
509 var $cb = $next.closest('.cb');
526 var $cb = $next.closest('.cb');
510 $cb.removeClass('cb-collapsed');
527 $cb.removeClass('cb-collapsed');
511
528
512 var $filediffCollapseState = $cb.closest('.filediff').prev();
529 var $filediffCollapseState = $cb.closest('.filediff').prev();
513 $filediffCollapseState.prop('checked', false);
530 $filediffCollapseState.prop('checked', false);
514 $next.addClass('comment-selected');
531 $next.addClass('comment-selected');
515 scrollToElement($next);
532 scrollToElement($next);
516 return false;
533 return false;
517 };
534 };
518
535
519 this.nextComment = function(node) {
536 this.nextComment = function(node) {
520 return self.scrollToComment(node, 1);
537 return self.scrollToComment(node, 1);
521 };
538 };
522
539
523 this.prevComment = function(node) {
540 this.prevComment = function(node) {
524 return self.scrollToComment(node, -1);
541 return self.scrollToComment(node, -1);
525 };
542 };
526
543
527 this.nextOutdatedComment = function(node) {
544 this.nextOutdatedComment = function(node) {
528 return self.scrollToComment(node, 1, true);
545 return self.scrollToComment(node, 1, true);
529 };
546 };
530
547
531 this.prevOutdatedComment = function(node) {
548 this.prevOutdatedComment = function(node) {
532 return self.scrollToComment(node, -1, true);
549 return self.scrollToComment(node, -1, true);
533 };
550 };
534
551
535 this.deleteComment = function(node) {
552 this.deleteComment = function(node) {
536 if (!confirm(_gettext('Delete this comment?'))) {
553 if (!confirm(_gettext('Delete this comment?'))) {
537 return false;
554 return false;
538 }
555 }
539 var $node = $(node);
556 var $node = $(node);
540 var $td = $node.closest('td');
557 var $td = $node.closest('td');
541 var $comment = $node.closest('.comment');
558 var $comment = $node.closest('.comment');
542 var comment_id = $comment.attr('data-comment-id');
559 var comment_id = $comment.attr('data-comment-id');
543 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
560 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
544 var postData = {
561 var postData = {
545 '_method': 'delete',
562 '_method': 'delete',
546 'csrf_token': CSRF_TOKEN
563 'csrf_token': CSRF_TOKEN
547 };
564 };
548
565
549 $comment.addClass('comment-deleting');
566 $comment.addClass('comment-deleting');
550 $comment.hide('fast');
567 $comment.hide('fast');
551
568
552 var success = function(response) {
569 var success = function(response) {
553 $comment.remove();
570 $comment.remove();
554 return false;
571 return false;
555 };
572 };
556 var failure = function(data, textStatus, xhr) {
573 var failure = function(data, textStatus, xhr) {
557 alert("error processing request: " + textStatus);
574 alert("error processing request: " + textStatus);
558 $comment.show('fast');
575 $comment.show('fast');
559 $comment.removeClass('comment-deleting');
576 $comment.removeClass('comment-deleting');
560 return false;
577 return false;
561 };
578 };
562 ajaxPOST(url, postData, success, failure);
579 ajaxPOST(url, postData, success, failure);
563 };
580 };
564
581
565 this.toggleWideMode = function (node) {
582 this.toggleWideMode = function (node) {
566 if ($('#content').hasClass('wrapper')) {
583 if ($('#content').hasClass('wrapper')) {
567 $('#content').removeClass("wrapper");
584 $('#content').removeClass("wrapper");
568 $('#content').addClass("wide-mode-wrapper");
585 $('#content').addClass("wide-mode-wrapper");
569 $(node).addClass('btn-success');
586 $(node).addClass('btn-success');
570 } else {
587 } else {
571 $('#content').removeClass("wide-mode-wrapper");
588 $('#content').removeClass("wide-mode-wrapper");
572 $('#content').addClass("wrapper");
589 $('#content').addClass("wrapper");
573 $(node).removeClass('btn-success');
590 $(node).removeClass('btn-success');
574 }
591 }
575 return false;
592 return false;
576 };
593 };
577
594
578 this.toggleComments = function(node, show) {
595 this.toggleComments = function(node, show) {
579 var $filediff = $(node).closest('.filediff');
596 var $filediff = $(node).closest('.filediff');
580 if (show === true) {
597 if (show === true) {
581 $filediff.removeClass('hide-comments');
598 $filediff.removeClass('hide-comments');
582 } else if (show === false) {
599 } else if (show === false) {
583 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
600 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
584 $filediff.addClass('hide-comments');
601 $filediff.addClass('hide-comments');
585 } else {
602 } else {
586 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
603 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
587 $filediff.toggleClass('hide-comments');
604 $filediff.toggleClass('hide-comments');
588 }
605 }
589 return false;
606 return false;
590 };
607 };
591
608
592 this.toggleLineComments = function(node) {
609 this.toggleLineComments = function(node) {
593 self.toggleComments(node, true);
610 self.toggleComments(node, true);
594 var $node = $(node);
611 var $node = $(node);
595 $node.closest('tr').toggleClass('hide-line-comments');
612 $node.closest('tr').toggleClass('hide-line-comments');
596 };
613 };
597
614
615 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
616 var pullRequestId = templateContext.pull_request_data.pull_request_id;
617 var commitId = templateContext.commit_data.commit_id;
618
619 var commentForm = new CommentForm(
620 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
621 var cm = commentForm.getCmInstance();
622
623 if (resolvesCommentId){
624 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
625 }
626
627 setTimeout(function() {
628 // callbacks
629 if (cm !== undefined) {
630 commentForm.setPlaceholder(placeholderText);
631 if (commentForm.isInline()) {
632 cm.focus();
633 cm.refresh();
634 }
635 }
636 }, 10);
637
638 // trigger scrolldown to the resolve comment, since it might be away
639 // from the clicked
640 if (resolvesCommentId){
641 var actionNode = $(commentForm.resolvesActionId).offset();
642
643 setTimeout(function() {
644 if (actionNode) {
645 $('body, html').animate({scrollTop: actionNode.top}, 10);
646 }
647 }, 100);
648 }
649
650 return commentForm;
651 };
652
653 this.createGeneralComment = function(lineNo, placeholderText, resolvesCommentId){
654
655 var tmpl = $('#cb-comment-general-form-template').html();
656 tmpl = tmpl.format(null, 'general');
657 var $form = $(tmpl);
658
659 var curForm = $('#cb-comment-general-form-placeholder').find('form');
660 if (curForm){
661 curForm.remove();
662 }
663 $('#cb-comment-general-form-placeholder').append($form);
664
665 var _form = $($form[0]);
666 var commentForm = this.createCommentForm(
667 _form, lineNo, placeholderText, true, resolvesCommentId);
668 commentForm.initStatusChangeSelector();
669 };
670
598 this.createComment = function(node, resolutionComment) {
671 this.createComment = function(node, resolutionComment) {
599 var resolvesCommentId = resolutionComment || null;
672 var resolvesCommentId = resolutionComment || null;
600 var $node = $(node);
673 var $node = $(node);
601 var $td = $node.closest('td');
674 var $td = $node.closest('td');
602 var $form = $td.find('.comment-inline-form');
675 var $form = $td.find('.comment-inline-form');
603
676
604 if (!$form.length) {
677 if (!$form.length) {
605 var tmpl = $('#cb-comment-inline-form-template').html();
678
606 var $filediff = $node.closest('.filediff');
679 var $filediff = $node.closest('.filediff');
607 $filediff.removeClass('hide-comments');
680 $filediff.removeClass('hide-comments');
608 var f_path = $filediff.attr('data-f-path');
681 var f_path = $filediff.attr('data-f-path');
609 var lineno = self.getLineNumber(node);
682 var lineno = self.getLineNumber(node);
610
683 // create a new HTML from template
684 var tmpl = $('#cb-comment-inline-form-template').html();
611 tmpl = tmpl.format(f_path, lineno);
685 tmpl = tmpl.format(f_path, lineno);
612 $form = $(tmpl);
686 $form = $(tmpl);
613
687
614 var $comments = $td.find('.inline-comments');
688 var $comments = $td.find('.inline-comments');
615 if (!$comments.length) {
689 if (!$comments.length) {
616 $comments = $(
690 $comments = $(
617 $('#cb-comments-inline-container-template').html());
691 $('#cb-comments-inline-container-template').html());
618 $td.append($comments);
692 $td.append($comments);
619 }
693 }
620
694
621 $td.find('.cb-comment-add-button').before($form);
695 $td.find('.cb-comment-add-button').before($form);
622
696
623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
697 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
624 var commitId = templateContext.commit_data.commit_id;
625 var _form = $($form[0]).find('form');
698 var _form = $($form[0]).find('form');
626
699
627 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false, resolvesCommentId);
700 var commentForm = this.createCommentForm(
628 var cm = commentForm.getCmInstance();
701 _form, lineno, placeholderText, false, resolvesCommentId);
702
703 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
704 form: _form,
705 parent: $td[0],
706 lineno: lineno,
707 f_path: f_path}
708 );
629
709
630 // set a CUSTOM submit handler for inline comments.
710 // set a CUSTOM submit handler for inline comments.
631 commentForm.setHandleFormSubmit(function(o) {
711 commentForm.setHandleFormSubmit(function(o) {
632 var text = commentForm.cm.getValue();
712 var text = commentForm.cm.getValue();
633 var commentType = commentForm.getCommentType();
713 var commentType = commentForm.getCommentType();
634 var resolvesCommentId = commentForm.getResolvesId();
714 var resolvesCommentId = commentForm.getResolvesId();
635
715
636 if (text === "") {
716 if (text === "") {
637 return;
717 return;
638 }
718 }
639
719
640 if (lineno === undefined) {
720 if (lineno === undefined) {
641 alert('missing line !');
721 alert('missing line !');
642 return;
722 return;
643 }
723 }
644 if (f_path === undefined) {
724 if (f_path === undefined) {
645 alert('missing file path !');
725 alert('missing file path !');
646 return;
726 return;
647 }
727 }
648
728
649 var excludeCancelBtn = false;
729 var excludeCancelBtn = false;
650 var submitEvent = true;
730 var submitEvent = true;
651 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
731 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
652 commentForm.cm.setOption("readOnly", true);
732 commentForm.cm.setOption("readOnly", true);
653 var postData = {
733 var postData = {
654 'text': text,
734 'text': text,
655 'f_path': f_path,
735 'f_path': f_path,
656 'line': lineno,
736 'line': lineno,
657 'comment_type': commentType,
737 'comment_type': commentType,
658 'csrf_token': CSRF_TOKEN
738 'csrf_token': CSRF_TOKEN
659 };
739 };
660 if (resolvesCommentId){
740 if (resolvesCommentId){
661 postData['resolves_comment_id'] = resolvesCommentId;
741 postData['resolves_comment_id'] = resolvesCommentId;
662 }
742 }
663
743
664 var submitSuccessCallback = function(json_data) {
744 var submitSuccessCallback = function(json_data) {
665 $form.remove();
745 $form.remove();
666 try {
746 try {
667 var html = json_data.rendered_text;
747 var html = json_data.rendered_text;
668 var lineno = json_data.line_no;
748 var lineno = json_data.line_no;
669 var target_id = json_data.target_id;
749 var target_id = json_data.target_id;
670
750
671 $comments.find('.cb-comment-add-button').before(html);
751 $comments.find('.cb-comment-add-button').before(html);
672
752
673 //mark visually which comment was resolved
753 //mark visually which comment was resolved
674 if (resolvesCommentId) {
754 if (resolvesCommentId) {
675 commentForm.markCommentResolved(resolvesCommentId);
755 commentForm.markCommentResolved(resolvesCommentId);
676 }
756 }
677
757
678 } catch (e) {
758 } catch (e) {
679 console.error(e);
759 console.error(e);
680 }
760 }
681
761
682 // re trigger the linkification of next/prev navigation
762 // re trigger the linkification of next/prev navigation
683 linkifyComments($('.inline-comment-injected'));
763 linkifyComments($('.inline-comment-injected'));
684 timeagoActivate();
764 timeagoActivate();
685 bindDeleteCommentButtons();
765 bindDeleteCommentButtons();
686 commentForm.setActionButtonsDisabled(false);
766 commentForm.setActionButtonsDisabled(false);
687
767
688 };
768 };
689 var submitFailCallback = function(){
769 var submitFailCallback = function(){
690 commentForm.resetCommentFormState(text)
770 commentForm.resetCommentFormState(text)
691 };
771 };
692 commentForm.submitAjaxPOST(
772 commentForm.submitAjaxPOST(
693 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
773 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
694 });
774 });
695
775
696 if (resolvesCommentId){
697 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
698
699 } else {
700 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
701 }
702
703 setTimeout(function() {
704 // callbacks
705 if (cm !== undefined) {
706 commentForm.setPlaceholder(placeholderText);
707 cm.focus();
708 cm.refresh();
709 }
710 }, 10);
711
712 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
713 form: _form,
714 parent: $td[0],
715 lineno: lineno,
716 f_path: f_path}
717 );
718
719 // trigger hash
720 if (resolvesCommentId){
721 var resolveAction = $(commentForm.resolvesActionId);
722 setTimeout(function() {
723 $('body, html').animate({ scrollTop: resolveAction.offset().top }, 10);
724 }, 100);
725 }
726 }
776 }
727
777
728 $form.addClass('comment-inline-form-open');
778 $form.addClass('comment-inline-form-open');
729 };
779 };
730
780
731 this.createResolutionComment = function(commentId){
781 this.createResolutionComment = function(commentId){
732 // hide the trigger text
782 // hide the trigger text
733 $('#resolve-comment-{0}'.format(commentId)).hide();
783 $('#resolve-comment-{0}'.format(commentId)).hide();
734
784
735 var comment = $('#comment-'+commentId);
785 var comment = $('#comment-'+commentId);
736 var commentData = comment.data();
786 var commentData = comment.data();
737
738 if (commentData.commentInline) {
787 if (commentData.commentInline) {
739 var resolutionComment = true;
740 this.createComment(comment, commentId)
788 this.createComment(comment, commentId)
741 } else {
789 } else {
742
790 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
743 this.createComment(comment, commentId)
744
745 console.log('TODO')
746 console.log(commentId)
747 }
791 }
748
792
749 return false;
793 return false;
750 };
794 };
751
795
752 this.submitResolution = function(commentId){
796 this.submitResolution = function(commentId){
753 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
797 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
754 var commentForm = form.get(0).CommentForm;
798 var commentForm = form.get(0).CommentForm;
755
799
756 var cm = commentForm.getCmInstance();
800 var cm = commentForm.getCmInstance();
757 var renderer = templateContext.visual.default_renderer;
801 var renderer = templateContext.visual.default_renderer;
758 if (renderer == 'rst'){
802 if (renderer == 'rst'){
759 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
803 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
760 } else if (renderer == 'markdown') {
804 } else if (renderer == 'markdown') {
761 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
805 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
762 } else {
806 } else {
763 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
807 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
764 }
808 }
765
809
766 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
810 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
767 form.submit();
811 form.submit();
768 return false;
812 return false;
769 };
813 };
770
814
771 this.renderInlineComments = function(file_comments) {
815 this.renderInlineComments = function(file_comments) {
772 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
816 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
773
817
774 for (var i = 0; i < file_comments.length; i++) {
818 for (var i = 0; i < file_comments.length; i++) {
775 var box = file_comments[i];
819 var box = file_comments[i];
776
820
777 var target_id = $(box).attr('target_id');
821 var target_id = $(box).attr('target_id');
778
822
779 // actually comments with line numbers
823 // actually comments with line numbers
780 var comments = box.children;
824 var comments = box.children;
781
825
782 for (var j = 0; j < comments.length; j++) {
826 for (var j = 0; j < comments.length; j++) {
783 var data = {
827 var data = {
784 'rendered_text': comments[j].outerHTML,
828 'rendered_text': comments[j].outerHTML,
785 'line_no': $(comments[j]).attr('line'),
829 'line_no': $(comments[j]).attr('line'),
786 'target_id': target_id
830 'target_id': target_id
787 };
831 };
788 }
832 }
789 }
833 }
790
834
791 // since order of injection is random, we're now re-iterating
835 // since order of injection is random, we're now re-iterating
792 // from correct order and filling in links
836 // from correct order and filling in links
793 linkifyComments($('.inline-comment-injected'));
837 linkifyComments($('.inline-comment-injected'));
794 bindDeleteCommentButtons();
838 bindDeleteCommentButtons();
795 firefoxAnchorFix();
839 firefoxAnchorFix();
796 };
840 };
797
841
798 };
842 };
@@ -1,353 +1,348 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 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
11
11
12 <div class="comment
12 <div class="comment
13 ${'comment-inline' if inline else 'comment-general'}
13 ${'comment-inline' if inline else 'comment-general'}
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 id="comment-${comment.comment_id}"
15 id="comment-${comment.comment_id}"
16 line="${comment.line_no}"
16 line="${comment.line_no}"
17 data-comment-id="${comment.comment_id}"
17 data-comment-id="${comment.comment_id}"
18 data-comment-type="${comment.comment_type}"
18 data-comment-type="${comment.comment_type}"
19 data-comment-inline=${h.json.dumps(inline)}
19 data-comment-inline=${h.json.dumps(inline)}
20 style="${'display: none;' if outdated_at_ver else ''}">
20 style="${'display: none;' if outdated_at_ver else ''}">
21
21
22 <div class="meta">
22 <div class="meta">
23 <div class="comment-type-label">
23 <div class="comment-type-label">
24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
25 % if comment.comment_type == 'todo':
25 % if comment.comment_type == 'todo':
26 % if comment.resolved:
26 % if comment.resolved:
27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
29 </div>
29 </div>
30 % else:
30 % else:
31 <div class="resolved tooltip" style="display: none">
31 <div class="resolved tooltip" style="display: none">
32 <span>${comment.comment_type}</span>
32 <span>${comment.comment_type}</span>
33 </div>
33 </div>
34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
35 ${comment.comment_type}
35 ${comment.comment_type}
36 </div>
36 </div>
37 % endif
37 % endif
38 % else:
38 % else:
39 % if comment.resolved_comment:
39 % if comment.resolved_comment:
40 fix
40 fix
41 % else:
41 % else:
42 ${comment.comment_type or 'note'}
42 ${comment.comment_type or 'note'}
43 % endif
43 % endif
44 % endif
44 % endif
45 </div>
45 </div>
46 </div>
46 </div>
47
47
48 <div class="author ${'author-inline' if inline else 'author-general'}">
48 <div class="author ${'author-inline' if inline else 'author-general'}">
49 ${base.gravatar_with_user(comment.author.email, 16)}
49 ${base.gravatar_with_user(comment.author.email, 16)}
50 </div>
50 </div>
51 <div class="date">
51 <div class="date">
52 ${h.age_component(comment.modified_at, time_is_local=True)}
52 ${h.age_component(comment.modified_at, time_is_local=True)}
53 </div>
53 </div>
54 % if inline:
54 % if inline:
55 <span></span>
55 <span></span>
56 % else:
56 % else:
57 <div class="status-change">
57 <div class="status-change">
58 % if comment.pull_request:
58 % if comment.pull_request:
59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
60 % if comment.status_change:
60 % if comment.status_change:
61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
62 % else:
62 % else:
63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
64 % endif
64 % endif
65 </a>
65 </a>
66 % else:
66 % else:
67 % if comment.status_change:
67 % if comment.status_change:
68 ${_('Status change on commit')}:
68 ${_('Status change on commit')}:
69 % endif
69 % endif
70 % endif
70 % endif
71 </div>
71 </div>
72 % endif
72 % endif
73
73
74 % if comment.status_change:
74 % if comment.status_change:
75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
76 <div title="${_('Commit status')}" class="changeset-status-lbl">
76 <div title="${_('Commit status')}" class="changeset-status-lbl">
77 ${comment.status_change[0].status_lbl}
77 ${comment.status_change[0].status_lbl}
78 </div>
78 </div>
79 % endif
79 % endif
80
80
81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
82
82
83 <div class="comment-links-block">
83 <div class="comment-links-block">
84
84
85 % if inline:
85 % if inline:
86 % if outdated_at_ver:
86 % if outdated_at_ver:
87 <div class="pr-version-inline">
87 <div class="pr-version-inline">
88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
89 <code class="pr-version-num">
89 <code class="pr-version-num">
90 outdated ${'v{}'.format(pr_index_ver)}
90 outdated ${'v{}'.format(pr_index_ver)}
91 </code>
91 </code>
92 </a>
92 </a>
93 </div>
93 </div>
94 |
94 |
95 % endif
95 % endif
96 % else:
96 % else:
97 % if comment.pull_request_version_id and pr_index_ver:
97 % if comment.pull_request_version_id and pr_index_ver:
98 |
98 |
99 <div class="pr-version">
99 <div class="pr-version">
100 % if comment.outdated:
100 % if comment.outdated:
101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
103 </a>
103 </a>
104 % else:
104 % else:
105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
106 <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)}">
106 <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)}">
107 <code class="pr-version-num">
107 <code class="pr-version-num">
108 ${'v{}'.format(pr_index_ver)}
108 ${'v{}'.format(pr_index_ver)}
109 </code>
109 </code>
110 </a>
110 </a>
111 </div>
111 </div>
112 % endif
112 % endif
113 </div>
113 </div>
114 % endif
114 % endif
115 % endif
115 % endif
116
116
117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
120 ## permissions to delete
120 ## permissions to delete
121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
122 ## TODO: dan: add edit comment here
122 ## TODO: dan: add edit comment here
123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
124 %else:
124 %else:
125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
126 %endif
126 %endif
127 %else:
127 %else:
128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
129 %endif
129 %endif
130
130
131 %if not outdated_at_ver:
131 %if not outdated_at_ver:
132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
134 %endif
134 %endif
135
135
136 </div>
136 </div>
137 </div>
137 </div>
138 <div class="text">
138 <div class="text">
139 ${comment.render(mentions=True)|n}
139 ${comment.render(mentions=True)|n}
140 </div>
140 </div>
141
141
142 </div>
142 </div>
143 </%def>
143 </%def>
144
144
145 ## generate main comments
145 ## generate main comments
146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
147 <div id="comments">
147 <div id="comments">
148 %for comment in c.comments:
148 %for comment in c.comments:
149 <div id="comment-tr-${comment.comment_id}">
149 <div id="comment-tr-${comment.comment_id}">
150 ## only render comments that are not from pull request, or from
150 ## only render comments that are not from pull request, or from
151 ## pull request and a status change
151 ## pull request and a status change
152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
153 ${comment_block(comment)}
153 ${comment_block(comment)}
154 %endif
154 %endif
155 </div>
155 </div>
156 %endfor
156 %endfor
157 ## to anchor ajax comments
157 ## to anchor ajax comments
158 <div id="injected_page_comments"></div>
158 <div id="injected_page_comments"></div>
159 </div>
159 </div>
160 </%def>
160 </%def>
161
161
162
162
163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
164
164
165 ## merge status, and merge action
165 ## merge status, and merge action
166 %if is_pull_request:
166 %if is_pull_request:
167 <div class="pull-request-merge">
167 <div class="pull-request-merge">
168 %if c.allowed_to_merge:
168 %if c.allowed_to_merge:
169 <div class="pull-request-wrap">
169 <div class="pull-request-wrap">
170 <div class="pull-right">
170 <div class="pull-right">
171 ${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')}
171 ${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')}
172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
175 ${h.end_form()}
175 ${h.end_form()}
176 </div>
176 </div>
177 </div>
177 </div>
178 %else:
178 %else:
179 <div class="pull-request-wrap">
179 <div class="pull-request-wrap">
180 <div class="pull-right">
180 <div class="pull-right">
181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
182 </div>
182 </div>
183 </div>
183 </div>
184 %endif
184 %endif
185 </div>
185 </div>
186 %endif
186 %endif
187
187
188 <div class="comments">
188 <div class="comments">
189 <%
189 <%
190 if is_pull_request:
190 if is_pull_request:
191 placeholder = _('Leave a comment on this Pull Request.')
191 placeholder = _('Leave a comment on this Pull Request.')
192 elif is_compare:
192 elif is_compare:
193 placeholder = _('Leave a comment on all commits in this range.')
193 placeholder = _('Leave a comment on all commits in this range.')
194 else:
194 else:
195 placeholder = _('Leave a comment on this Commit.')
195 placeholder = _('Leave a comment on this Commit.')
196 %>
196 %>
197
197
198 % if c.rhodecode_user.username != h.DEFAULT_USER:
198 % if c.rhodecode_user.username != h.DEFAULT_USER:
199 <div class="comment-form ac">
199 <div class="js-template" id="cb-comment-general-form-template">
200 ## template generated for injection
201 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 </div>
203
204 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
200 ## inject form here
205 ## inject form here
201 ${comment_form(form_type='general', form_id='general_comment', lineno_id='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 </div>
206 </div>
203 <script type="text/javascript">
207 <script type="text/javascript">
204 // init active elements of commentForm
205 var commitId = templateContext.commit_data.commit_id;
206 var pullRequestId = templateContext.pull_request_data.pull_request_id;
207 var lineNo = 'general';
208 var lineNo = 'general';
208 var resolvesCommitId = null;
209 var resolvesCommentId = null;
209
210 Rhodecode.comments.createGeneralComment(lineNo, "${placeholder}", resolvesCommentId)
210 var mainCommentForm = new CommentForm(
211 "#general_comment", commitId, pullRequestId, lineNo, true, resolvesCommitId);
212 mainCommentForm.setPlaceholder("${placeholder}");
213 mainCommentForm.initStatusChangeSelector();
214 </script>
211 </script>
215
216
217 % else:
212 % else:
218 ## form state when not logged in
213 ## form state when not logged in
219 <div class="comment-form ac">
214 <div class="comment-form ac">
220
215
221 <div class="comment-area">
216 <div class="comment-area">
222 <div class="comment-area-header">
217 <div class="comment-area-header">
223 <ul class="nav-links clearfix">
218 <ul class="nav-links clearfix">
224 <li class="active">
219 <li class="active">
225 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
220 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
226 </li>
221 </li>
227 <li class="">
222 <li class="">
228 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
223 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
229 </li>
224 </li>
230 </ul>
225 </ul>
231 </div>
226 </div>
232
227
233 <div class="comment-area-write" style="display: block;">
228 <div class="comment-area-write" style="display: block;">
234 <div id="edit-container">
229 <div id="edit-container">
235 <div style="padding: 40px 0">
230 <div style="padding: 40px 0">
236 ${_('You need to be logged in to leave comments.')}
231 ${_('You need to be logged in to leave comments.')}
237 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
232 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
238 </div>
233 </div>
239 </div>
234 </div>
240 <div id="preview-container" class="clearfix" style="display: none;">
235 <div id="preview-container" class="clearfix" style="display: none;">
241 <div id="preview-box" class="preview-box"></div>
236 <div id="preview-box" class="preview-box"></div>
242 </div>
237 </div>
243 </div>
238 </div>
244
239
245 <div class="comment-area-footer">
240 <div class="comment-area-footer">
246 <div class="toolbar">
241 <div class="toolbar">
247 <div class="toolbar-text">
242 <div class="toolbar-text">
248 </div>
243 </div>
249 </div>
244 </div>
250 </div>
245 </div>
251 </div>
246 </div>
252
247
253 <div class="comment-footer">
248 <div class="comment-footer">
254 </div>
249 </div>
255
250
256 </div>
251 </div>
257 % endif
252 % endif
258
253
259 <script type="text/javascript">
254 <script type="text/javascript">
260 bindToggleButtons();
255 bindToggleButtons();
261 </script>
256 </script>
262 </div>
257 </div>
263 </%def>
258 </%def>
264
259
265
260
266 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
261 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
267 ## comment injected based on assumption that user is logged in
262 ## comment injected based on assumption that user is logged in
268
263
269 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
264 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
270
265
271 <div class="comment-area">
266 <div class="comment-area">
272 <div class="comment-area-header">
267 <div class="comment-area-header">
273 <ul class="nav-links clearfix">
268 <ul class="nav-links clearfix">
274 <li class="active">
269 <li class="active">
275 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
270 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
276 </li>
271 </li>
277 <li class="">
272 <li class="">
278 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
273 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
279 </li>
274 </li>
280 <li class="pull-right">
275 <li class="pull-right">
281 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
276 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
282 % for val in c.visual.comment_types:
277 % for val in c.visual.comment_types:
283 <option value="${val}">${val.upper()}</option>
278 <option value="${val}">${val.upper()}</option>
284 % endfor
279 % endfor
285 </select>
280 </select>
286 </li>
281 </li>
287 </ul>
282 </ul>
288 </div>
283 </div>
289
284
290 <div class="comment-area-write" style="display: block;">
285 <div class="comment-area-write" style="display: block;">
291 <div id="edit-container_${lineno_id}">
286 <div id="edit-container_${lineno_id}">
292 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
287 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
293 </div>
288 </div>
294 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
289 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
295 <div id="preview-box_${lineno_id}" class="preview-box"></div>
290 <div id="preview-box_${lineno_id}" class="preview-box"></div>
296 </div>
291 </div>
297 </div>
292 </div>
298
293
299 <div class="comment-area-footer">
294 <div class="comment-area-footer">
300 <div class="toolbar">
295 <div class="toolbar">
301 <div class="toolbar-text">
296 <div class="toolbar-text">
302 ${(_('Comments parsed using %s syntax with %s support.') % (
297 ${(_('Comments parsed using %s syntax with %s support.') % (
303 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
298 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
304 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
299 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
305 )
300 )
306 )|n}
301 )|n}
307 </div>
302 </div>
308 </div>
303 </div>
309 </div>
304 </div>
310 </div>
305 </div>
311
306
312 <div class="comment-footer">
307 <div class="comment-footer">
313
308
314 % if review_statuses:
309 % if review_statuses:
315 <div class="status_box">
310 <div class="status_box">
316 <select id="change_status" name="changeset_status">
311 <select id="change_status_${lineno_id}" name="changeset_status">
317 <option></option> ## Placeholder
312 <option></option> ## Placeholder
318 % for status, lbl in review_statuses:
313 % for status, lbl in review_statuses:
319 <option value="${status}" data-status="${status}">${lbl}</option>
314 <option value="${status}" data-status="${status}">${lbl}</option>
320 %if is_pull_request and change_status and status in ('approved', 'rejected'):
315 %if is_pull_request and change_status and status in ('approved', 'rejected'):
321 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
316 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
322 %endif
317 %endif
323 % endfor
318 % endfor
324 </select>
319 </select>
325 </div>
320 </div>
326 % endif
321 % endif
327
322
328 ## inject extra inputs into the form
323 ## inject extra inputs into the form
329 % if form_extras and isinstance(form_extras, (list, tuple)):
324 % if form_extras and isinstance(form_extras, (list, tuple)):
330 <div id="comment_form_extras">
325 <div id="comment_form_extras">
331 % for form_ex_el in form_extras:
326 % for form_ex_el in form_extras:
332 ${form_ex_el|n}
327 ${form_ex_el|n}
333 % endfor
328 % endfor
334 </div>
329 </div>
335 % endif
330 % endif
336
331
337 <div class="action-buttons">
332 <div class="action-buttons">
338 ## inline for has a file, and line-number together with cancel hide button.
333 ## inline for has a file, and line-number together with cancel hide button.
339 % if form_type == 'inline':
334 % if form_type == 'inline':
340 <input type="hidden" name="f_path" value="{0}">
335 <input type="hidden" name="f_path" value="{0}">
341 <input type="hidden" name="line" value="${lineno_id}">
336 <input type="hidden" name="line" value="${lineno_id}">
342 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
337 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
343 ${_('Cancel')}
338 ${_('Cancel')}
344 </button>
339 </button>
345 % endif
340 % endif
346 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
341 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
347
342
348 </div>
343 </div>
349 </div>
344 </div>
350
345
351 </form>
346 </form>
352
347
353 </%def> No newline at end of file
348 </%def>
@@ -1,964 +1,964 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/debug_style/index.html"/>
2 <%inherit file="/debug_style/index.html"/>
3
3
4 <%def name="breadcrumbs_links()">
4 <%def name="breadcrumbs_links()">
5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
6 &raquo;
6 &raquo;
7 ${c.active}
7 ${c.active}
8 </%def>
8 </%def>
9
9
10
10
11 <%def name="real_main()">
11 <%def name="real_main()">
12 <div class="box">
12 <div class="box">
13 <div class="title">
13 <div class="title">
14 ${self.breadcrumbs()}
14 ${self.breadcrumbs()}
15 </div>
15 </div>
16
16
17 <div class='sidebar-col-wrapper'>
17 <div class='sidebar-col-wrapper'>
18 ${self.sidebar()}
18 ${self.sidebar()}
19
19
20 <div class="main-content">
20 <div class="main-content">
21
21
22 <h2>Collapsable Content</h2>
22 <h2>Collapsable Content</h2>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
24
24
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
26 </p>
26 </p>
27 <p>Notes:</p>
27 <p>Notes:</p>
28 <ul>
28 <ul>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
31 </ul>
31 </ul>
32
32
33 </div> <!-- .main-content -->
33 </div> <!-- .main-content -->
34 </div> <!-- .sidebar-col-wrapper -->
34 </div> <!-- .sidebar-col-wrapper -->
35 </div> <!-- .box -->
35 </div> <!-- .box -->
36
36
37 <!-- CONTENT -->
37 <!-- CONTENT -->
38 <div id="content" class="wrapper">
38 <div id="content" class="wrapper">
39
39
40 <div class="main">
40 <div class="main">
41
41
42 <div class="box">
42 <div class="box">
43 <div class="title">
43 <div class="title">
44 <h1>
44 <h1>
45 Diff: enable filename with spaces on diffs
45 Diff: enable filename with spaces on diffs
46 </h1>
46 </h1>
47 <h1>
47 <h1>
48 <i class="icon-hg" ></i>
48 <i class="icon-hg" ></i>
49
49
50 <i class="icon-lock"></i>
50 <i class="icon-lock"></i>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
52
52
53 </h1>
53 </h1>
54 </div>
54 </div>
55
55
56 <div class="box pr-summary">
56 <div class="box pr-summary">
57 <div class="summary-details block-left">
57 <div class="summary-details block-left">
58
58
59 <div class="pr-details-title">
59 <div class="pr-details-title">
60
60
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
62 <div class="btn-collapse" data-toggle="description">Show More</div>
62 <div class="btn-collapse" data-toggle="description">Show More</div>
63 </div>
63 </div>
64 <div id="summary" class="fields pr-details-content">
64 <div id="summary" class="fields pr-details-content">
65 <div class="field">
65 <div class="field">
66 <div class="label-summary">
66 <div class="label-summary">
67 <label>Origin:</label>
67 <label>Origin:</label>
68 </div>
68 </div>
69 <div class="input">
69 <div class="input">
70 <div>
70 <div>
71 <span class="tag">
71 <span class="tag">
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
73 </span>
73 </span>
74 <span class="clone-url">
74 <span class="clone-url">
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
76 </span>
76 </span>
77 </div>
77 </div>
78 <div>
78 <div>
79 <br>
79 <br>
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
81 </div>
81 </div>
82 </div>
82 </div>
83 </div>
83 </div>
84 <div class="field">
84 <div class="field">
85 <div class="label-summary">
85 <div class="label-summary">
86 <label>Review:</label>
86 <label>Review:</label>
87 </div>
87 </div>
88 <div class="input">
88 <div class="input">
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
91 Under Review
91 Under Review
92 </span>
92 </span>
93
93
94 </div>
94 </div>
95 </div>
95 </div>
96 <div class="field collapsable-content" data-toggle="description">
96 <div class="field collapsable-content" data-toggle="description">
97 <div class="label-summary">
97 <div class="label-summary">
98 <label>Description:</label>
98 <label>Description:</label>
99 </div>
99 </div>
100 <div class="input">
100 <div class="input">
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
102 </div>
102 </div>
103 </div>
103 </div>
104 <div class="field collapsable-content" data-toggle="description">
104 <div class="field collapsable-content" data-toggle="description">
105 <div class="label-summary">
105 <div class="label-summary">
106 <label>Comments:</label>
106 <label>Comments:</label>
107 </div>
107 </div>
108 <div class="input">
108 <div class="input">
109 <div>
109 <div>
110 <div class="comments-number">
110 <div class="comments-number">
111 <a href="#inline-comments-container">0 Pull request comments</a>,
111 <a href="#inline-comments-container">0 Pull request comments</a>,
112 0 Inline Comments
112 0 Inline Comments
113 </div>
113 </div>
114 </div>
114 </div>
115 </div>
115 </div>
116 </div>
116 </div>
117 </div>
117 </div>
118 </div>
118 </div>
119 <div>
119 <div>
120 <div class="reviewers-title block-right">
120 <div class="reviewers-title block-right">
121 <div class="pr-details-title">
121 <div class="pr-details-title">
122 Author
122 Author
123 </div>
123 </div>
124 </div>
124 </div>
125 <div class="block-right pr-details-content reviewers">
125 <div class="block-right pr-details-content reviewers">
126 <ul class="group_members">
126 <ul class="group_members">
127 <li>
127 <li>
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
130 </li>
130 </li>
131 </ul>
131 </ul>
132 </div>
132 </div>
133 <div class="reviewers-title block-right">
133 <div class="reviewers-title block-right">
134 <div class="pr-details-title">
134 <div class="pr-details-title">
135 Pull request reviewers
135 Pull request reviewers
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
137 </div>
137 </div>
138
138
139 </div>
139 </div>
140 <div id="reviewers" class="block-right pr-details-content reviewers">
140 <div id="reviewers" class="block-right pr-details-content reviewers">
141
141
142 <ul id="review_members" class="group_members">
142 <ul id="review_members" class="group_members">
143 <li id="reviewer_70">
143 <li id="reviewer_70">
144 <div class="reviewers_member">
144 <div class="reviewers_member">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
147 </div>
147 </div>
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 </div>
150 </div>
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
153 <i class="icon-remove-sign"></i>
153 <i class="icon-remove-sign"></i>
154 </div>
154 </div>
155 </li>
155 </li>
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
157 <div class="reviewers_member">
157 <div class="reviewers_member">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
159 <div class="flag_status approved pull-left reviewer_member_status"></div>
160 </div>
160 </div>
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
163 </div>
163 </div>
164 </li>
164 </li>
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
166 <div class="reviewers_member">
166 <div class="reviewers_member">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
169 </div>
169 </div>
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
172 </div>
172 </div>
173 </li>
173 </li>
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
175 <div class="reviewers_member">
175 <div class="reviewers_member">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
177 <div class="flag_status approved pull-left reviewer_member_status"></div>
178 </div>
178 </div>
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
181 </div>
181 </div>
182 </li>
182 </li>
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
184 <div class="reviewers_member">
184 <div class="reviewers_member">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
187 </div>
187 </div>
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
190 </div>
190 </div>
191 </li>
191 </li>
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
193 <div class="reviewers_member">
193 <div class="reviewers_member">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
195 <div class="flag_status approved pull-left reviewer_member_status"></div>
196 </div>
196 </div>
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
199 </div>
199 </div>
200 </li>
200 </li>
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
202 <div class="reviewers_member">
202 <div class="reviewers_member">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
204 <div class="flag_status approved pull-left reviewer_member_status"></div>
205 </div>
205 </div>
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
208 </div>
208 </div>
209 </li>
209 </li>
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
211 <div class="reviewers_member">
211 <div class="reviewers_member">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
214 </div>
214 </div>
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
217 </div>
217 </div>
218 </li>
218 </li>
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
220 <div class="reviewers_member">
220 <div class="reviewers_member">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
222 <div class="flag_status approved pull-left reviewer_member_status"></div>
223 </div>
223 </div>
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
226 </div>
226 </div>
227 </li>
227 </li>
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
229 <div class="reviewers_member">
229 <div class="reviewers_member">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
232 </div>
232 </div>
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
235 </div>
235 </div>
236 </li>
236 </li>
237 </ul>
237 </ul>
238 <div id="add_reviewer_input" class="ac" style="display: none;">
238 <div id="add_reviewer_input" class="ac" style="display: none;">
239 </div>
239 </div>
240 </div>
240 </div>
241 </div>
241 </div>
242 </div>
242 </div>
243 </div>
243 </div>
244 <div class="box">
244 <div class="box">
245 <div class="table" >
245 <div class="table" >
246 <div id="changeset_compare_view_content">
246 <div id="changeset_compare_view_content">
247 <div class="compare_view_commits_title">
247 <div class="compare_view_commits_title">
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
249
249
250 </div>
250 </div>
251 <div class="container">
251 <div class="container">
252
252
253
253
254 <table class="rctable compare_view_commits">
254 <table class="rctable compare_view_commits">
255 <tr>
255 <tr>
256 <th>Time</th>
256 <th>Time</th>
257 <th>Author</th>
257 <th>Author</th>
258 <th>Commit</th>
258 <th>Commit</th>
259 <th></th>
259 <th></th>
260 <th>Title</th>
260 <th>Title</th>
261 </tr>
261 </tr>
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
263 <td class="td-time">
263 <td class="td-time">
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
265 </td>
265 </td>
266 <td class="td-user">
266 <td class="td-user">
267 <div class="gravatar_with_user">
267 <div class="gravatar_with_user">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
270 </div>
270 </div>
271 </td>
271 </td>
272 <td class="td-hash">
272 <td class="td-hash">
273 <code>
273 <code>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
275 </code>
275 </code>
276 </td>
276 </td>
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
278 <div class="show_more_col">
278 <div class="show_more_col">
279 <i class="show_more"></i>
279 <i class="show_more"></i>
280 </div>
280 </div>
281 </td>
281 </td>
282 <td class="mid td-description">
282 <td class="mid td-description">
283 <div class="log-container truncate-wrap">
283 <div class="log-container truncate-wrap">
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
285 </div>
285 </div>
286 </td>
286 </td>
287 </tr>
287 </tr>
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
289 <td class="td-time">
289 <td class="td-time">
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
291 </td>
291 </td>
292 <td class="td-user">
292 <td class="td-user">
293 <div class="gravatar_with_user">
293 <div class="gravatar_with_user">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
296 </div>
296 </div>
297 </td>
297 </td>
298 <td class="td-hash">
298 <td class="td-hash">
299 <code>
299 <code>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
301 </code>
301 </code>
302 </td>
302 </td>
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
304 <div class="show_more_col">
304 <div class="show_more_col">
305 <i class="show_more"></i>
305 <i class="show_more"></i>
306 </div>
306 </div>
307 </td>
307 </td>
308 <td class="mid td-description">
308 <td class="mid td-description">
309 <div class="log-container truncate-wrap">
309 <div class="log-container truncate-wrap">
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
311 </div>
311 </div>
312 </td>
312 </td>
313 </tr>
313 </tr>
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
315 <td class="td-time">
315 <td class="td-time">
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
317 </td>
317 </td>
318 <td class="td-user">
318 <td class="td-user">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
321 </span>
321 </span>
322 <span class="author">brian (Brian Butler)</span>
322 <span class="author">brian (Brian Butler)</span>
323 </td>
323 </td>
324 <td class="td-hash">
324 <td class="td-hash">
325 <code>
325 <code>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
327 </code>
327 </code>
328 </td>
328 </td>
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
330 <div class="show_more_col">
330 <div class="show_more_col">
331 <i class="show_more"></i>
331 <i class="show_more"></i>
332 </div>
332 </div>
333 </td>
333 </td>
334 <td class="mid td-description">
334 <td class="mid td-description">
335 <div class="log-container truncate-wrap">
335 <div class="log-container truncate-wrap">
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
337 </div>
337 </div>
338 </td>
338 </td>
339 </tr>
339 </tr>
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
341 <td class="td-time">
341 <td class="td-time">
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
343 </td>
343 </td>
344 <td class="td-user">
344 <td class="td-user">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
347 </span>
347 </span>
348 <span class="author">brian (Brian Butler)</span>
348 <span class="author">brian (Brian Butler)</span>
349 </td>
349 </td>
350 <td class="td-hash">
350 <td class="td-hash">
351 <code>
351 <code>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
353 </code>
353 </code>
354 </td>
354 </td>
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
356 <div class="show_more_col">
356 <div class="show_more_col">
357 <i class="show_more"></i>
357 <i class="show_more"></i>
358 </div>
358 </div>
359 </td>
359 </td>
360 <td class="mid td-description">
360 <td class="mid td-description">
361 <div class="log-container truncate-wrap">
361 <div class="log-container truncate-wrap">
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
363 </div>
363 </div>
364 </td>
364 </td>
365 </tr>
365 </tr>
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
367 <td class="td-time">
367 <td class="td-time">
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
369 </td>
369 </td>
370 <td class="td-user">
370 <td class="td-user">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
373 </span>
373 </span>
374 <span class="author">anderson (Anderson Santos)</span>
374 <span class="author">anderson (Anderson Santos)</span>
375 </td>
375 </td>
376 <td class="td-hash">
376 <td class="td-hash">
377 <code>
377 <code>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
379 </code>
379 </code>
380 </td>
380 </td>
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
382 <div class="show_more_col">
382 <div class="show_more_col">
383 <i class="show_more" ></i>
383 <i class="show_more" ></i>
384 </div>
384 </div>
385 </td>
385 </td>
386 <td class="mid td-description">
386 <td class="mid td-description">
387 <div class="log-container truncate-wrap">
387 <div class="log-container truncate-wrap">
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
389
389
390 </div>
390 </div>
391 </td>
391 </td>
392 </tr>
392 </tr>
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
394 <td class="td-time">
394 <td class="td-time">
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
396 </td>
396 </td>
397 <td class="td-user">
397 <td class="td-user">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
400 </span>
400 </span>
401 <span class="author">anderson (Anderson Santos)</span>
401 <span class="author">anderson (Anderson Santos)</span>
402 </td>
402 </td>
403 <td class="td-hash">
403 <td class="td-hash">
404 <code>
404 <code>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
406 </code>
406 </code>
407 </td>
407 </td>
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
409 <div class="show_more_col">
409 <div class="show_more_col">
410 <i class="show_more" ></i>
410 <i class="show_more" ></i>
411 </div>
411 </div>
412 </td>
412 </td>
413 <td class="mid td-description">
413 <td class="mid td-description">
414 <div class="log-container truncate-wrap">
414 <div class="log-container truncate-wrap">
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
416
416
417 </div>
417 </div>
418 </td>
418 </td>
419 </tr>
419 </tr>
420 </table>
420 </table>
421 </div>
421 </div>
422
422
423 <script>
423 <script>
424 $('.expand_commit').on('click',function(e){
424 $('.expand_commit').on('click',function(e){
425 $(this).children('i').hide();
425 $(this).children('i').hide();
426 var cid = $(this).data('commitId');
426 var cid = $(this).data('commitId');
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
429 });
429 });
430 $('.compare_select').on('click',function(e){
430 $('.compare_select').on('click',function(e){
431 var cid = $(this).attr('commit_id');
431 var cid = $(this).attr('commit_id');
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
433 });
433 });
434 </script>
434 </script>
435 <div class="cs_files_title">
435 <div class="cs_files_title">
436 <span class="cs_files_expand">
436 <span class="cs_files_expand">
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
438 </span>
438 </span>
439 <h2>
439 <h2>
440 7 files changed: 55 inserted, 9 deleted
440 7 files changed: 55 inserted, 9 deleted
441 </h2>
441 </h2>
442 </div>
442 </div>
443 <div class="cs_files">
443 <div class="cs_files">
444 <table class="compare_view_files">
444 <table class="compare_view_files">
445
445
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
447 <td class="cs_icon_td">
447 <td class="cs_icon_td">
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
449 </td>
449 </td>
450 <td class="cs_icon_td">
450 <td class="cs_icon_td">
451 <div class="flag_status not_reviewed hidden"></div>
451 <div class="flag_status not_reviewed hidden"></div>
452 </td>
452 </td>
453 <td id="a_c--efbe5b7a3f13">
453 <td id="a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
456 </a>
456 </a>
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
459 Unified Diff
459 Unified Diff
460 </a>
460 </a>
461 |
461 |
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
463 Side-by-side Diff
463 Side-by-side Diff
464 </a>
464 </a>
465 </span>
465 </span>
466 </td>
466 </td>
467 <td>
467 <td>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
470 <i class="icon-comment"></i>
470 <i class="icon-comment"></i>
471 </div>
471 </div>
472 </td>
472 </td>
473 </tr>
473 </tr>
474 <tr id="tr_c--efbe5b7a3f13">
474 <tr id="tr_c--efbe5b7a3f13">
475 <td></td>
475 <td></td>
476 <td></td>
476 <td></td>
477 <td class="injected_diff" colspan="2">
477 <td class="injected_diff" colspan="2">
478
478
479 <div class="diff-container" id="diff-container-140716195039928">
479 <div class="diff-container" id="diff-container-140716195039928">
480 <div id="c--efbe5b7a3f13_target" ></div>
480 <div id="c--efbe5b7a3f13_target" ></div>
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
482 <div class="code-body">
482 <div class="code-body">
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
484 <table class="code-difftable">
484 <table class="code-difftable">
485 <tr class="line context">
485 <tr class="line context">
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
489 <td class="code no-comment">
489 <td class="code no-comment">
490 <pre>new file 100644</pre>
490 <pre>new file 100644</pre>
491 </td>
491 </td>
492 </tr>
492 </tr>
493 <tr class="line add">
493 <tr class="line add">
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
497 <td class="code">
497 <td class="code">
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
499 </pre>
499 </pre>
500 </td>
500 </td>
501 </tr>
501 </tr>
502 <tr class="line add">
502 <tr class="line add">
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
506 <td class="code">
506 <td class="code">
507 <pre>similarity index 100%
507 <pre>similarity index 100%
508 </pre>
508 </pre>
509 </td>
509 </td>
510 </tr>
510 </tr>
511 <tr class="line add">
511 <tr class="line add">
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
515 <td class="code">
515 <td class="code">
516 <pre>rename from file_with_ spaces.txt
516 <pre>rename from file_with_ spaces.txt
517 </pre>
517 </pre>
518 </td>
518 </td>
519 </tr>
519 </tr>
520 <tr class="line add">
520 <tr class="line add">
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
524 <td class="code">
524 <td class="code">
525 <pre>rename to file_with_ two spaces.txt
525 <pre>rename to file_with_ two spaces.txt
526 </pre>
526 </pre>
527 </td>
527 </td>
528 </tr>
528 </tr>
529 <tr class="line context">
529 <tr class="line context">
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
533 <td class="code no-comment">
533 <td class="code no-comment">
534 <pre> No newline at end of file</pre>
534 <pre> No newline at end of file</pre>
535 </td>
535 </td>
536 </tr>
536 </tr>
537 </table>
537 </table>
538 </div>
538 </div>
539 </div>
539 </div>
540 </div>
540 </div>
541
541
542 </td>
542 </td>
543 </tr>
543 </tr>
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
545 <td class="cs_icon_td">
545 <td class="cs_icon_td">
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
547 </td>
547 </td>
548 <td class="cs_icon_td">
548 <td class="cs_icon_td">
549 <div class="flag_status not_reviewed hidden"></div>
549 <div class="flag_status not_reviewed hidden"></div>
550 </td>
550 </td>
551 <td id="a_c--c21377f778f9">
551 <td id="a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
554 </a>
554 </a>
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
557 Unified Diff
557 Unified Diff
558 </a>
558 </a>
559 |
559 |
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
561 Side-by-side Diff
561 Side-by-side Diff
562 </a>
562 </a>
563 </span>
563 </span>
564 </td>
564 </td>
565 <td>
565 <td>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
568 <i class="icon-comment"></i>
568 <i class="icon-comment"></i>
569 </div>
569 </div>
570 </td>
570 </td>
571 </tr>
571 </tr>
572 <tr id="tr_c--c21377f778f9">
572 <tr id="tr_c--c21377f778f9">
573 <td></td>
573 <td></td>
574 <td></td>
574 <td></td>
575 <td class="injected_diff" colspan="2">
575 <td class="injected_diff" colspan="2">
576
576
577 <div class="diff-container" id="diff-container-140716195038344">
577 <div class="diff-container" id="diff-container-140716195038344">
578 <div id="c--c21377f778f9_target" ></div>
578 <div id="c--c21377f778f9_target" ></div>
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
579 <div id="c--c21377f778f9" class="diffblock margined comm" >
580 <div class="code-body">
580 <div class="code-body">
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
582 <table class="code-difftable">
582 <table class="code-difftable">
583 <tr class="line context">
583 <tr class="line context">
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
587 <td class="code no-comment">
587 <td class="code no-comment">
588 <pre>new file 100644</pre>
588 <pre>new file 100644</pre>
589 </td>
589 </td>
590 </tr>
590 </tr>
591 <tr class="line add">
591 <tr class="line add">
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
595 <td class="code">
595 <td class="code">
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
597 </pre>
597 </pre>
598 </td>
598 </td>
599 </tr>
599 </tr>
600 <tr class="line add">
600 <tr class="line add">
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
604 <td class="code">
604 <td class="code">
605 <pre>copy from file_changed_without_spaces.txt
605 <pre>copy from file_changed_without_spaces.txt
606 </pre>
606 </pre>
607 </td>
607 </td>
608 </tr>
608 </tr>
609 <tr class="line add">
609 <tr class="line add">
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
613 <td class="code">
613 <td class="code">
614 <pre>copy to file_copied_ with spaces.txt
614 <pre>copy to file_copied_ with spaces.txt
615 </pre>
615 </pre>
616 </td>
616 </td>
617 </tr>
617 </tr>
618 <tr class="line context">
618 <tr class="line context">
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
622 <td class="code no-comment">
622 <td class="code no-comment">
623 <pre> No newline at end of file</pre>
623 <pre> No newline at end of file</pre>
624 </td>
624 </td>
625 </tr>
625 </tr>
626 </table>
626 </table>
627 </div>
627 </div>
628 </div>
628 </div>
629 </div>
629 </div>
630
630
631 </td>
631 </td>
632 </tr>
632 </tr>
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
634 <td class="cs_icon_td">
634 <td class="cs_icon_td">
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
636 </td>
636 </td>
637 <td class="cs_icon_td">
637 <td class="cs_icon_td">
638 <div class="flag_status not_reviewed hidden"></div>
638 <div class="flag_status not_reviewed hidden"></div>
639 </td>
639 </td>
640 <td id="a_c--ee62085ad7a8">
640 <td id="a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
643 </a>
643 </a>
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
646 Unified Diff
646 Unified Diff
647 </a>
647 </a>
648 |
648 |
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
650 Side-by-side Diff
650 Side-by-side Diff
651 </a>
651 </a>
652 </span>
652 </span>
653 </td>
653 </td>
654 <td>
654 <td>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
657 <i class="icon-comment"></i>
657 <i class="icon-comment"></i>
658 </div>
658 </div>
659 </td>
659 </td>
660 </tr>
660 </tr>
661 <tr id="tr_c--ee62085ad7a8">
661 <tr id="tr_c--ee62085ad7a8">
662 <td></td>
662 <td></td>
663 <td></td>
663 <td></td>
664 <td class="injected_diff" colspan="2">
664 <td class="injected_diff" colspan="2">
665
665
666 <div class="diff-container" id="diff-container-140716195039496">
666 <div class="diff-container" id="diff-container-140716195039496">
667 <div id="c--ee62085ad7a8_target" ></div>
667 <div id="c--ee62085ad7a8_target" ></div>
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
669 <div class="code-body">
669 <div class="code-body">
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
671 <table class="code-difftable">
671 <table class="code-difftable">
672 <tr class="line context">
672 <tr class="line context">
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
676 <td class="code no-comment">
676 <td class="code no-comment">
677 <pre>new file 100644</pre>
677 <pre>new file 100644</pre>
678 </td>
678 </td>
679 </tr>
679 </tr>
680 <tr class="line add">
680 <tr class="line add">
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
684 <td class="code">
684 <td class="code">
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
686 </pre>
686 </pre>
687 </td>
687 </td>
688 </tr>
688 </tr>
689 <tr class="line add">
689 <tr class="line add">
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
693 <td class="code">
693 <td class="code">
694 <pre>rename from file_ with update.txt
694 <pre>rename from file_ with update.txt
695 </pre>
695 </pre>
696 </td>
696 </td>
697 </tr>
697 </tr>
698 <tr class="line add">
698 <tr class="line add">
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
702 <td class="code">
702 <td class="code">
703 <pre>rename to file_changed _.txt</pre>
703 <pre>rename to file_changed _.txt</pre>
704 </td>
704 </td>
705 </tr>
705 </tr>
706 </table>
706 </table>
707 </div>
707 </div>
708 </div>
708 </div>
709 </div>
709 </div>
710
710
711 </td>
711 </td>
712 </tr>
712 </tr>
713
713
714 </table>
714 </table>
715 </div>
715 </div>
716 </div>
716 </div>
717 </div>
717 </div>
718
718
719 </td>
719 </td>
720 </tr>
720 </tr>
721 </table>
721 </table>
722 </div>
722 </div>
723 </div>
723 </div>
724 </div>
724 </div>
725
725
726
726
727
727
728
728
729 <div id="comment-inline-form-template" style="display: none;">
729 <div id="comment-inline-form-template" style="display: none;">
730 <div class="comment-inline-form ac">
730 <div class="comment-inline-form ac">
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
732 <form action="#" class="inline-form" method="get">
732 <form action="#" class="inline-form" method="get">
733 <div id="edit-container_{1}" class="clearfix">
733 <div id="edit-container_{1}" class="clearfix">
734 <div class="comment-title pull-left">
734 <div class="comment-title pull-left">
735 Commenting on line {1}.
735 Commenting on line {1}.
736 </div>
736 </div>
737 <div class="comment-help pull-right">
737 <div class="comment-help pull-right">
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
739 </div>
739 </div>
740 <div style="clear: both"></div>
740 <div style="clear: both"></div>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
742 </div>
742 </div>
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
744 <div class="comment-help">
744 <div class="comment-help">
745 Comment preview
745 Comment preview
746 </div>
746 </div>
747 <div id="preview-box_{1}" class="preview-box"></div>
747 <div id="preview-box_{1}" class="preview-box"></div>
748 </div>
748 </div>
749 <div class="comment-button pull-right">
749 <div class="comment-button pull-right">
750 <input type="hidden" name="f_path" value="{0}">
750 <input type="hidden" name="f_path" value="{0}">
751 <input type="hidden" name="line" value="{1}">
751 <input type="hidden" name="line" value="{1}">
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
755 </div>
755 </div>
756 <div class="comment-button hide-inline-form-button">
756 <div class="comment-button hide-inline-form-button">
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
758 </div>
758 </div>
759 </form>
759 </form>
760 </div>
760 </div>
761 </div>
761 </div>
762
762
763
763
764
764
765 <div class="comments">
765 <div class="comments">
766 <div id="inline-comments-container">
766 <div id="inline-comments-container">
767
767
768 <h2>0 Pull Request Comments</h2>
768 <h2>0 Pull Request Comments</h2>
769
769
770
770
771 </div>
771 </div>
772
772
773 </div>
773 </div>
774
774
775
775
776
776
777
777
778 <div class="pull-request-merge">
778 <div class="pull-request-merge">
779 </div>
779 </div>
780 <div class="comments">
780 <div class="comments">
781 <div class="comment-form ac">
781 <div class="comment-form ac">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
784 <div id="edit-container" class="clearfix">
784 <div id="edit-container" class="clearfix">
785 <div class="comment-title pull-left">
785 <div class="comment-title pull-left">
786 Create a comment on this Pull Request.
786 Create a comment on this Pull Request.
787 </div>
787 </div>
788 <div class="comment-help pull-right">
788 <div class="comment-help pull-right">
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
790 </div>
790 </div>
791 <div style="clear: both"></div>
791 <div style="clear: both"></div>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
793 </div>
793 </div>
794
794
795 <div id="preview-container" class="clearfix" style="display: none;">
795 <div id="preview-container" class="clearfix" style="display: none;">
796 <div class="comment-title">
796 <div class="comment-title">
797 Comment preview
797 Comment preview
798 </div>
798 </div>
799 <div id="preview-box" class="preview-box"></div>
799 <div id="preview-box" class="preview-box"></div>
800 </div>
800 </div>
801
801
802 <div id="comment_form_extras">
802 <div id="comment_form_extras">
803 </div>
803 </div>
804 <div class="action-button pull-right">
804 <div class="action-button pull-right">
805 <div id="preview-btn" class="btn">
805 <div id="preview-btn" class="btn">
806 Preview
806 Preview
807 </div>
807 </div>
808 <div id="edit-btn" class="btn" style="display: none;">
808 <div id="edit-btn" class="btn" style="display: none;">
809 Edit
809 Edit
810 </div>
810 </div>
811 <div class="comment-button">
811 <div class="comment-button">
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
813 </div>
813 </div>
814 </div>
814 </div>
815 </form>
815 </form>
816 </div>
816 </div>
817 </div>
817 </div>
818 <script>
818 <script>
819
819
820 $(document).ready(function() {
820 $(document).ready(function() {
821
821
822 var cm = initCommentBoxCodeMirror('#text');
822 var cm = initCommentBoxCodeMirror('#text');
823
823
824 // main form preview
824 // main form preview
825 $('#preview-btn').on('click', function(e) {
825 $('#preview-btn').on('click', function(e) {
826 $('#preview-btn').hide();
826 $('#preview-btn').hide();
827 $('#edit-btn').show();
827 $('#edit-btn').show();
828 var _text = cm.getValue();
828 var _text = cm.getValue();
829 if (!_text) {
829 if (!_text) {
830 return;
830 return;
831 }
831 }
832 var post_data = {
832 var post_data = {
833 'text': _text,
833 'text': _text,
834 'renderer': DEFAULT_RENDERER,
834 'renderer': DEFAULT_RENDERER,
835 'csrf_token': CSRF_TOKEN
835 'csrf_token': CSRF_TOKEN
836 };
836 };
837 var previewbox = $('#preview-box');
837 var previewbox = $('#preview-box');
838 previewbox.addClass('unloaded');
838 previewbox.addClass('unloaded');
839 previewbox.html(_gettext('Loading ...'));
839 previewbox.html(_gettext('Loading ...'));
840 $('#edit-container').hide();
840 $('#edit-container').hide();
841 $('#preview-container').show();
841 $('#preview-container').show();
842
842
843 var url = pyroutes.url('changeset_comment_preview', {'repo_name': 'rhodecode-momentum'});
843 var url = pyroutes.url('changeset_comment_preview', {'repo_name': 'rhodecode-momentum'});
844
844
845 ajaxPOST(url, post_data, function(o) {
845 ajaxPOST(url, post_data, function(o) {
846 previewbox.html(o);
846 previewbox.html(o);
847 previewbox.removeClass('unloaded');
847 previewbox.removeClass('unloaded');
848 });
848 });
849 });
849 });
850 $('#edit-btn').on('click', function(e) {
850 $('#edit-btn').on('click', function(e) {
851 $('#preview-btn').show();
851 $('#preview-btn').show();
852 $('#edit-btn').hide();
852 $('#edit-btn').hide();
853 $('#edit-container').show();
853 $('#edit-container').show();
854 $('#preview-container').hide();
854 $('#preview-container').hide();
855 });
855 });
856
856
857 var formatChangeStatus = function(state, escapeMarkup) {
857 var formatChangeStatus = function(state, escapeMarkup) {
858 var originalOption = state.element;
858 var originalOption = state.element;
859 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
859 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
860 '<span>' + escapeMarkup(state.text) + '</span>';
860 '<span>' + escapeMarkup(state.text) + '</span>';
861 };
861 };
862
862
863 var formatResult = function(result, container, query, escapeMarkup) {
863 var formatResult = function(result, container, query, escapeMarkup) {
864 return formatChangeStatus(result, escapeMarkup);
864 return formatChangeStatus(result, escapeMarkup);
865 };
865 };
866
866
867 var formatSelection = function(data, container, escapeMarkup) {
867 var formatSelection = function(data, container, escapeMarkup) {
868 return formatChangeStatus(data, escapeMarkup);
868 return formatChangeStatus(data, escapeMarkup);
869 };
869 };
870
870
871 $('#change_status').select2({
871 $('#change_status_general').select2({
872 placeholder: "Status Review",
872 placeholder: "Status Review",
873 formatResult: formatResult,
873 formatResult: formatResult,
874 formatSelection: formatSelection,
874 formatSelection: formatSelection,
875 containerCssClass: "drop-menu status_box_menu",
875 containerCssClass: "drop-menu status_box_menu",
876 dropdownCssClass: "drop-menu-dropdown",
876 dropdownCssClass: "drop-menu-dropdown",
877 dropdownAutoWidth: true,
877 dropdownAutoWidth: true,
878 minimumResultsForSearch: -1
878 minimumResultsForSearch: -1
879 });
879 });
880 });
880 });
881 </script>
881 </script>
882
882
883
883
884 <script type="text/javascript">
884 <script type="text/javascript">
885 // TODO: switch this to pyroutes
885 // TODO: switch this to pyroutes
886 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
886 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
887
887
888 $(function(){
888 $(function(){
889 ReviewerAutoComplete('user');
889 ReviewerAutoComplete('user');
890
890
891 $('#open_edit_reviewers').on('click', function(e){
891 $('#open_edit_reviewers').on('click', function(e){
892 $('#open_edit_reviewers').hide();
892 $('#open_edit_reviewers').hide();
893 $('#close_edit_reviewers').show();
893 $('#close_edit_reviewers').show();
894 $('#add_reviewer_input').show();
894 $('#add_reviewer_input').show();
895 $('.reviewer_member_remove').css('visibility', 'visible');
895 $('.reviewer_member_remove').css('visibility', 'visible');
896 });
896 });
897
897
898 $('#close_edit_reviewers').on('click', function(e){
898 $('#close_edit_reviewers').on('click', function(e){
899 $('#open_edit_reviewers').show();
899 $('#open_edit_reviewers').show();
900 $('#close_edit_reviewers').hide();
900 $('#close_edit_reviewers').hide();
901 $('#add_reviewer_input').hide();
901 $('#add_reviewer_input').hide();
902 $('.reviewer_member_remove').css('visibility', 'hidden');
902 $('.reviewer_member_remove').css('visibility', 'hidden');
903 });
903 });
904
904
905 $('.show-inline-comments').on('change', function(e){
905 $('.show-inline-comments').on('change', function(e){
906 var show = 'none';
906 var show = 'none';
907 var target = e.currentTarget;
907 var target = e.currentTarget;
908 if(target.checked){
908 if(target.checked){
909 show = ''
909 show = ''
910 }
910 }
911 var boxid = $(target).attr('id_for');
911 var boxid = $(target).attr('id_for');
912 var comments = $('#{0} .inline-comments'.format(boxid));
912 var comments = $('#{0} .inline-comments'.format(boxid));
913 var fn_display = function(idx){
913 var fn_display = function(idx){
914 $(this).css('display', show);
914 $(this).css('display', show);
915 };
915 };
916 $(comments).each(fn_display);
916 $(comments).each(fn_display);
917 var btns = $('#{0} .inline-comments-button'.format(boxid));
917 var btns = $('#{0} .inline-comments-button'.format(boxid));
918 $(btns).each(fn_display);
918 $(btns).each(fn_display);
919 });
919 });
920
920
921 var commentTotals = {};
921 var commentTotals = {};
922 $.each(file_comments, function(i, comment) {
922 $.each(file_comments, function(i, comment) {
923 var path = $(comment).attr('path');
923 var path = $(comment).attr('path');
924 var comms = $(comment).children().length;
924 var comms = $(comment).children().length;
925 if (path in commentTotals) {
925 if (path in commentTotals) {
926 commentTotals[path] += comms;
926 commentTotals[path] += comms;
927 } else {
927 } else {
928 commentTotals[path] = comms;
928 commentTotals[path] = comms;
929 }
929 }
930 });
930 });
931 $.each(commentTotals, function(path, total) {
931 $.each(commentTotals, function(path, total) {
932 var elem = $('.comment-bubble[data-path="'+ path +'"]')
932 var elem = $('.comment-bubble[data-path="'+ path +'"]')
933 elem.css('visibility', 'visible');
933 elem.css('visibility', 'visible');
934 elem.html(elem.html() + ' ' + total );
934 elem.html(elem.html() + ' ' + total );
935 });
935 });
936
936
937 $('#merge_pull_request_form').submit(function() {
937 $('#merge_pull_request_form').submit(function() {
938 if (!$('#merge_pull_request').attr('disabled')) {
938 if (!$('#merge_pull_request').attr('disabled')) {
939 $('#merge_pull_request').attr('disabled', 'disabled');
939 $('#merge_pull_request').attr('disabled', 'disabled');
940 }
940 }
941 return true;
941 return true;
942 });
942 });
943
943
944 $('#update_pull_request').on('click', function(e){
944 $('#update_pull_request').on('click', function(e){
945 updateReviewers(undefined, "rhodecode-momentum", "720");
945 updateReviewers(undefined, "rhodecode-momentum", "720");
946 });
946 });
947
947
948 $('#update_commits').on('click', function(e){
948 $('#update_commits').on('click', function(e){
949 updateCommits("rhodecode-momentum", "720");
949 updateCommits("rhodecode-momentum", "720");
950 });
950 });
951
951
952 $('#close_pull_request').on('click', function(e){
952 $('#close_pull_request').on('click', function(e){
953 closePullRequest("rhodecode-momentum", "720");
953 closePullRequest("rhodecode-momentum", "720");
954 });
954 });
955 })
955 })
956 </script>
956 </script>
957
957
958 </div>
958 </div>
959 </div></div>
959 </div></div>
960
960
961 </div>
961 </div>
962
962
963
963
964 </%def>
964 </%def>
General Comments 0
You need to be logged in to leave comments. Login now