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