diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -156,15 +156,24 @@ class ChangesetController(BaseRepoContro c.ignorews_url = _ignorews_url c.context_url = _context_url c.fulldiff = fulldiff = request.GET.get('fulldiff') + + # fetch global flags of ignore ws or context lines + context_lcl = get_line_ctx('', request.GET) + ign_whitespace_lcl = get_ignore_ws('', request.GET) + + # diff_limit will cut off the whole diff if the limit is applied + # otherwise it will just hide the big files from the front-end + diff_limit = self.cut_off_limit_diff + file_limit = self.cut_off_limit_file + # get ranges of commit ids if preset commit_range = commit_id_range.split('...')[:2] - enable_comments = True + try: pre_load = ['affected_files', 'author', 'branch', 'date', 'message', 'parents'] if len(commit_range) == 2: - enable_comments = False commits = c.rhodecode_repo.get_commits( start_id=commit_range[0], end_id=commit_range[1], pre_load=pre_load) @@ -190,60 +199,45 @@ class ChangesetController(BaseRepoContro c.lines_deleted = 0 c.commit_statuses = ChangesetStatus.STATUSES - c.comments = [] - c.statuses = [] c.inline_comments = [] c.inline_cnt = 0 c.files = [] + c.statuses = [] + c.comments = [] + if len(c.commit_ranges) == 1: + commit = c.commit_ranges[0] + c.comments = ChangesetCommentsModel().get_comments( + c.rhodecode_db_repo.repo_id, + revision=commit.raw_id) + c.statuses.append(ChangesetStatusModel().get_status( + c.rhodecode_db_repo.repo_id, commit.raw_id)) + # comments from PR + statuses = ChangesetStatusModel().get_statuses( + c.rhodecode_db_repo.repo_id, commit.raw_id, + with_revisions=True) + prs = set(st.pull_request for st in statuses + if st is st.pull_request is not None) + + # from associated statuses, check the pull requests, and + # show comments from them + for pr in prs: + c.comments.extend(pr.comments) + # Iterate over ranges (default commit view is always one commit) for commit in c.commit_ranges: - if method == 'show': - c.statuses.extend([ChangesetStatusModel().get_status( - c.rhodecode_db_repo.repo_id, commit.raw_id)]) - - c.comments.extend(ChangesetCommentsModel().get_comments( - c.rhodecode_db_repo.repo_id, - revision=commit.raw_id)) - - # comments from PR - st = ChangesetStatusModel().get_statuses( - c.rhodecode_db_repo.repo_id, commit.raw_id, - with_revisions=True) - - # from associated statuses, check the pull requests, and - # show comments from them - - prs = set(x.pull_request for x in - filter(lambda x: x.pull_request is not None, st)) - for pr in prs: - c.comments.extend(pr.comments) - - inlines = ChangesetCommentsModel().get_inline_comments( - c.rhodecode_db_repo.repo_id, revision=commit.raw_id) - c.inline_comments.extend(inlines.iteritems()) - c.changes[commit.raw_id] = [] commit2 = commit commit1 = commit.parents[0] if commit.parents else EmptyCommit() - # fetch global flags of ignore ws or context lines - context_lcl = get_line_ctx('', request.GET) - ign_whitespace_lcl = get_ignore_ws('', request.GET) - _diff = c.rhodecode_repo.get_diff( commit1, commit2, ignore_whitespace=ign_whitespace_lcl, context=context_lcl) - - # diff_limit will cut off the whole diff if the limit is applied - # otherwise it will just hide the big files from the front-end - diff_limit = self.cut_off_limit_diff - file_limit = self.cut_off_limit_file - diff_processor = diffs.DiffProcessor( _diff, format='newdiff', diff_limit=diff_limit, file_limit=file_limit, show_full_diff=fulldiff) + commit_changes = OrderedDict() if method == 'show': _parsed = diff_processor.prepare() @@ -259,10 +253,15 @@ class ChangesetController(BaseRepoContro return None return get_node + inline_comments = ChangesetCommentsModel().get_inline_comments( + c.rhodecode_db_repo.repo_id, revision=commit.raw_id) + c.inline_cnt += len(inline_comments) + diffset = codeblocks.DiffSet( repo_name=c.repo_name, source_node_getter=_node_getter(commit1), target_node_getter=_node_getter(commit2), + comments=inline_comments ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id) c.changes[commit.raw_id] = diffset else: @@ -273,10 +272,6 @@ class ChangesetController(BaseRepoContro # sort comments by how they were generated c.comments = sorted(c.comments, key=lambda x: x.comment_id) - # count inline comments - for __, lines in c.inline_comments: - for comments in lines.values(): - c.inline_cnt += len(comments) if len(c.commit_ranges) == 1: c.commit = c.commit_ranges[0] diff --git a/rhodecode/lib/codeblocks.py b/rhodecode/lib/codeblocks.py --- a/rhodecode/lib/codeblocks.py +++ b/rhodecode/lib/codeblocks.py @@ -358,6 +358,7 @@ class DiffSet(object): source_nodes=None, target_nodes=None, max_file_size_limit=150 * 1024, # files over this size will # use fast highlighting + comments=None, ): self.highlight_mode = highlight_mode @@ -367,7 +368,7 @@ class DiffSet(object): self.source_nodes = source_nodes or {} self.target_nodes = target_nodes or {} self.repo_name = repo_name - + self.comments = comments or {} self.max_file_size_limit = max_file_size_limit def render_patchset(self, patchset, source_ref=None, target_ref=None): @@ -537,6 +538,8 @@ class DiffSet(object): original.lineno = before['old_lineno'] original.content = before['line'] original.action = self.action_to_op(before['action']) + original.comments = self.get_comments_for('old', + source_file, before['old_lineno']) if after: if after['action'] == 'new-no-nl': @@ -548,6 +551,8 @@ class DiffSet(object): modified.lineno = after['new_lineno'] modified.content = after['line'] modified.action = self.action_to_op(after['action']) + modified.comments = self.get_comments_for('new', + target_file, after['new_lineno']) # diff the lines if before_tokens and after_tokens: @@ -569,6 +574,20 @@ class DiffSet(object): return lines + def get_comments_for(self, version, file, line_number): + if hasattr(file, 'unicode_path'): + file = file.unicode_path + + if not isinstance(file, basestring): + return None + + line_key = { + 'old': 'o', + 'new': 'n', + }[version] + str(line_number) + + return self.comments.get(file, {}).get(line_key) + def get_line_tokens(self, line_text, line_number, file=None): filenode = None filename = None @@ -619,22 +638,26 @@ class DiffSet(object): if line.original: if line.original.action == ' ': yield (line.original.lineno, line.modified.lineno, - line.original.action, line.original.content) + line.original.action, line.original.content, + line.original.comments) continue if line.original.action == '-': yield (line.original.lineno, None, - line.original.action, line.original.content) + line.original.action, line.original.content, + line.original.comments) if line.modified.action == '+': buf.append(( None, line.modified.lineno, - line.modified.action, line.modified.content)) + line.modified.action, line.modified.content, + line.modified.comments)) continue if line.modified: yield (None, line.modified.lineno, - line.modified.action, line.modified.content) + line.modified.action, line.modified.content, + line.modified.comments) for b in buf: yield b diff --git a/rhodecode/public/css/code-block.less b/rhodecode/public/css/code-block.less --- a/rhodecode/public/css/code-block.less +++ b/rhodecode/public/css/code-block.less @@ -730,6 +730,7 @@ input.filediff-collapse-state { } } } + .filediff { border: 1px solid @grey5; @@ -785,12 +786,13 @@ input.filediff-collapse-state { .filediff-menu { float: right; - a, span { + &> a, &> span { padding: 5px; display: block; float: left } } + .pill { &[op="name"] { background: none; @@ -857,7 +859,87 @@ input.filediff-collapse-state { .filediff-collapsed .filediff-expand-button { display: inline; } + + @comment-padding: 5px; + + /**** COMMENTS ****/ + + .filediff-menu { + .show-comment-button { + display: none; + } + } + &.hide-comments { + .inline-comments { + display: none; + } + .filediff-menu { + .show-comment-button { + display: inline; + } + .show-comment-button { + display: none; + } + } + } + .inline-comments { + border-radius: @border-radius; + background: @grey6; + .comment { + margin: 0; + border-radius: @border-radius; + } + .comment-outdated { + opacity: 0.5; + } + .comment-inline { + background: white; + padding: (@comment-padding + 3px) @comment-padding; + border: @comment-padding solid @grey6; + + .text { + border: none; + } + .meta { + border-bottom: 1px solid @grey6; + padding-bottom: 10px; + } + } + .comment-selected { + border-left: 6px solid @comment-highlight-color; + } + .comment-inline-form { + padding: @comment-padding; + display: none; + } + .cb-comment-add-button { + margin: @comment-padding; + } + /* hide add comment button when form is open */ + .comment-inline-form-open + .cb-comment-add-button { + display: none; + } + .comment-inline-form-open { + display: block; + } + /* hide add comment button when form but no comments */ + .comment-inline-form:first-child + .cb-comment-add-button { + display: none; + } + /* hide add comment button when no comments or form */ + .cb-comment-add-button:first-child { + display: none; + } + /* hide add comment button when only comment is being deleted */ + .comment-deleting:first-child + .cb-comment-add-button { + display: none; + } + } + /**** END COMMENTS ****/ + } + + table.cb { width: 100%; border-collapse: collapse; @@ -956,6 +1038,27 @@ table.cb { font-family: @font-family-monospace; word-break: break-word; } + + &> button.cb-comment-box-opener { + padding: 2px 6px 2px 6px; + margin-left: -20px; + margin-top: -2px; + border-radius: @border-radius; + position: absolute; + display: none; + } + .cb-comment { + margin-top: 10px; + white-space: normal; + } + } + &:hover { + button.cb-comment-box-opener { + display: block; + } + &+ td button.cb-comment-box-opener { + display: block + } } &.cb-lineno { diff --git a/rhodecode/public/css/helpers.less b/rhodecode/public/css/helpers.less --- a/rhodecode/public/css/helpers.less +++ b/rhodecode/public/css/helpers.less @@ -19,6 +19,9 @@ a { cursor: pointer; } clear: both; } +.js-template { /* mark a template for javascript use */ + display: none; +} .linebreak { display: block; diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js --- a/rhodecode/public/js/src/rhodecode/comments.js +++ b/rhodecode/public/js/src/rhodecode/comments.js @@ -323,7 +323,7 @@ var bindToggleButtons = function() { }; var linkifyComments = function(comments) { - + /* TODO: dan: remove this - it should no longer needed */ for (var i = 0; i < comments.length; i++) { var comment_id = $(comments[i]).data('comment-id'); var prev_comment_id = $(comments[i - 1]).data('comment-id'); @@ -347,7 +347,7 @@ var linkifyComments = function(comments) } }; - + /** * Iterates over all the inlines, and places them inside proper blocks of data */ diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -114,6 +114,218 @@ c.template_context['visual']['default_re rhodecode_edition: '${c.rhodecode_edition}' } }; + + +Rhodecode = (function() { + function _Rhodecode() { + this.comments = new (function() { /* comments controller */ + var self = this; + + this.cancelComment = function(node) { + var $node = $(node); + var $td = $node.closest('td'); + $node.closest('.comment-inline-form').removeClass('comment-inline-form-open'); + return false; + } + this.getLineNumber = function(node) { + var $node = $(node); + return $node.closest('td').attr('data-line-number'); + } + this.scrollToComment = function(node, offset) { + if (!node) { + node = $('.comment-selected'); + if (!node.length) { + node = $('comment-current') + } + } + $comment = $(node).closest('.comment-current'); + $comments = $('.comment-current'); + + $('.comment-selected').removeClass('comment-selected'); + + var nextIdx = $('.comment-current').index($comment) + offset; + if (nextIdx >= $comments.length) { + nextIdx = 0; + } + var $next = $('.comment-current').eq(nextIdx); + var $cb = $next.closest('.cb'); + $cb.removeClass('cb-collapsed') + + var $filediffCollapseState = $cb.closest('.filediff').prev(); + $filediffCollapseState.prop('checked', false); + $next.addClass('comment-selected'); + scrollToElement($next); + return false; + } + this.nextComment = function(node) { + return self.scrollToComment(node, 1); + } + this.prevComment = function(node) { + return self.scrollToComment(node, -1); + } + this.deleteComment = function(node) { + if (!confirm(_gettext('Delete this comment?'))) { + return false; + } + var $node = $(node); + var $td = $node.closest('td'); + var $comment = $node.closest('.comment'); + var comment_id = $comment.attr('data-comment-id'); + var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id); + var postData = { + '_method': 'delete', + 'csrf_token': CSRF_TOKEN + }; + + $comment.addClass('comment-deleting'); + $comment.hide('fast'); + + var success = function(response) { + $comment.remove(); + return false; + }; + var failure = function(data, textStatus, xhr) { + alert("error processing request: " + textStatus); + $comment.show('fast'); + $comment.removeClass('comment-deleting'); + return false; + }; + ajaxPOST(url, postData, success, failure); + } + this.createComment = function(node) { + var $node = $(node); + var $td = $node.closest('td'); + var $form = $td.find('.comment-inline-form'); + + if (!$form.length) { + var tmpl = $('#cb-comment-inline-form-template').html(); + var f_path = $node.closest('.filediff').attr('data-f-path'); + var lineno = self.getLineNumber(node); + tmpl = tmpl.format(f_path, lineno); + $form = $(tmpl); + + var $comments = $td.find('.inline-comments'); + if (!$comments.length) { + $comments = $( + $('#cb-comments-inline-container-template').html()); + $td.append($comments); + } + + $td.find('.cb-comment-add-button').before($form); + + var pullRequestId = templateContext.pull_request_data.pull_request_id; + var commitId = templateContext.commit_data.commit_id; + var _form = $form[0]; + var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false); + var cm = commentForm.getCmInstance(); + + // set a CUSTOM submit handler for inline comments. + commentForm.setHandleFormSubmit(function(o) { + var text = commentForm.cm.getValue(); + + if (text === "") { + return; + } + + if (lineno === undefined) { + alert('missing line !'); + return; + } + if (f_path === undefined) { + alert('missing file path !'); + return; + } + + var excludeCancelBtn = false; + var submitEvent = true; + commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); + commentForm.cm.setOption("readOnly", true); + var postData = { + 'text': text, + 'f_path': f_path, + 'line': lineno, + 'csrf_token': CSRF_TOKEN + }; + var submitSuccessCallback = function(json_data) { + $form.remove(); + console.log(json_data) + try { + var html = json_data.rendered_text; + var lineno = json_data.line_no; + var target_id = json_data.target_id; + + $comments.find('.cb-comment-add-button').before(html); + console.log(lineno, target_id, $comments); + + } catch (e) { + console.error(e); + } + + + // re trigger the linkification of next/prev navigation + linkifyComments($('.inline-comment-injected')); + timeagoActivate(); + bindDeleteCommentButtons(); + commentForm.setActionButtonsDisabled(false); + + }; + var submitFailCallback = function(){ + commentForm.resetCommentFormState(text) + }; + commentForm.submitAjaxPOST( + commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback); + }); + + setTimeout(function() { + // callbacks + if (cm !== undefined) { + cm.focus(); + } + }, 10); + + $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({ + form: _form, + parent: $td[0], + lineno: lineno, + f_path: f_path} + ); + } + + $form.addClass('comment-inline-form-open'); + } + + this.renderInlineComments = function(file_comments) { + show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true; + + for (var i = 0; i < file_comments.length; i++) { + var box = file_comments[i]; + + var target_id = $(box).attr('target_id'); + + // actually comments with line numbers + var comments = box.children; + + for (var j = 0; j < comments.length; j++) { + var data = { + 'rendered_text': comments[j].outerHTML, + 'line_no': $(comments[j]).attr('line'), + 'target_id': target_id + }; + } + } + + // since order of injection is random, we're now re-iterating + // from correct order and filling in links + linkifyComments($('.inline-comment-injected')); + bindDeleteCommentButtons(); + firefoxAnchorFix(); + }; + + })(); + } + return new _Rhodecode(); +})(); + <%include file="/base/plugins_base.html"/>
${ungettext("%d Pull Request Comment", "%d Pull Request Comments", len(c.comments)) % len(c.comments)}
- %else: -${ungettext("%d Commit Comment", "%d Commit Comments", len(c.comments)) % len(c.comments)}
- %endif - %for path, lines_comments in c.inline_comments: - % for line, comments in lines_comments.iteritems(): -